Variables, types and pointers

Variables let a program remember values: a name, a score, a price. In this lesson you’ll declare variables with var and :=, learn Go’s basic types and their zero values, convert between types, and finish with pointers, which let you share a variable instead of copying it.

You should know the structure of a Go program from Hello, world and how to run one from Build and run a Go program. Every example here is a complete program you can paste into a main.go file and run with go run ..

What is a variable?

A variable is a named place in memory that holds a value. In Go, every variable has three things:

  • a name, so you can refer to it, such as score;
  • a type, which says what kind of value it holds, such as a whole number or a piece of text;
  • a value, the data currently stored in it, which you can change.

The type is fixed. Once a variable is a number, it stays a number. That’s what people mean when they say Go is statically typed, and it lets the compiler catch a lot of mistakes for you.

Declaring variables with var

The long form uses the keyword var, then the name, then the type:

package main

import "fmt"

func main() {
	var language string
	language = "Go"

	var year int = 2009

	fmt.Println(language, year)
}Code language: Go (go)
Go 2009Code language: plaintext (plaintext)

var language string creates a variable called language that holds a string. The next line assigns a value to it with =. var year int = 2009 declares and assigns in one go.

When you assign a value right away, Go can work out the type itself, so you can leave it out: var year = 2009 also makes year an int. This is called type inference.

The short form: :=

Inside a function you’ll mostly see the short variable declaration, :=. It declares the variable and infers its type from the value, without the word var:

package main

import "fmt"

func main() {
	language := "Go"
	year := 2009
	language = "Golang"

	fmt.Println(language, year)
}Code language: Go (go)
Golang 2009Code language: plaintext (plaintext)

Note the difference: := creates a new variable, = changes an existing one. Use := once per variable; after that, assign with =. If you write := again for a variable that already exists, Go stops with no new variables on left side of :=.

Which one should you use? I use := almost everywhere inside functions. I reach for var when I want a variable that starts at its zero value (next section), when I want a specific type that inference wouldn’t pick, and outside functions, where := isn’t allowed.

Zero values

In some languages, a variable you declare without a value contains garbage or is “undefined”. Go doesn’t have that problem. Every variable starts with the zero value of its type:

package main

import "fmt"

func main() {
	var count int
	var price float64
	var name string
	var done bool

	fmt.Println(count, price, done)
	fmt.Printf("%q\n", name)
}Code language: Go (go)
0 0 false
""Code language: plaintext (plaintext)

Numbers start at 0, booleans at false, and strings at "", the empty string. An empty string prints as nothing at all, so I used fmt.Printf with the %q verb, which prints a string with quotes around it. (Printf takes a format string first; each % verb in it is replaced by the next value.)

Zero values are useful: a counter can start at zero without you saying so, and many Go types are designed so their zero value is ready to use.

Basic types

You’ll use these four types most of the time:

TypeHoldsExampleZero value
intwhole numbers, positive or negative42, -70
float64numbers with a fractional part3.14, -0.50
stringtext"hello"""
booltrue or falsetruefalse

Go has more numeric types, and you’ll meet them in other people’s code:

  • int8, int16, int32, int64: integers with an exact size in bits. Use them when a file format, network protocol or database column demands that size, or when you store millions of numbers and memory matters. Plain int is 64 bits on 64-bit systems and 32 bits on 32-bit ones.
  • uint, uint8 … uint64: unsigned integers, which can’t be negative. You need them less often than you’d think; don’t use uint just because a value “can’t be negative”.
  • byte is another name for uint8, used for raw data. rune is another name for int32, used for a single Unicode character. Both come back in Strings.
  • float32 uses half the memory of float64 but is much less precise. Use float64 unless you have a reason not to.

My rule of thumb: int for counting and indexes, float64 for measurements, and the sized types only when something outside your program dictates them.

What type did Go infer?

With := the type comes from the value. The %T verb prints a value’s type, and %v prints the value itself:

package main

import "fmt"

func main() {
	a := 42
	b := 3.14
	c := "hello"
	d := true

	fmt.Printf("%v is a %T\n", a, a)
	fmt.Printf("%v is a %T\n", b, b)
	fmt.Printf("%v is a %T\n", c, c)
	fmt.Printf("%v is a %T\n", d, d)
}Code language: Go (go)
42 is a int
3.14 is a float64
hello is a string
true is a boolCode language: plaintext (plaintext)

