Skip to main content
Go · lesson 3 of 25
Go beginner Lesson 3 of 25

Variables in Go

Go variables run for real: var vs :=, zero values, the unused-variable compile error, multiple assignment and swaps, iota constants, and := shadowing.

By SkillByExample · Updated

Go has two ways to declare a variable, a zero value for every type, and a compiler that refuses to build code with unused locals. Each example below is a complete program; the output under it is what go run printed.

var and :=

var names the type (or lets Go infer it); := declares and infers in one step, and only works inside a function.

package main

import "fmt"

var appName = "inventory" // package level: var only, := is not allowed here

func main() {
	var age int = 25
	var score = 98.5 // type inferred: float64
	name := "Alice"  // short declaration: string
	active := true

	fmt.Println(appName, age, score, name, active)
	fmt.Printf("%T %T %T %T\n", age, score, name, active)
}
$ go run declare.go
inventory 25 98.5 Alice true
int float64 string bool

%T prints a value’s type. 98.5 became float64 and 25 became int: an untyped constant takes its default type when nothing else pins it down, so you never get float32 or int64 by accident.

Zero values

A variable declared without a value is not garbage; it is the zero value of its type.

package main

import "fmt"

func main() {
	var i int
	var f float64
	var b bool
	var s string
	var p *int
	var sl []int
	var m map[string]int

	fmt.Println(i, f, b, p, sl, m)
	fmt.Printf("%q\n", s)
	fmt.Println(sl == nil, m == nil, len(sl), len(m), m["missing"])

	var total int // usable immediately: starts at 0
	for n := range 5 {
		total += n
	}
	fmt.Println(total)
}
$ go run zero.go
0 0 false <nil> [] map[]
""
true true 0 0 0
10

The empty string prints as nothing, so %q shows it quoted. A nil slice prints as [] and a nil map as map[], and both have length 0 — reading from a nil map even returns the zero value. Writing to a nil map is the one thing that panics; the maps lesson shows it.

Unused variables don’t compile

Go treats an unused local variable as an error, not a warning.

package main

import "fmt"

func main() {
	count := 10
	name := "Gopher"
	fmt.Println(name)
}
$ go run unused.go
# command-line-arguments
./unused.go:6:2: declared and not used: count

The build fails before anything runs: # command-line-arguments is how the compiler names a package built from files listed on the command line, and the error points at line 6, column 2. Delete count, use it, or — if a function returns a value you genuinely do not need — assign it to the blank identifier _, which discards it.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	n, _ := strconv.Atoi("42") // ignore the error on purpose
	fmt.Println(n + 1)

	for _, name := range []string{"Ada", "Linus"} { // ignore the index
		fmt.Println(name)
	}
}
$ go run blank.go
43
Ada
Linus

Multiple assignment

Several variables can be declared or assigned in one statement, and the right-hand side is evaluated completely before anything is stored.

package main

import "fmt"

func divmod(a, b int) (int, int) {
	return a / b, a % b
}

func main() {
	var x, y, z = 1, 2, 3
	a, b := 10, 20
	a, b = b, a // swap: both values are read before either is written
	q, r := divmod(17, 5)

	fmt.Println(x, y, z)
	fmt.Println(a, b)
	fmt.Println(q, r)
}
$ go run multi.go
1 2 3
20 10
3 2

The swap needs no temporary because b, a is evaluated into two values first. The same mechanism carries a function’s multiple return values, which is how Go returns a result together with an error.

:= needs at least one new name

:= may reuse existing names as long as at least one name on the left is new.

package main

import "fmt"

func main() {
	x := 10
	x, y := 30, 40 // fine: y is new, x is just assigned
	fmt.Println(x, y)
	x, y := 50, 60 // nothing new on the left
	fmt.Println(x, y)
}
$ go run redeclare.go
# command-line-arguments
./redeclare.go:9:7: no new variables on left side of :=

The first := compiles because y is new, so x is merely reassigned. The second has no new names and is rejected; use = there.

Shadowing: := in an inner block

The flip side: inside a new block, := always creates a fresh variable, even when the name exists outside.

package main

import "fmt"

func main() {
	limit := 10
	if true {
		limit := 99 // a new variable that shadows the outer one
		limit++
		fmt.Println("inner:", limit)
	}
	fmt.Println("outer:", limit)

	count := 0
	for range 3 {
		count = count + 1 // = assigns to the outer variable
	}
	fmt.Println("count:", count)
}
$ go run shadow.go
inner: 100
outer: 10
count: 3

