Variables, Constants and iota

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Go 1.27 (go1.27.2) and was checked on 2026-10-18. Every program below was compiled with that toolchain, passes gofmt and go vet, and was run: the output shown is the real output. All of them belong to one scratch module whose go.mod is:

module example.com/shelf

go 1.27

A variable is a named storage location holding a value of one static type; a constant is a name for a value that is known at compile time and can never change. Go keeps both deliberately plain: there is no let, no final, no mutable, and no implicit conversion between numeric types. What makes Go constants unusual is that most of them are untyped and computed with arbitrary precision, and that iota turns a block of constants into an enumeration. The types themselves are the subject of Types and Values; operators are in Operators and Expressions. The examples use the shelf library-catalogue scenario (Book, ISBN, LoanState), the same domain as the Rust Reference’s shelf workspace, so the two languages can be compared on the same types (see Variables, Mutability & Constants and Constants, Enumerations & Initialization in C).

Declaring variables

The full form is var name Type = expression, and either the type or the expression (but not both) may be omitted. When the type is omitted it is inferred from the expression; when the expression is omitted the variable gets the zero value of its type. Several names can be declared at once, and a parenthesised var ( ... ) block groups declarations.

Form Meaning

var n int

type only: n is 0

var n = 42

value only: the type is the default type of the constant, here int

var n int64 = 42

type and value: the constant is converted to int64

var a, b = "x", 7

several names; each gets its own inferred type

var a, b int = 1, 2

several names of one type

var a, b = f()

several names initialized from one multi-valued call

n := 42

short variable declaration (functions only), see below

package main

import "fmt"

func main() {
	var a int           // type only: zero value
	var b = 42          // value only: type inferred (int)
	var c float32 = 1.5 // type and value
	var d, e = "x", 7   // several at once, types inferred separately
	var f, g int = 1, 2 // several of one type
	var (
		h = 'x'     // untyped rune constant: default type is rune (int32)
		i = 2.0     // untyped floating-point constant: float64
		j = 3i      // untyped complex constant: complex128
		k = 1 << 10 // untyped integer constant: int
	)
	fmt.Printf("%T %T %T %T %T %T\n", a, b, c, d, e, f+g)
	fmt.Printf("%T %T %T %T\n", h, i, j, k)

	l := []int{1, 2, 3} // short variable declaration: only inside functions
	m, n := len(l), cap(l)
	fmt.Println(m, n)
}
int int float32 string int int
int32 float64 complex128 int
3 3

The default types of untyped constants — int for integers, rune (int32) for rune literals, float64 for floating-point and complex128 for imaginary — are explained in Typed and untyped constants.

Zero values

Go has no uninitialized variables. Every variable is set to its type’s zero value when it is created: false for booleans, 0 for numbers, "" for strings, and nil for pointers, functions, interfaces, slices, channels and maps. The elements of an array and the fields of a struct are zeroed recursively. This is a design rule that library authors lean on: types are written so that their zero value is ready to use (sync.Mutex, bytes.Buffer, a nil slice that append can grow). Reading a nil map or slice is safe; writing to a nil map panics.

package main

import (
	"fmt"

	"example.com/shelf/catalog"
)

// Package-level variables: declared with var, never with :=.
var (
	count   int
	title   string
	ratio   float64
	open    bool
	book    catalog.Book
	authors []catalog.Author
	index   map[catalog.ISBN]catalog.Book
	state   catalog.LoanState
	err     error
)

func main() {
	fmt.Printf("%d %q %v %t\n", count, title, ratio, open)
	fmt.Printf("%+v\n", book)
	fmt.Println(authors == nil, len(authors), index == nil, len(index))
	fmt.Println(state, err == nil)
	defer func() { fmt.Println("recovered:", recover()) }()
	index["x"] = book // reading a nil map is fine; writing to it panics
}
0 "" 0 false
{ISBN: Title: Authors:[]}
true 0 true 0
available true
recovered: assignment to entry in nil map

The zero value of catalog.LoanState is Available because the enumeration below makes its first constant the zero value — a deliberate choice (see iota enumerations). A complete table of zero values by type is on the Types and Values page.

