Functions
Go functions look superficially like C functions — until they don't. They return
multiple values (the canonical
(result, err) pair), they're first-class (pass them around), and
they support closures. Once those click, half of Go's standard library reads
differently.
1 · The intuition
Most languages return one value from a function. If you want more, you wrap them in a tuple, a struct, an out-parameter, or a callback. Go just lets the function return more than one. This sounds like a small detail; in practice it's the pivot point of Go's whole error-handling philosophy.
The canonical Go function signature is func foo(x int) (result T, err error) —
the result and the error are siblings. Every function that can fail returns both;
the caller checks err first. No exceptions. No try / catch.
No Result<T, E> generic. Just two return values.
2 · Try it — the canonical shape
package main
import (
"errors"
"fmt"
)
// divmod returns quotient and remainder, plus an error if b is zero.
func divmod(a, b int) (int, int, error) {
if b == 0 {
return 0, 0, errors.New("division by zero")
}
return a / b, a % b, nil // nil error means "all good"
}
func main() {
q, r, err := divmod(17, 5)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("17 / 5 = %d remainder %d\n", q, r)
// Try the failure case
_, _, err = divmod(10, 0)
if err != nil {
fmt.Println("expected:", err)
}
}Three return values; the third is error by convention. The blank
identifier _ discards the values we don't care about — without it,
Go would complain about unused variables.
3 · Named return values — when they help
You can give the return values names in the signature. They start as zero values
and you can just return with no arguments to send them back. Useful
for documentation; controversial for everything else.
// Named returns — q, r, err are pre-declared.
func divmod2(a, b int) (q, r int, err error) {
if b == 0 {
err = errors.New("division by zero")
return // "naked return" — returns q, r, err
}
q = a / b
r = a % b
return // again, naked
}func (r *Reader) ReadByte() (c byte, err error) in io.
Don't use them just to avoid typing the return statement. Don't use them with
naked returns in functions longer than ~5 lines — they obscure the flow.4 · Variadic functions
A function can accept any number of arguments of one type using ...T.
The classic example is fmt.Println. You can write your own.
// sum accepts any number of ints. Inside the function, nums is a []int.
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
// Pass a slice as variadic with the ... spread
xs := []int{5, 5, 5}
fmt.Println(sum(xs...)) // 15
}5 · First-class functions & closures
Functions in Go are values. You can store them in variables, pass them as arguments, and return them from other functions. They can close over outer variables — exactly like a closure in JavaScript or Python.
// Pass a function as an argument
func apply(x int, fn func(int) int) int {
return fn(x)
}
func double(x int) int { return x * 2 }
func square(x int) int { return x * x }
func main() {
fmt.Println(apply(5, double)) // 10
fmt.Println(apply(5, square)) // 25
// Anonymous function (function literal)
fmt.Println(apply(5, func(x int) int { return x + 100 })) // 105
}Closures — counter factory
// makeCounter returns a function that closes over 'count'.
func makeCounter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
next := makeCounter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3
// Each call to makeCounter() gives a fresh closure with its own count
other := makeCounter()
fmt.Println(other()) // 1 — independent
}6 · defer — the surprise
defer schedules a function to run when the enclosing function
returns. It's how Go does "cleanup at the end" without try-finally. Multiple
defers run in reverse order (LIFO) — last deferred, first run.
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close() // runs when readFile returns, even on panic
// ... read from f ...
return result, nil
}
// Multiple defers — LIFO order
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("body")
}
// Output:
// body
// 3
// 2
// 1defer evaluates arguments
immediately, but runs the call later. defer fmt.Println("x=", x)
captures x's current value, not its value at function exit. If
you want the latest value, wrap in a closure:
defer func() { fmt.Println("x=", x) }().7 · Common mistakes
- Forgetting to check
err. Theif err != nilpattern feels repetitive — but skipping it produces bugs the type system can't catch. The compiler doesn't force you; the conventions do. - Returning a pointer to a stack variable. In Go, this is fine — the compiler does escape analysis and moves the variable to the heap automatically. Coming from C, it feels wrong; in Go, it's idiomatic.
- Mutating a closed-over loop variable. Classic gotcha:
for i := range xs { defer func() { fmt.Println(i) }() }— all defers print the same finali. Fix: pass it as an argument, or shadow withi := iinside the loop. (Since Go 1.22 this specific case was fixed forforloops, but the issue still applies elsewhere.) - Naked returns past 5 lines. Named returns + naked
returnin a long function makes it impossible to tell what's being returned. Either return explicitly or shrink the function. - Functions that take many arguments. Go conventions: 4+ arguments is a signal to introduce a struct.
func New(name string, age int, addr string, phone string, email string, isAdmin bool)begs to befunc New(opts UserOpts).
8 · When it clicks
- You write
(result, error)returns by reflex. - You spot when
deferis the right tool (file close, mutex unlock, panic recovery) versus when it's overkill. - You can read a closure that returns a closure and not flinch.
- You know that
func(int) intis a type, not a syntax oddity.