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:
| Type | Holds | Example | Zero value |
|---|---|---|---|
int | whole numbers, positive or negative | 42, -7 | 0 |
float64 | numbers with a fractional part | 3.14, -0.5 | 0 |
string | text | "hello" | "" |
bool | true or false | true | false |
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. Plainintis 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 useuintjust because a value “can’t be negative”.byteis another name foruint8, used for raw data.runeis another name forint32, used for a single Unicode character. Both come back in Strings.float32uses half the memory offloat64but is much less precise. Usefloat64unless 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:
&xgives you the address ofx: a pointer tox.*pfollows the pointerpto 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)
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 withname := valueinside functions;=assigns to an existing variable. - Every variable starts at its type’s zero value:
0,"",falseornil. - Use
int,float64,stringandboolby default; sized types when something external requires them. - Go never converts types implicitly: write
float64(n), and usestrconv.Itoafor number-to-text. - Unused local variables are a compile error, and a variable only exists inside its block.
&xgives a pointer tox,*preads or writes through it; a nil pointer points nowhere and crashes if you follow it.