Short variable declarations

Inside a function, name := expression declares and initializes a variable in one step. It is shorthand for a var declaration without a type, and it is the form used for the great majority of local variables.

  • It is allowed only inside functions. At package level every declaration must start with a keyword (var, const, type, func); the compiler reports non-declaration statement outside function body.

  • The left side may list several names. At least one non-blank name must be new in the current scope; the others are simply assigned (redeclaration). The variable that is reused keeps its original type.

  • The new variables take the types of the right-hand side. An untyped constant takes its default type.

  • A := in a new scope (an if, for or switch header, a block) declares new variables even when the names exist in an outer scope. That is the root of the shadowing pitfall below.

package main

import (
	"fmt"
	"strconv"
)

func main() {
	n, err := strconv.Atoi("42") // declares n and err
	fmt.Println(n, err)

	m, err := strconv.Atoi("x") // m is new, err is reused (assigned, not redeclared)
	fmt.Println(m, err)

	if m, err := strconv.Atoi("7"); err == nil { // new scope: both are new variables
		fmt.Println("inner", m)
	}
	fmt.Println("outer", m, err != nil)
}
42 <nil>
0 strconv.Atoi: parsing "x": invalid syntax
inner 7
outer 0 true

The compiler enforces each rule with a precise message:

package main

count := 0

func main() {}
# example.com/shelf/ref/err/shortdecl-pkg
./main.go:3:1: syntax error: non-declaration statement outside function body
package main

import "fmt"

func main() {
	a, b := 1, 2
	a, b := 3, 4
	fmt.Println(a, b)
}
# example.com/shelf/ref/err/no-new-vars
./main.go:7:7: no new variables on left side of :=
package main

import "fmt"

func main() {
	n := 1
	n = "one"
	fmt.Println(n)
}
# example.com/shelf/ref/err/type-change
./main.go:7:6: cannot use "one" (untyped string constant) as int value in assignment

The last one shows static typing at work: n := 1 fixes the type of n as int for good, and Go never changes it because of a later assignment.

var or :=?

Situation Prefer

a local variable with an initial value

:= (title := book.Title)

a local variable that starts at its zero value

var buf bytes.Buffer, var total int

a local that must have a type different from the value’s default

var ratio float32 = 0.5, or ratio := float32(0.5)

a package-level variable

var (the only choice)

declaring several results of a call

v, err := f()

declaring a variable that outlives an if or for header

var before the header, or declare it in the enclosing block

The zero-value form var total int signals "this starts empty and will be filled in", which total := 0 does not say as clearly; both compile to the same thing.

Package-level variables and initialization

A variable declared outside any function lives for the whole run of the program and is visible throughout its package (and to other packages if its name starts with an upper-case letter). Package-level variables are initialized before main runs, in dependency order, not in the textual order of the source: the next variable to initialize is the first one, in declaration order, that has no uninitialized dependencies. A dependency is a reference to a variable or to a function whose body (transitively) references the variable.

package main

import "fmt"

var (
	a = c + b // depends on c and b, so they are initialized first
	b = f()   // depends on f, which depends on d
	c = f()
	d = 3
)

func f() int {
	d++
	fmt.Println("f called, d =", d)
	return d
}

func init() { fmt.Println("init:", a, b, c, d) }

func main() { fmt.Println("main:", a, b, c, d) }
f called, d = 4
f called, d = 5
init: 9 4 5 5
main: 9 4 5 5

a is declared first but depends on b and c, which depend on f, which uses d; so d is initialized first, then b, then c, then a. The init function runs after all variables of the package are initialized. Go 1.21 made this procedure precise for the whole program (imported packages are initialized first, then sorted by import path where they are independent); the details, including multiple init functions per file and the order across files, are on Packages, Imports and Initialization.

Package-level state is shared by every goroutine, so mutable globals need synchronisation (see Sync and Atomics); a package-level var initialized by a function call (var ErrInvalidISBN = errors.New(...)) is the standard way to declare sentinel errors.

Unused variables and imports, and the blank identifier

