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 |
|---|---|
|
type only: |
|
value only: the type is the default type of the constant, here |
|
type and value: the constant is converted to |
|
several names; each gets its own inferred type |
|
several names of one type |
|
several names initialized from one multi-valued call |
|
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
Source: A Tour of Go: Zero values
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 reportsnon-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 (anif,fororswitchheader, 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 |
|
a local variable that starts at its zero value |
|
a local that must have a type different from the value’s default |
|
a package-level variable |
|
declaring several results of a call |
|
declaring a variable that outlives an |
|
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 |
|
ignore the index or value in a range loop |
|
import for side effects only |
|
compile-time check that a type implements an interface |
|
silence the compiler temporarily while debugging |
|
a placeholder for padding or an unused |
|
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
Source: The Go Blog: Constants
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.
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
lenandcapwhen the argument has array type (or is a string constant) and contains no channel receives or non-constant function calls;real,imagandcomplexof constants;minandmaxof constants (Go 1.21); -
unsafe.Sizeof,unsafe.Alignofandunsafe.Offsetofof 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 |
|---|---|
|
|
|
skips |
|
|
|
|
|
|
|
|
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:
-
Availableis the first constant, so it is the zero value ofLoanState. A freshly createdLoanis thereforeAvailablewithout any initialization, and decoders that leave fields untouched produce a valid value. -
numStatesis 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 betweentype S stringandtype S intis usually about how the value is stored and compared (see defined types). -
LoanStateis a defined type, not a new kind:s = 2compiles because2is an untyped constant, andLoanState(7)compiles too. Go enumerations are not closed, so a function that receives aLoanStatemust be prepared for invalid values (henceValidand theLoanState(7)branch ofString). -
With
%v,%s,%qand%x,fmtcalls theStringmethod;%dprints 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.
Source: golang.org/x/tools/cmd/stringer
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 |
|
|
|
|
immutable binding |
none for variables; |
|
|
|
uninitialized variable |
impossible: zero value |
compile error |
indeterminate value |
compile error for locals |
compile-time constant |
|
|
|
|
enumeration |
|
|
|
|
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
varagainst:=and typed against untyped constants correctly, and still holds up. Two pointers worth adding: since Go 1.26new(expr)removes theptrhelpers that chapter 6 shows, andmin,maxandclear(Go 1.21) are available as builtins. The "unused variable" rule applies to locals only, as described above. -
Bodner, chapter 7 shows
iotaenumerations and correctly warns when not to use them. The current practice for theStringmethod isstringerthrough atooldirective (Go 1.24), and for new code aValidmethod and explicit values for persisted constants, as shown above. -
Vivien, chapter 2 explains typed and untyped constants and
iotapatterns (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 printsdeclared and not used: x. The rule itself has not changed.
Version notes
| Release | Change |
|---|---|
Go 1.13 |
|
Go 1.17 |
|
Go 1.21 |
|
Go 1.22 |
per-iteration loop variables ( |
Go 1.24 |
|
Go 1.26 |
|
Source: Go 1.13 Release Notes
Source: Go 1.17 Release Notes