Module 1 · Lesson 1 · ~25 min read

Zero Values, nil, and Go's Philosophy

Every Go variable has a useful default without you writing a constructor. nil is not one concept — it's the zero value for several type families, and they behave differently. Get these two ideas right and half of Go's "weirdness" disappears.

The first rule: no uninitialized state

In Python, this is a NameError:

print(x)  # NameError: name 'x' is not defined

In JavaScript, it's either undefined or a ReferenceError, depending on whether the identifier was declared. Either way the concept of "uninitialized" exists at runtime.

In Go, uninitialized does not exist as a runtime state. Every declaration is immediately set to its type's zero value. The compiler won't let you have an unbound variable.

var n int      // n == 0
var s string   // s == "" (empty string, not nil)
var b bool     // b == false
var p *User   // p == nil
var xs []int   // xs == nil, but len(xs) == 0 and you can range over it
var m map[string]int  // m == nil, reading is fine, WRITING panics

This is a language-level design choice, not a style choice. It means a lot of defensive initialization code you'd write in Python or JS is unnecessary in Go.

The zero value table — memorize this

TypeZero valueCan you use it without more init?
int, int32, int64, uint*0Yes
float32, float640.0Yes
boolfalseYes
string""Yes
Pointer *TnilNo — dereferencing panics
Function func(...)nilNo — calling panics
Interface (any interface{...})nilNo — calling a method panics
Slice []TnilYes — len/cap/range/append all work. Reading panics only for indexing.
Map map[K]VnilPartially. Reading fine, writing panics.
Channel chan TnilNo — send/receive blocks forever, close panics
Struct struct {...}all fields zero-valued recursivelyUsually yes — this is idiomatic
Array [N]Tall elements zero-valuedYes
The slice vs map asymmetry

This is the #1 gotcha for newcomers. append on a nil slice just works — it allocates as needed. Writing to a nil map panics. Why the difference? A slice header (pointer, len, cap) is a value — append returns a new one. A map is always a reference to an opaque hash structure; there's no way to "return" a new map from an assignment.

var xs []int           // nil slice
xs = append(xs, 1)       // fine: len(xs) == 1

var m map[string]int   // nil map
m["x"] = 1              // PANIC: assignment to entry in nil map

var _ = m["x"]           // fine — reads return zero value (0 here)

What nil actually is

Python has None, a singleton instance of NoneType. JavaScript has null and undefined. Hack has null. In all these languages, the null-like value is (roughly) uniform — one kind of "nothing."

In Go, nil is the zero value of six distinct type families: pointers, interfaces, slices, maps, channels, and functions. Each family's nil is a different runtime representation:

You can't compare a nil of one type family to nil of another without conversion. You can only assign nil to a variable of a nilable type.

var x int = nil  // COMPILE ERROR: cannot use nil as int value

The nil-interface trap

This one will bite you. Read it twice:

type MyErr struct{}
func (*MyErr) Error() string { return "oops" }

func doThing() error {
    var err *MyErr  // err is (*MyErr)(nil)
    return err        // returned as an error interface
}

func main() {
    err := doThing()
    if err != nil {     // TRUE! and now we're hurting
        fmt.Println(err.Error())  // panic — *MyErr is nil so Error dereferences nil
    }
}

The interface value is not nil, because its type descriptor is set to *MyErr. Only its data pointer is nil. An interface equals nil only when both type descriptor and data pointer are zero.

Rule

Never return a typed nil pointer as an interface. If a function returns error, write return nil (a literal nil that becomes a nil interface). Don't return var e *MyErr; return e — it'll look non-nil to callers.

This trap is the single most common nil-related bug in production Go code. The Canton codebase, any gRPC Go client, any HTTP handler — all rely on this pattern being handled correctly.

Quick check

1. What does the following print?
var m map[string]int
fmt.Println(m["missing"], len(m))
0 0. Reading from a nil map is allowed — it returns the value type's zero value, so 0 for int. len(nil map) is 0. Only writing to a nil map panics.
2. Given:
type Config struct {
    Timeout time.Duration
    Retries int
    Hosts   []string
    Labels  map[string]string
}

var c Config
c.Hosts = append(c.Hosts, "a")
c.Labels["env"] = "prod"
Which line causes the problem, and why?
The map write. `var c Config` zero-values the struct, which means Hosts is nil []string and Labels is nil map. The slice append is fine (nil slices accept append). The map write panics. You must explicitly initialize the map, either via literal (`Labels: map[string]string{}`), make(), or a constructor function.
3. Which declaration zero-initializes a slice you can immediately append to?
All three. The first gives a nil slice (nil data pointer, len=0, cap=0). The second and third give non-nil empty slices. For append, they're indistinguishable. The nil-vs-empty distinction matters only for JSON marshaling (nil → null, empty → []) and explicit equality checks against nil. Effective Go actively prefers var xs []int for "empty-so-far" slices.

The philosophy behind all this

Go's designers made three interlocking choices:

  1. Every type has a useful zero value. So you can declare a variable and use it immediately. No constructors required for the common case.
  2. Errors are values, not exceptions. So the type system forces you to handle or propagate them. No "what might throw here?" guessing.
  3. The language has one obvious way for most things. There's no ternary, no operator overloading, no implicit conversion. Boring is a feature.

These are not neutral choices. They're trade-offs against expressiveness. Go is explicit and occasionally verbose because the designers want code to be readable by someone who didn't write it — and who may be reviewing it under time pressure.

Why this matters for Canton work

A Canton Go client will be part of critical financial infrastructure. Someone will read your code at 2 AM when a settlement is stuck. Go's boring, explicit style pays off in exactly that context. Don't fight it.

Idioms that flow from "zero value works"

Zero-value structs are constructors

type Buffer struct {
    data []byte
}

func (b *Buffer) Write(p []byte) (int, error) {
    b.data = append(b.data, p...)
    return len(p), nil
}

// No constructor needed:
var buf Buffer
buf.Write([]byte("hi"))  // works — b.data starts nil, append handles it

This is why bytes.Buffer, sync.Mutex, sync.WaitGroup, and many other standard library types don't have constructors — the zero value is the valid empty state.

"If you do need real init, hide it in a constructor"

type Cache struct {
    m map[string]string
    mu sync.Mutex
}

func NewCache() *Cache {
    return &Cache{m: make(map[string]string)}
}

Convention: name the constructor NewX. Canton-era codebases follow this religiously.

Takeaways