Go refuses to compile a function that declares a local variable and never uses it, and a file that imports a package it never refers to. The rule exists because dead variables and imports are almost always mistakes, and because the compiler’s speed relies on never loading packages it does not need. An assignment alone is not a use.

package main

func main() {
	x := 1
	x = 2
	var y int
}
# example.com/shelf/ref/err/unused-var
./main.go:4:2: declared and not used: x
./main.go:6:6: declared and not used: y
package main

import (
	"fmt"
	"os"
)

func main() {
	fmt.Println("hi")
}
# example.com/shelf/ref/err/unused-import
./main.go:5:2: "os" imported and not used

Only local variables and imports are checked. Unused function parameters, named results, constants, types, functions and package-level variables are legal (linters such as unused in staticcheck report some of them). Older releases worded the message x declared but not used; the Go 1.27 compiler prints declared and not used: x, as shown above.

The blank identifier _ is a write-only name: assigning to it discards the value, and it is never declared.

Use Example

ignore a result

_, err := strconv.Atoi(s)

ignore the index or value in a range loop

for _, r := range s, for range 3

import for side effects only

import _ "embed"

compile-time check that a type implements an interface

var _ Shelf = (*Store)(nil)

silence the compiler temporarily while debugging

_ = value

a placeholder for padding or an unused iota value

_ = iota

package main

import (
	"fmt"
	"os"
	"strconv"
)

type Shelf interface{ Len() int }

type slice []int

func (s slice) Len() int { return len(s) }

var _ Shelf = slice(nil) // compile-time check that slice implements Shelf

func main() {
	_, err := strconv.Atoi("12") // ignore the value
	fmt.Println(err)
	for range 2 { // no variable needed
		fmt.Print("tick ")
	}
	for _, r := range "héé" { // ignore the index
		fmt.Print(string(r), " ")
	}
	fmt.Println()
	_ = os.Args // "use" a value to silence the compiler while debugging
}
<nil>
tick tick h é é

Never discard an error with _ unless ignoring it is genuinely correct; linters (errcheck) and code review treat that as a defect. See Error Handling.

Scope and shadowing

Go is block-scoped. The spec defines these scopes, from widest to narrowest: the universe block (predeclared identifiers such as len, true, string, error), the package block, the file block (imported package names), the function block, and every { ... } block, including the implicit blocks of if, for and switch statements and of each case clause. An identifier declared in an inner block shadows any same-named identifier from an outer block for the rest of the inner block. Shadowing is legal, and it is the source of one of the most common Go bugs:

package main

import (
	"errors"
	"fmt"
)

func lookup(isbn string) (string, error) {
	if isbn == "" {
		return "", errors.New("empty ISBN")
	}
	return "Dune", nil
}

func title(isbn string) (string, error) {
	var t string
	var err error
	if isbn != "" {
		t, err := lookup(isbn) // := declares NEW t and err in the if block
		if err != nil {
			return "", err
		}
		fmt.Println("found", t) // the inner t is used, the outer one never assigned
	}
	return t, err // the outer t is still ""
}

func main() {
	t, err := title("978-0-441-17271-9")
	fmt.Printf("%q %v\n", t, err)
}
found Dune
"" <nil>

t, err := lookup(isbn) inside the if declares a new t and a new err, because the if body is a new scope. The function then returns the outer t (still "") and the outer err (still nil): the lookup result is silently lost, and go vet does not complain because the code is valid. The fix is to assign with = (declaring the variables first with var) or to restructure so that the inner variables are the ones returned.

The shadow analyzer finds this pattern: it reports a variable declared in an inner scope with the same name and type as one in an outer scope when the outer variable is mentioned after the inner declaration. It is not part of the default go vet checks (the analyzer documentation explains that it produces too many false positives). It can be run as a separate vet tool; this was not run here because it requires downloading the module, which is not possible offline:

go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@v0.52.0
go vet -vettool="$(command -v shadow)" ./...

Predeclared identifiers can be shadowed too (len := 3, true := false, error := ""); that works, compiles, and is demonstrated on the Types and Values page. Since Go 1.22 each iteration of a for loop declares its own copies of the loop variables, which removed the old need for v := v; see Control Flow.

Constants