A whole number becomes an int, a number with a decimal point becomes a float64. So x := 5 and x := 5.0 give you different types.

Converting between types

Go never converts between types behind your back. Multiplying an int by a float64 is an error:

package main

import "fmt"

func main() {
	count := 3
	price := 1.5
	total := count * price
	fmt.Println(total)
}Code language: Go (go)
./main.go:8:11: invalid operation: count * price (mismatched types int and float64)Code language: plaintext (plaintext)

That can feel strict at first, but it means you always know what happens to your numbers. To convert, write the type name like a function call: float64(count).

package main

import (
	"fmt"
	"strconv"
)

func main() {
	count := 3
	price := 1.5
	total := float64(count) * price
	fmt.Println(total)

	f := 3.9
	fmt.Println(int(f))

	n := 42
	s := strconv.Itoa(n)
	fmt.Println(s + "!")
}Code language: Go (go)
4.5
3
42!Code language: plaintext (plaintext)

Two things to notice. Converting a float64 to an int cuts off the fraction; it doesn’t round, so 3.9 becomes 3. And to turn a number into text, use strconv.Itoa (“integer to ASCII”) from the strconv package. The + between two strings glues them together.

string(n) looks like it should turn 42 into "42", but it doesn’t: it gives you the character with that Unicode number (65 becomes "A"). It’s such a common mistake that go vet reports it: conversion from int to string yields a string of one rune, not a string of digits.

Declared and not used

Go refuses to compile a program with a local variable you never use:

package main

import "fmt"

func main() {
	name := "Gopher"
	age := 12
	fmt.Println(name)
}Code language: Go (go)
./main.go:7:2: declared and not used: ageCode language: plaintext (plaintext)

An unused variable is often a bug: you computed something and then forgot to use it, or you used the wrong name. Go makes you fix it, either by using the variable or by deleting it. While you’re experimenting, you can silence the error temporarily by assigning the value to the blank identifier, _ = age, but don’t leave that in finished code.

Several variables at once

You can declare or assign several variables in one statement, separated by commas. Go evaluates everything on the right first, then assigns, which makes swapping two values a one-liner. For a group of var declarations, use parentheses:

package main

import "fmt"

func main() {
	x, y := 1, 2
	x, y = y, x
	fmt.Println(x, y)

	var (
		city     = "Amsterdam"
		visitors int
	)
	fmt.Println(city, visitors)
}Code language: Go (go)
2 1
Amsterdam 0Code language: plaintext (plaintext)

Scope: where a variable exists

A variable exists from where you declare it until the closing brace of the block it’s in. A block is anything between { and }. Variables declared outside any function, at package level, can be used everywhere in the package. Package-level variables must use var; := only works inside functions.

package main

import "fmt"

var greeting = "Hello"

func main() {
	{
		name := "Gopher"
		fmt.Println(greeting, name)
	}
	fmt.Println(name)
}Code language: Go (go)
./main.go:12:14: undefined: nameCode language: plaintext (plaintext)

greeting is visible everywhere, but name stopped existing at the inner closing brace. You’ll mostly see blocks as part of if, for and functions, and the same rule applies there. Keep variables in the smallest scope that works, and use package-level variables sparingly: any code in the package can change them, which makes bugs harder to track down.

Pointers

Every variable lives somewhere in your computer’s memory, at an address. A pointer is a variable that holds such an address. Two operators do all the work:

  • &x gives you the address of x: a pointer to x.
  • *p follows the pointer p to the value it points at, to read it or to change it.
package main

import "fmt"

func main() {
	score := 10
	p := &score

	fmt.Println(*p)
	*p = 20
	fmt.Println(score)
	fmt.Printf("%T\n", p)
}Code language: Go (go)
10
20
*intCode language: plaintext (plaintext)
A pointer holds the address of another variable The variable score of type int holds the value 10. The variable p of type pointer to int holds the address of score, drawn as an arrow from p to score. Reading or writing *p reads or writes the value in score. variable p *int holds the address of score p := &score variable score int 10 *p reads or writes this value &score gives its address
p doesn’t hold a copy of 10. It holds the address of score, so changing *p changes score.

