Go · 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.