Skip to content

Table of Contents

Go Syntax Basics

Contents

Variables

var a int            // declared, zero value 0
var b = 10           // type inferred (int)
var c, d = 1, "two"  // multiple, mixed types
var (                // grouped
    e bool
    f float64 = 3.14
)

g := 42              // short decl, only inside functions
h, err := doThing()  // common idiom; at least one new var on LHS

_ = g                // blank identifier discards a value

Rules: every declared local variable must be used (compile error otherwise). := is function-body only; package-level needs var.

Constants & iota

Constants are compile-time, untyped unless given a type. They can be numbers, strings, booleans, runes.

const Pi = 3.14159
const Greeting string = "hi"

// Untyped constants adapt to context:
const big = 1 << 62          // fine as a constant
var x float64 = big          // converts on use

// iota: resets to 0 per const block, increments per line.
type Weekday int
const (
    Sunday Weekday = iota // 0
    Monday                // 1
    Tuesday               // 2
)

// Bit flags
const (
    FlagA = 1 << iota // 1
    FlagB             // 2
    FlagC             // 4
)

// Skip values with _ and use expressions
const (
    _  = iota             // ignore 0
    KB = 1 << (10 * iota) // 1<<10
    MB                    // 1<<20
    GB                    // 1<<30
)

Basic types

bool
string
int  int8  int16  int32  int64       // int is 32 or 64 bit (platform)
uint uint8 uint16 uint32 uint64 uintptr
byte    // alias for uint8
rune    // alias for int32, a Unicode code point
float32 float64
complex64 complex128

int/uint size is platform-dependent; use sized types when it matters (serialization, bit ops).

Zero values

No uninitialized memory in Go. Every type has a zero value:

Type Zero
numeric 0
bool false
string ""
pointer, slice, map, chan, func, interface nil
struct each field zeroed
array each element zeroed
var s []int          // nil slice: len 0, cap 0, safe to range/append
var m map[string]int // nil map: reads ok (zero value), writes PANIC
var p *int           // nil pointer

Type conversion

Go has no implicit numeric conversion. Convert explicitly:

i := 42
f := float64(i)
u := uint(f)
b := []byte("hi")
s := string(b)
r := rune(65)        // 'A'

Control flow

// if with optional init statement (scope limited to if/else)
if v, ok := m["k"]; ok {
    use(v)
} else {
    // v still in scope here
}

// for is the only loop keyword
for i := 0; i < 10; i++ {}     // classic
for cond {}                    // while-style
for {}                         // infinite
for i := range 5 {}            // 0..4 (Go 1.22+)
for i, v := range slice {}     // index, value
for k, v := range mp {}        // key, value (random order)
for r := range "héllo" {}      // r is a rune; index jumps by UTF-8 width

// switch: no fallthrough by default; cases can be expressions
switch x := f(); {
case x < 0:
    neg()
case x == 0:
    zero()
default:
    pos()
}

switch x {
case 1, 2, 3:        // multiple values
    small()
case 4:
    fallthrough      // explicit fallthrough to next case
case 5:
    medium()
}

// label + break/continue for nested loops
outer:
for _, row := range grid {
    for _, c := range row {
        if c == 'x' {
            break outer
        }
    }
}

goto done            // exists but rarely idiomatic
done:

Functions

func add(a, b int) int { return a + b } // shared type for a,b

func variadic(prefix string, nums ...int) int {
    sum := 0
    for _, n := range nums { sum += n }
    return sum
}
variadic("x", 1, 2, 3)
xs := []int{1, 2, 3}
variadic("x", xs...)            // spread a slice

// Functions are values / first-class
var op func(int, int) int = add
apply := func(f func(int, int) int, a, b int) int { return f(a, b) }

// Closures capture variables by reference
func counter() func() int {
    n := 0
    return func() int { n++; return n }
}

Multiple & named returns

func divmod(a, b int) (int, int) {
    return a / b, a % b
}
q, r := divmod(17, 5)

// Named return values: pre-declared, zero-valued, settable by defer
func read(path string) (data []byte, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("read %s: %w", path, err)
        }
    }()
    data, err = os.ReadFile(path)
    return // "naked" return uses named values
}

Idiom: return (value, error) with error last; check if err != nil immediately.

defer

Deferred calls run LIFO when the surrounding function returns (normally or via panic).

f, err := os.Open(name)
if err != nil { return err }
defer f.Close()            // runs at function exit

// Arguments are evaluated WHEN defer is reached, not when it runs:
i := 0
defer fmt.Println(i)       // prints 0
i++

// But closures see the latest value:
defer func() { fmt.Println(i) }() // prints 1

// LIFO ordering
for i := 0; i < 3; i++ {
    defer fmt.Print(i)     // prints 210
}

Common uses: closing files/connections, unlocking mutexes, recovering from panics, modifying named return values.