The type of p is *int, “pointer to int”. A * in front of a type means “pointer to”; a * in front of a variable means “follow the pointer”. After *p = 20, score is 20, because p and score refer to the same spot in memory.

If you print the pointer itself, you get the address, which is different every run and on every machine:

package main

import "fmt"

func main() {
	score := 10
	p := &score
	fmt.Println(p)
}Code language: Go (go)
0x5992597ae008Code language: plaintext (plaintext)

Why pointers exist

When you pass a variable to a function, Go passes a copy of its value. The function can change its copy all it wants; your original stays the same. If you want a function to change your variable, you pass a pointer to it. Functions get their own lesson in Functions, but this example is small enough to follow now:

package main

import "fmt"

func addBonus(points int) {
	points = points + 5
}

func addBonusPtr(points *int) {
	*points = *points + 5
}

func main() {
	score := 10
	addBonus(score)
	fmt.Println(score)

	addBonusPtr(&score)
	fmt.Println(score)
}Code language: Go (go)
10
15Code language: plaintext (plaintext)

addBonus added 5 to its own copy, which was thrown away. addBonusPtr got the address of score and changed the real thing. The second reason for pointers is size: passing a pointer to a large value avoids copying all of it. You’ll see that again with structs and methods.

This is also why reading input with fmt.Scan(&name) needs the &: Scan has to store what the user typed in your variable, so it needs its address.

nil: a pointer to nothing

The zero value of a pointer is nil: it doesn’t point anywhere. You can compare a pointer with nil, but following a nil pointer crashes your program:

package main

import "fmt"

func main() {
	var p *int
	fmt.Println(p == nil)
	fmt.Println(*p)
}Code language: Go (go)
true
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x2 addr=0x0 pc=0x10448b594]

goroutine 1 [running]:
main.main()
…Code language: plaintext (plaintext)

A panic stops the program with an error message and a trace of where it happened. The compiler can’t catch this one, so before you follow a pointer that might be nil, check it. You’ll learn how with if.

You can also get a pointer to a brand-new variable with the built-in new. new(int) returns a pointer to an int set to its zero value. Since Go 1.26 you can pass a value instead of a type, so new(42) returns a pointer to an int that holds 42:

package main

import "fmt"

func main() {
	a := new(int)
	b := new(42)
	*a = *a + 1
	fmt.Println(*a, *b)
}Code language: Go (go)
1 42Code language: plaintext (plaintext)

If you’ve used C: Go has no pointer arithmetic, so you can’t step a pointer through memory, and Go’s garbage collector frees memory when nothing points to it anymore. It’s fine for a function to return a pointer to one of its local variables.

Practice

1. Store a temperature of 21.5 degrees Celsius in a variable, convert it to Fahrenheit (multiply by 9, divide by 5, add 32), and print both. Then store a whole number of minutes, 135, and print it as hours and minutes. Hint: for integers, / divides without a fraction and % gives the remainder.

Show a solution
package main

import "fmt"

func main() {
	celsius := 21.5
	fahrenheit := celsius*9/5 + 32
	fmt.Println(celsius, "C is", fahrenheit, "F")

	total := 135
	hours, minutes := total/60, total%60
	fmt.Println(total, "minutes is", hours, "h", minutes, "m")
}Code language: Go (go)
21.5 C is 70.7 F
135 minutes is 2 h 15 mCode language: plaintext (plaintext)

2. Create a variable lives := 3 and a pointer p to it. Using only p (not lives), take one life away. Then print lives and check whether p is nil.

Show a solution
package main

import "fmt"

func main() {
	lives := 3
	p := &lives
	*p = *p - 1
	fmt.Println("lives:", lives)
	fmt.Println("p is nil:", p == nil)
}Code language: Go (go)
lives: 2
p is nil: falseCode language: plaintext (plaintext)

Key takeaways

  • Declare with var name type, or with name := value inside functions; = assigns to an existing variable.
  • Every variable starts at its type’s zero value: 0, "", false or nil.
  • Use int, float64, string and bool by default; sized types when something external requires them.
  • Go never converts types implicitly: write float64(n), and use strconv.Itoa for number-to-text.
  • Unused local variables are a compile error, and a variable only exists inside its block.
  • &x gives a pointer to x, *p reads or writes through it; a nil pointer points nowhere and crashes if you follow it.