The outer limit never changed: the if block had its own. This is legal Go and a common source of “my variable didn’t update” bugs, especially with err inside if blocks. count shows the fix — plain = assigns to the variable that already exists.

Constants and iota

const values are fixed at compile time. In a const block, iota counts lines from 0, and a line with no expression repeats the previous one.

package main

import "fmt"

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
)

const (
	_  = iota             // skip 0
	KB = 1 << (10 * iota) // 1 << 10
	MB                    // 1 << 20
	GB                    // 1 << 30
)

func main() {
	fmt.Println(Sunday, Monday, Tuesday)
	fmt.Println(KB, MB, GB)
	fmt.Printf("%T\n", Tuesday)
}
$ go run consts.go
0 1 2
1024 1048576 1073741824
main.Weekday

Monday and Tuesday have no expression, so they repeat Weekday = iota with iota at 1 and 2. Likewise MB and GB reuse 1 << (10 * iota). The constants carry the named type Weekday, so a function taking a Weekday won’t silently accept any int variable.

Untyped constants are flexible

A constant without a type is just a number until it is used, so it fits any type that can hold it.

package main

import "fmt"

const big = 1_000_000 // untyped integer constant

func main() {
	var a int32 = big
	var b float64 = big
	var c uint8 = 200
	fmt.Println(a, b, c)

	r := 'A' // a rune, not a string
	fmt.Printf("%T %v %c\n", r, r, r)
}
$ go run untyped.go
1000000 1e+06 200
int32 65 A

big became an int32 in one place and a float64 in another without a conversion; Println formats that large float as 1e+06. Single quotes make a rune — an alias for int32 — so r prints as 65 with %v and as A with %c. Change c to uint8 = 300 and the compiler rejects it: cannot use 300 (untyped int constant) as uint8 value in variable declaration (overflows).

Practice

1. In zero.go, add a var e error and a var fn func() and print whether each is nil.

Solution
package main

import "fmt"

func main() {
	var e error
	var fn func()
	fmt.Println(e == nil, fn == nil)
	fmt.Println(e)
}
$ go run p1.go
true true
<nil>

Interfaces (error is one) and function values have nil as their zero value too. Calling fn() here would panic with a nil dereference.

2. Fix redeclare.go so it compiles and prints 30 40 then 50 60.

Solution
package main

import "fmt"

func main() {
	x := 10
	x, y := 30, 40
	fmt.Println(x, y)
	x, y = 50, 60 // assignment, not declaration
	fmt.Println(x, y)
}
$ go run p2.go
30 40
50 60

Both names already exist, so the second statement must use =.

3. Extend the Weekday block in consts.go through Saturday, and print Saturday and the number of weekdays as int(Saturday) + 1.

Solution
package main

import "fmt"

type Weekday int

const (
	Sunday Weekday = iota
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

func main() {
	fmt.Println(Saturday, int(Saturday)+1)
}
$ go run p3.go
6 7

Each new line repeats Weekday = iota, so Saturday is 6 and the count is 7.

4. Rotate three variables a, b, c := 1, 2, 3 left in one statement so they become 2 3 1.

Solution
package main

import "fmt"

func main() {
	a, b, c := 1, 2, 3
	a, b, c = b, c, a
	fmt.Println(a, b, c)
}
$ go run p4.go
2 3 1

All three right-hand values are read before any assignment happens, so no temporary variable is needed.

Frequently Asked Questions

What is the difference between var and := in Go?
var works anywhere, at package level or inside a function, and lets you name the type explicitly or leave the value out to get the zero value. := declares and assigns in one step, infers the type from the value, and is only allowed inside functions.
What is a zero value in Go?
Every variable declared without an initial value is set to its type's zero value: 0 for numbers, false for bool, the empty string for string, and nil for pointers, slices, maps, channels, functions and interfaces. Go never exposes uninitialised memory.
Can I declare a variable in Go and not use it?
Not a local one. The compiler rejects an unused local variable with the error 'declared and not used'. Assign the value to the blank identifier _ if you deliberately want to discard it. Unused package-level variables are allowed.
What does iota do in a Go const block?
iota is a counter that starts at 0 on the first line of each const block and increases by one on every line. A line with no expression repeats the previous expression with the new iota, which makes enumerations and bit-size constants short to write.
Why did my variable not change after an if block in Go?
Using := inside the inner block declared a new variable with the same name that shadows the outer one, so the assignment went to the inner copy. Use = to assign to the existing variable, or run go vet with the shadow analyzer to find such cases.