Functions
Multiple return values, named results, variadics, closures — and the error convention that shapes every Go API.
func add(a int, b int) int {
return a + b
}
func addSub(a, b int) (int, int) { // same type? declare it once
return a + b, a - b
}Multiple return values#
This is the feature that defines Go's style. Almost every function that can fail returns two values, and the second is an error.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("divide %v by zero", a)
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil {
return fmt.Errorf("compute ratio: %w", err)
}Three conventions worth learning immediately:
- The error is always the last return value.
- On error, other return values are their zero value and must not be used.
%winfmt.Errorfwraps the error soerrors.Isanderrors.Ascan find it later.%vdoes not — that difference is invisible in the message and breaks callers.
Named return values#
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // "naked" return — returns x and y
}Useful for documenting what each value means, and useful with defer for modifying a result on the way out. Avoid naked returns in anything longer than a few lines: the reader has to scroll to find what is actually returned.
Variadic functions#
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3)
sum(values...) // spread a sliceFunctions are values#
func apply(nums []int, f func(int) int) []int {
out := make([]int, len(nums))
for i, n := range nums {
out[i] = f(n)
}
return out
}
doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })Closures#
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
next := counter()
next() // 1
next() // 2Each call to counter gets its own count.
defer#
defer schedules a call to run when the surrounding function returns — including when it returns because of a panic.
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
defer f.Close() // runs no matter which return we take
return io.ReadAll(f)
}Deferred calls run last-in-first-out, and their arguments are evaluated immediately — at the defer line, not when it runs. That trips people up exactly once.
Common questions#
Why does Go not have default parameter values?#
Deliberate simplicity: there is one calling convention and no overload resolution to reason about. The idiomatic replacements are a config struct, or functional options (WithTimeout(5*time.Second)) when a package needs many optional settings.
When should I use named return values?#
When the names document something the types do not — (width, height int) — or when you need defer to modify the result, as in the close example above. Otherwise prefer explicit returns.
Is defer expensive?#
Not meaningfully since Go 1.14; it is close to free in the common case. Use it. The one place to think about it is a defer inside a hot loop, where it accumulates until the function returns rather than the loop iteration.
Get the Go agent pack
A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.
AGENTS.md now — no email needed.