A constant declaration uses const. Unlike a variable, a constant must be initialized with a constant expression, and its value can never be assigned or addressed. The kinds of constants are boolean, rune, integer, floating-point, complex and string; there are no constant arrays, slices, maps, structs or pointers (const cannot hold a []string; use a variable or a function).

Typed and untyped constants

A constant is either typed (it has an explicit type, or it is the result of operations on typed constants) or untyped. An untyped constant has only a kind — integer, rune, floating-point, complex, string or boolean — and a value. It has a default type used when a context needs a type and there is none: bool, rune, int, float64, complex128 and string. An untyped constant converts implicitly to any type that can represent its value, which is why var f float64 = 5, var b byte = 5 and 5 * time.Second all compile.

package main

import (
	"fmt"
	"time"
)

const (
	MaxLoans         = 5                   // untyped integer constant
	Fine             = 0.25                // untyped floating-point constant
	Library          = "Central"           // untyped string constant
	Grace    int64   = 3                   // typed constant: int64
	Rate     float32 = 0.5                 // typed constant: float32
	LoanTerm         = 14 * 24 * time.Hour // typed: time.Duration, from the typed time.Hour
)

func main() {
	var f float64 = MaxLoans // an untyped constant adapts to its context
	var b byte = MaxLoans
	var d time.Duration = MaxLoans * time.Second
	fmt.Println(f, b, d, LoanTerm)
	fmt.Printf("%T %T %T %T %T\n", MaxLoans, Fine, Library, Grace, LoanTerm)
	fmt.Println(MaxLoans/2, MaxLoans/2.0, Fine*MaxLoans)
}
5 5 5s 336h0m0s
int float64 string int64 time.Duration
2 2.5 1.25

Note the last line: MaxLoans/2 is an integer division (both operands are integer constants), so it is 2, whereas MaxLoans/2.0 has a floating-point operand and is 2.5. When untyped constants of different kinds are mixed, the result has the kind that appears later in the order integer, rune, floating-point, complex.

package main

import "fmt"

func main() {
	// Mixing untyped constants: the result has the "larger" kind.
	// integer < rune < floating-point < complex
	fmt.Printf("%T\n", 1+'a')   // rune
	fmt.Printf("%T\n", 'a'+1.0) // float64
	fmt.Printf("%T\n", 1.0+2i)  // complex128
	fmt.Printf("%T\n", 1<<3)    // int
	x := 1 << 3.0               // 3.0 is representable as an integer: legal
	fmt.Println(x)

	// Untyped constants are exact; the conversion to a type happens last.
	const third = 1.0 / 3
	var f32 float32 = third
	var f64 float64 = third
	fmt.Println(f32, f64, float64(f32) == f64)
	fmt.Println(1/2, 1/2.0, 1.0/2, 7/2*2.0)
}
int32
float64
complex128
int
8
0.33333334 0.3333333333333333 false
0 0.5 0.5 6

1+'a' is a rune constant and 'a'+1.0 a floating-point constant. float32(third) is not the same number as the float64 one: the untyped constant 1.0/3 is exact until the type forces rounding, and float32 rounds to fewer bits. Also 7/2*2.0 evaluates to 6, not 7: 7/2 is an integer division of two integer constants (3), and only then is the product with 2.0 formed.

A typed constant behaves like a variable of that type for assignability: it does not convert implicitly. That is the price of giving a constant a type, and the reason time.Duration constants multiply cleanly with each other but not with plain int variables:

package main

import "fmt"

const limit int = 5

func main() {
	var f float64 = limit
	fmt.Println(f)
}
# example.com/shelf/ref/err/const-typed-mismatch
./main.go:8:18: cannot use limit (constant 5 of type int) as float64 value in variable declaration

Prefer untyped constants for plain numbers and strings, and typed constants where the type carries meaning (a LoanState, a time.Duration, a Perm bit set).

Arbitrary-precision arithmetic

Untyped constant expressions are evaluated exactly: the language treats constants as having arbitrary precision, and the spec requires every compiler to represent integer constants with at least 256 bits and floating-point constants with a mantissa of at least 256 bits (and to give an error rather than silently round an integer). Only when the value meets a context does the compiler check that it is representable in the destination type; intermediate results can therefore be astronomically larger than any machine type.

