← Go, End to End

Go, End to End

Idiomatic Go and Common Pitfalls

A closing chapter on how experienced Go programmers actually write it day to day, and the mistakes that catch almost everyone at least once.

gofmt and go vet are not optional

gofmt (or go fmt) reformats your code to Go's one canonical style. There's no configuration, no debate about tabs vs spaces or brace placement, because the tool decides and the entire ecosystem uses the same output. Run it on save; most editors do this automatically once configured. go vet catches a different class of issue: suspicious constructs that compile fine but are almost certainly bugs (a Printf format string that doesn't match its arguments, a struct passed by value to a method expecting a pointer receiver used via sync.Mutex copying). Run both, treat them as part of compiling, not an optional linting step.

Effective Go's core conventions

  • Short variable names in short scopes (i in a five-line loop, not currentLoopIndex); longer, descriptive names as scope grows
  • Accept interfaces, return concrete structs, from the interfaces chapter: lets callers pass whatever satisfies the small interface your function needs, while giving them a concrete, fully-featured type back
  • Handle errors immediately after the call that can produce them, don't let them bubble past several statements unchecked
  • Prefer composition (embedding) over trying to simulate inheritance, this course's own composition chapter covers why

Common mistakes, in roughly the order newcomers hit them

Nil pointer dereferences

A nil pointer's zero value is perfectly legal to hold, and only panics when you try to dereference it or call a method with a value (not pointer) receiver on it. The fix is the same discipline as anywhere: check for nil before use when a value might legitimately be nil (a function that returns (*T, error), check the error first, but also don't assume a nil error always means a non-nil result if the function's contract doesn't actually guarantee that).

Arrays are values, slices and maps are reference-like

Assigning or passing an array copies the whole thing. Assigning or passing a slice or map copies the header (pointer, length, capacity for slices; just a pointer for maps), so mutations through either the original or the copy are visible through both, since they share the same underlying data. This distinction trips up people moving from languages where "array" and "list"/"slice" are the same underlying concept.

go · live, editable, runnable

Shadowing with :=

:= declares a NEW variable if the name doesn't already exist in the CURRENT scope, even if a variable with the same name exists in an outer scope. Inside an if/for/block, this can silently create a shadow instead of assigning to the outer variable you meant to update:

go · live, editable, runnable

The fix is deliberate: use = (not :=) when you mean to assign to an existing variable from an outer scope, and go vet/careful review catch most accidental shadowing before it ships.

Closure-over-loop-variable (historical)

Covered in depth in this course's Functions chapter: pre-1.22, all iterations of a for loop shared the same loop variable, so a closure capturing it captured whatever value it held when the closure finally RAN, usually the last one, not the value at the time the closure was created. Go 1.22 changed loop variables to be per-iteration by default, fixing this for anyone on a current toolchain, but you'll still see the old workaround pattern (shadowing the loop variable inside the loop body) in codebases and blog posts written before the change.

If you remember five things from this course

  • Errors are values, checked explicitly at the call site, not exceptions caught somewhere far away
  • Goroutines and channels are the concurrency primitives; share memory by communicating, reach for a mutex only when you genuinely need shared mutable state
  • Interfaces are satisfied implicitly; design small interfaces, accept them as parameters, return concrete structs
  • Generics solve real duplication in container types and type-agnostic algorithms; they're not a replacement for interfaces when different types need different behavior
  • The standard library is unusually complete; check it before reaching for a dependency