Functions in Go
Functions are first-class: you can define, return, pass, and store them.
Basics
Function parameters are typed after names. Return types follow the parameter list. Functions in the same package may be in different files and compile together as one unit. Exported functions start with an uppercase letter.
package main
import "fmt"
func add(a, b int) int { return a + b }
func main() {
fmt.Println(add(2, 3))
}
Multiple return values
Functions can return multiple values. Named results act like local variables; a bare return returns their current values. Use named results sparingly—they can reduce clarity if overused.
func divmod(a, b int) (quot, rem int) {
if b == 0 { return 0, 0 }
return a / b, a % b
}
func stats(xs []int) (sum int, avg float64) {
for _, v := range xs { sum += v }
if len(xs) > 0 { avg = float64(sum)/float64(len(xs)) }
return // named results
}
Error + value and ok-idiom
Go favors explicit error returns over exceptions. Errors are ordinary values; wrap with %w in fmt.Errorf to retain cause chains. Many operations also use the comma-ok idiom for presence tests (maps, type assertions, channel receives).
import "errors"
func divide(a, b float64) (float64, error) {
if b == 0 { return 0, errors.New("division by zero") }
return a / b, nil
}
m := map[string]int{"a":1}
if v, ok := m["a"]; ok { _ = v }
// wrap and check
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
Variadic parameters
Variadic parameters must be last and are received as a slice. Spread with ... when forwarding. Avoid allocating in hot paths by reusing slices when possible.
func sum(nums ...int) int {
s := 0
for _, v := range nums { s += v }
return s
}
xs := []int{1,2,3}
_ = sum(xs...) // spread
Defer (LIFO)
defer runs functions after the surrounding function returns (LIFO). Arguments are evaluated immediately, but execution is delayed—be mindful when deferring calls with large temporaries or needing updated values.
func do() {
defer cleanup()
// work
}
// capture updated value: use a closure
start := time.Now()
defer func() { log.Printf("took %v", time.Since(start)) }()
Function values
Function types are first-class values. You can pass, return, and store them in variables and structs. The zero value of a function type is nil; calling a nil function panics—guard when optional.
func apply(x int, f func(int) int) int { return f(x) }
func inc(n int) int { return n + 1 }
_ = apply(5, inc)
Methods on types (brief)
Define methods on named types with receivers. Use pointer receivers to mutate or to avoid copying large structs. Method sets differ: a value of type T has methods with receiver (T), a value of type *T has methods with receiver (T) and (*T); this affects interface satisfaction.
type Counter int
func (c *Counter) Inc() { *c++ } // pointer receiver mutates
func (c Counter) Value() int { return int(c) } // value receiver copies
Closures and capturing
Inner functions can capture outer variables by reference; be mindful of loop variable capture.
func Acc() func() int {
x := 0
return func() int { x++; return x }
}
Design guidelines
- Keep parameter lists small and explicit; consider small structs for related options
- Prefer returning concrete types and accepting interfaces (not the other way around)
- Document ownership: who allocates, who closes, who cancels context
- Make functions pure when possible; isolate side effects at boundaries
Best practices
- Keep functions small; use clear names and explicit params
- Return early on errors; avoid deep nesting
- Prefer explicit types at APIs; use named results sparingly
- Document side effects, ownership, and concurrency assumptions
- For methods: prefer pointer receivers when mutating or for large structs
Summary
- Functions are first-class
- Multiple returns, variadic params,
defer, function values, and methods provide flexibility