Flow of an untyped constant. The constant Huge, defined as 1 shifted left by 100, is an untyped integer constant with the exact value 2 to the 100. The expression Huge shifted right by 99 is still untyped and exact, with the value 2. When a context supplies a type, three outcomes are possible: assigning Huge shifted right by 99 to an int compiles because 2 is representable; assigning Huge itself to an int is a compile error because it overflows; and short declaration x := Huge shifted right by 99 uses the default type int.
package main

import (
	"fmt"
	"math"
	"math/big"
)

const (
	Huge  = 1 << 100              // far beyond any integer type
	Small = Huge >> 99            // the arithmetic is exact; the result fits int
	Tiny  = 1e-300 * 1e-300       // underflows float64 if computed at run time...
	Big   = 1e300 * 1e300 / 1e300 // ...and so would this overflow
)

func main() {
	fmt.Println(Small, Huge/(1<<98))
	fmt.Println(Big, Tiny*1e600)
	fmt.Println(math.Pi*1e100/1e100 == math.Pi)

	// At run time the same arithmetic wraps or overflows.
	h := 1 << 62
	fmt.Println(h*2, h*4)

	// For arbitrary precision at run time, use math/big.
	fmt.Println(new(big.Int).Lsh(big.NewInt(1), 100))
}
2 4
1e+300 1
true
-9223372036854775808 0
1267650600228229401496703205376

Huge has no type, so declaring it is fine; Huge>>99 is exactly 2; 1e300*1e300/1e300 never overflows because the multiplication is not carried out in float64; and math.Pi*1e100/1e100 == math.Pi holds because the arithmetic is exact. At run time the same expressions would wrap or overflow (h*2 and h*4 above). For arbitrary precision at run time use math/big. Precision is a compile-time property of constants; it is not a property of memory, and untyped constants are not stored "without loss of precision" in a running program (Vivien’s chapter 2 says otherwise).

The compiler is strict about the moment of conversion. Overflow, truncation and division by zero in a constant expression are compile errors, not run-time behaviour:

package main

import "fmt"

const big = 1 << 100

func main() {
	var small int8 = 200
	fmt.Println(small)
	fmt.Println(big)
}
# example.com/shelf/ref/err/const-overflow
./main.go:8:19: cannot use 200 (untyped int constant) as int8 value in variable declaration (overflows)
./main.go:10:14: cannot use big (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows)
package main

import "fmt"

func main() {
	var pages int = 1.5
	var ok int = 2.0
	fmt.Println(pages, ok)
}
# example.com/shelf/ref/err/const-truncated
./main.go:6:18: cannot use 1.5 (untyped float constant) as int value in variable declaration (truncated)
package main

import "fmt"

func main() {
	const zero = 0
	fmt.Println(10 / zero)
}
# example.com/shelf/ref/err/const-div-zero
./main.go:7:19: invalid operation: division by zero

var ok int = 2.0 compiles (the second line of const-truncated), because 2.0 is representable as an integer; 1.5 is not and is reported as (truncated).

Constant expressions

A constant expression contains only constant operands and is evaluated at compile time. The spec allows:

  • literals, constant identifiers, iota, and parenthesised constant expressions;

  • operators on constants: arithmetic, comparison, logical, bitwise and shifts (a constant shift of an untyped constant yields an untyped result);

  • conversions of a constant to a type (the result is a typed constant if the target is a basic type);

  • the builtins len and cap when the argument has array type (or is a string constant) and contains no channel receives or non-constant function calls; real, imag and complex of constants; min and max of constants (Go 1.21);

  • unsafe.Sizeof, unsafe.Alignof and unsafe.Offsetof of suitable operands (see sizes and alignment).

Everything else is not a constant: function calls (except the builtins above), composite literals, indexing a slice or map, and variables. A constant also cannot be assigned or have its address taken.

package main

import (
	"fmt"
	"time"
)

const now = time.Now()

