Variables#

Go has three declaration forms (var, := short declaration, const) and one identifier discard form (_). Bindings are statically typed; the compiler infers the type from the right-hand side when do not write one.

/types`.

Declarations#

var x int                    // declared, zero-valued (0)
var y = 10                   // type inferred (int)
var z int = 10               // both annotated and initialised

a := 10                      // short declaration; function-scope only

const Pi = 3.14159           // constant; compile-time evaluated
const (
    Pending = iota           // 0
    Active                   // 1
    Closed                   // 2
)

The short form := works only inside functions; var is the only form allowed at package scope.

Zero values#

Every binding starts at its zero value: 0 for numbers, false for bool, "" for string, nil for pointers, slices, maps, channels, interfaces, and functions. struct zero values have each field zeroed in place.

var n int            // 0
var s string         // ""
var p *User          // nil
var u User           // User{Name: "", Age: 0}

Zero values make declared-but-unassigned bindings safe to use.

Multiple assignment#

a, b := 1, 2
a, b = b, a                  // swap

x, y, z := 1, 2, 3
x, _, z := 1, 2, 3           // discard the middle value

Functions return multiple values; the operator destructures with the same form.

value, err := strconv.Atoi("42")
if err != nil { return err }

_, err = os.Stat(path)       // value discarded

Scope#

Go is lexically scoped. var and := introduce a binding that is visible from the next statement to the end of the enclosing block (function, if, for, switch, select, or a bare { ... } block).

{
    secret := "rk"
    fmt.Println(secret)      // visible
}
fmt.Println(secret)          // compile error; out of scope

if and for accept an optional init statement that binds a variable scoped to the conditional body.

if v, err := load(); err == nil {
    use(v)                   // v in scope here
}                            // v out of scope here

for i := 0; i < 10; i++ {
    // i in scope here only
}

Shadowing#

:= re-declares a binding when at least one name on the left is new. The operator watches for accidental shadows inside if blocks; go vet -shadow catches them.

err := load()
if err != nil {
    err := wrap(err)         // SHADOWS the outer err
    return err               // returns the inner; outer unchanged
}

const and iota#

Constants are typed or untyped at the compile level; untyped constants get their type from the context they are used in. iota is a per-block counter that resets at each const ( ) group, useful for enumerations.

const (
    KB = 1 << (10 * (iota + 1))   // 1<<10 = 1024
    MB                            // 1<<20
    GB                            // 1<<30
)

const (
    _      = iota                // skip 0
    Red                          // 1
    Green                        // 2
    Blue                         // 3
)

Package-level state#

Package-level var declarations are visible to every file in the package. The operator keeps these minimal; tests prefer construction over global mutable state.

// logger is the package-wide structured logger.
var logger = slog.New(slog.NewJSONHandler(os.Stdout, nil))

Initialise package-level state in func init() when the expression cannot be a constant.

var rng *rand.Rand

func init() {
    rng = rand.New(rand.NewSource(time.Now().UnixNano()))
}

Pointers#

Pointers hold an address. &x takes the address of x; *p reads through the pointer. Method receivers and large struct passes use pointers; everything else passes by value.

var x int = 10
p := &x                      // *int
*p = 20                      // x is now 20

u := &User{Name: "rk"}       // pointer-to-struct literal
u.Name = "operator"          // automatic deref through . operator

Pointer dereference is automatic on field access (u.Name works whether u is User or *User).

References#