func main() {
	const n = len([]int{1, 2})
	fmt.Println(now, n)
}
# example.com/shelf/ref/err/const-not-constant
./main.go:8:13: time.Now() (value of struct type time.Time) is not constant
./main.go:11:12: len([]int{…}) (value of type int) is not constant
package main

const MaxLoans = 5

func main() {
	MaxLoans = 6
	p := &MaxLoans
	_ = p
}
# example.com/shelf/ref/err/const-assign
./main.go:6:2: cannot assign to MaxLoans (neither addressable nor a map index expression)
./main.go:7:8: invalid operation: cannot take address of MaxLoans (untyped int constant 5)

The first message is reported for time.Now(), and the second shows that len([]int{1, 2}) is a call on a slice, not an array or a constant string. len("abc") and len([3]int{}) are constants.

Shifts of untyped constants

A shift whose left operand is an untyped constant and whose count is not constant takes the type the left operand would have if the shift were replaced by its left operand alone. In other words, the surrounding context decides the type, and that decides where the bits are lost. A float context cannot shift:

package main

import "fmt"

func main() {
	var s uint = 33
	var i = 1 << s         // 1 has type int (the type it would have without the shift)
	var j int32 = 1 << s   // 1 has type int32; j == 0
	var k = uint64(1 << s) // 1 has type uint64; k == 1<<33
	var m int = 1.0 << s   // 1.0 has type int
	fmt.Println(i, j, k, m)
	fmt.Printf("%T %T\n", i, 1<<s == 2)
}
8589934592 0 8589934592 8589934592
int bool
package main

import "fmt"

func main() {
	var s uint = 3
	var f float64 = 1 << s
	fmt.Println(f)
}
# example.com/shelf/ref/err/shift-float
./main.go:7:18: invalid operation: shifted operand 1 (type float64) must be integer

var j int32 = 1 << s makes the 1 an int32, so shifting by 33 gives 0; uint64(1 << s) makes it a uint64. 1 << s == 2 is a comparison where 1 takes the default type int. Shift counts have been allowed to be signed since Go 1.13 (a negative count panics at run time).

iota enumerations

iota is a predeclared identifier valid only inside a constant declaration. In each const (...) block it is 0 in the first constant specification, and increases by one for each following specification (not for each name or each use). A specification with no expression repeats the previous expression list and type, with the new iota.

Idiom Result

A = iota followed by B, C

0, 1, 2

_ = iota first

skips 0; the next name is 1

KB = 1 << (10 * (iota + 1)) followed by MB, GB

1<<10, 1<<20, 1<<30

Flag1 Perm = 1 << iota followed by Flag2, Flag3

1, 2, 4 of type Perm

X, Y = iota, iota * 10 then Z, W

0 0 and 1 10 (one iota per specification)

A = iota / B = "b" / C / D = iota

0, "b", "b", 3

package main

import "fmt"

const (
	A = iota // 0
	B        // 1: the previous expression and type are repeated
	C        // 2
)

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

const (
	X, Y = iota, iota * 10 // both names on the line share one iota
	Z, W                   // 1, 10
)

const (
	P = iota // 0
	Q = "q"  // iota is 1 here, but unused
	R        // repeats "q"
	S = iota // 3: iota counts constant specs, not uses
)

func main() {
	fmt.Println(A, B, C)
	fmt.Println(KB, MB, GB)
	fmt.Println(X, Y, Z, W)
	fmt.Println(P, Q, R, S)
	const (
		Skip = iota + 1
		_
		Four // 3 + 1
	)
	fmt.Println(Skip, Four)
}
0 1 2
1024 1048576 1073741824
0 0 1 10
0 q q 3
1 3

Typed enumerations with a String method

Go has no enum keyword. The idiom is a defined integer type plus a block of typed constants, with String (and often Valid) methods. This is the real enumeration the shelf catalog package uses:

package main

import "fmt"

// LoanState is an enumeration: a defined integer type plus named constants.
type LoanState int

const (
	Available LoanState = iota
	OnLoan
	Overdue
	numStates // unexported sentinel: the number of valid states
)

var stateNames = [...]string{Available: "available", OnLoan: "on loan", Overdue: "overdue"}

func (s LoanState) String() string {
	if s < 0 || s >= numStates {
		return fmt.Sprintf("LoanState(%d)", int(s))
	}
	return stateNames[s]
}

func (s LoanState) Valid() bool { return s >= 0 && s < numStates }

func main() {
	var s LoanState = OnLoan
	fmt.Println(s, Overdue, LoanState(7))
	fmt.Printf("%v %d %s %q %x\n", s, s, s, s, s)
	fmt.Println(LoanState(7).Valid(), Available.Valid())
	var zero LoanState
	fmt.Println(zero == Available) // the zero value is the first constant
	s = 2                          // an untyped constant converts silently
	fmt.Println(s)
}
on loan overdue LoanState(7)
on loan 1 on loan "on loan" 6f6e206c6f616e
false true
true
overdue

Source: fmt: Stringer

Points to note:

  • Available is the first constant, so it is the zero value of LoanState. A freshly created Loan is therefore Available without any initialization, and decoders that leave fields untouched produce a valid value.

  • numStates is an unexported sentinel counting the valid values. It sizes tables and bounds checks, and stays out of the public API.

  • The array literal [...]string{Available: "available", ...} is indexed by the constants themselves.

  • A string-typed enumeration has no array-index form and does not need String; the choice between type S string and type S int is usually about how the value is stored and compared (see defined types).

  • LoanState is a defined type, not a new kind: s = 2 compiles because 2 is an untyped constant, and LoanState(7) compiles too. Go enumerations are not closed, so a function that receives a LoanState must be prepared for invalid values (hence Valid and the LoanState(7) branch of String).

  • With %v, %s, %q and %x, fmt calls the String method; %d prints the number.

Generating String with stringer

For long enumerations, stringer from golang.org/x/tools/cmd/stringer writes the String method for you. Given the type and its constants, it generates <type>_string.go with a compact lookup (a string holding all names and an index table). It is invoked by go generate:

//go:generate stringer -type=LoanState -trimprefix=Loan
type LoanState int

The flags -type (a comma-separated list), -trimprefix, -linecomment (use the trailing line comment as the name), -output and -tags are documented on the package page. Since Go 1.24 the tool can be tracked in go.mod with a tool directive (go get -tool golang.org/x/tools/cmd/stringer@v0.52.0, run as go tool stringer), replacing the old tools.go file. This was not run here (it needs the network); the snippet above is a fragment, not a program. See Directives, Code Generation and Embedding for go generate.

Bit flags

1 << iota creates a set of bit flags. A defined unsigned type holds them, and &, | and &^ (AND NOT, which clears bits) combine and test them:

package main

import (
	"fmt"
	"strings"
)

// Perm is a bit set of member permissions.
type Perm uint8

const (
	Borrow Perm = 1 << iota
	Reserve
	Renew
	Admin
	None Perm = 0
)

func (p Perm) Has(q Perm) bool { return p&q == q }

func (p Perm) String() string {
	if p == None {
		return "none"
	}
	var parts []string
	for _, f := range []struct {
		bit  Perm
		name string
	}{{Borrow, "borrow"}, {Reserve, "reserve"}, {Renew, "renew"}, {Admin, "admin"}} {
		if p.Has(f.bit) {
			parts = append(parts, f.name)
		}
	}
	return strings.Join(parts, "|")
}

func main() {
	p := Borrow | Renew
	fmt.Println(p, p.Has(Renew), p.Has(Admin), uint8(p))
	p |= Admin  // set
	p &^= Renew // clear
	fmt.Println(p, None)
	fmt.Printf("%08b %08b\n", Borrow, Admin)
}
borrow|renew true false 5
borrow|admin none
00000001 00001000

Has uses p&q == q, so it is true only when all bits of q are set. With flags beyond the width of the type (1 << iota reaching 8 for uint8), the constant overflows and compilation fails, which is the safe outcome.

When not to use iota

iota computes values from position, so position becomes meaning: inserting, deleting or reordering a line changes the value of everything after it. That is harmless for values that live only in memory during one run, and dangerous for any value that crosses a boundary: stored in a database, written to a file, sent over the wire, or part of a published API.

package main

import "fmt"

// Status codes are part of a wire format: never derive them from iota.
const (
	StatusOK       = 200
	StatusNotFound = 404
)

// Ordinals derived from iota change when somebody inserts a line.
const (
	Fiction = iota
	Poetry
	Drama
)

func main() {
	fmt.Println(StatusOK, StatusNotFound, Fiction, Poetry, Drama)
	// If a new genre is inserted before Poetry, Poetry and Drama silently
	// shift. A value stored in a database or file as "1" changes meaning.
}
200 404 0 1 2

Use explicit literals for persisted and external values (HTTP status codes, protocol opcodes, database enums), or give each constant an explicit value and add a test that pins the numbers. It is also reasonable to persist the String() form rather than the integer, with a parser that rejects unknown names.

iota outside a constant declaration is an error:

package main

import "fmt"

func main() {
	var x = iota
	fmt.Println(x)
}
# example.com/shelf/ref/err/iota-outside
./main.go:6:10: cannot use iota outside constant declaration

Pointers to values: new

new(T) allocates a zeroed variable of type T and returns a *T. Before Go 1.26 a pointer to an initialized value needed a temporary variable or a helper such as ptr(v); since Go 1.26 new also accepts an expression, whose value becomes the initial value of the new variable. It is most useful for optional fields represented as pointers:

package main

import (
	"fmt"
	"time"
)

type Loan struct {
	Member  string
	DueDate *time.Time // optional: nil means "not set"
	Renewed *int
}

func main() {
	due := time.Date(2026, 11, 1, 0, 0, 0, 0, time.UTC)
	l := Loan{Member: "ana", DueDate: new(due), Renewed: new(2)} // Go 1.26: new(expression)
	fmt.Println(l.DueDate.Format(time.DateOnly), *l.Renewed)
	n := new(int) // classic form: pointer to a zero int
	*n = 10
	fmt.Println(*n)
}
2026-11-01 2
10

The pointer rules (nil dereference, escape to the heap, copies) are on Pointers and Value Semantics.

Comparison with other languages

Concept Go Rust C Java

declare with inference

n := 1

let n = 1;

__auto_type n = 1; (C23 auto)

var n = 1;

immutable binding

none for variables; const for compile-time values

let is immutable by default

const qualifier

final

uninitialized variable

impossible: zero value

compile error

indeterminate value

compile error for locals

compile-time constant

const, untyped, arbitrary precision

const, typed

#define, enum, constexpr (C23)

static final

enumeration

iota and a defined type

enum

enum

enum

unused local

compile error

warning

warning

warning

shadowing

allowed in inner scopes

allowed, even in the same scope

allowed in inner scopes

not allowed for locals

Where the books are outdated

  • Bodner, chapter 2 treats var against := and typed against untyped constants correctly, and still holds up. Two pointers worth adding: since Go 1.26 new(expr) removes the ptr helpers that chapter 6 shows, and min, max and clear (Go 1.21) are available as builtins. The "unused variable" rule applies to locals only, as described above.

  • Bodner, chapter 7 shows iota enumerations and correctly warns when not to use them. The current practice for the String method is stringer through a tool directive (Go 1.24), and for new code a Valid method and explicit values for persisted constants, as shown above.

  • Vivien, chapter 2 explains typed and untyped constants and iota patterns (1 << iota, skipping values), but says untyped constants are "stored in memory without loss of precision". Arbitrary precision exists only at compile time; once a value is stored in a variable it has a fixed machine type. The chapter also predates binary and octal prefixes and digit separators in literals (Go 1.13) — see numeric literals.

  • Both books quote the older wording x declared but not used; Go 1.27 prints declared and not used: x. The rule itself has not changed.

Version notes

Release Change

Go 1.13

0b/0o prefixes, digit separators _, hex floats; signed shift counts

Go 1.17

math.MaxInt, math.MinInt and math.MaxUint constants

Go 1.21

min, max and clear builtins; package initialization order specified precisely

Go 1.22

per-iteration loop variables (for loops with go 1.22 or later in go.mod); range over integers

Go 1.24

tool directives in go.mod (replace tools.go); generic type aliases

Go 1.26

new(expression)