Go Functions
Overview
Functions are first-class citizens in Go — they can be assigned to variables, passed as arguments, and returned from other functions. Types come after variable names. Go natively supports multiple return values, making error handling idiomatic without exceptions.
Basic Syntax
The type is written after the parameter name. If consecutive parameters share a type, you can omit all but the last.
// Full form
func add(a int, b int) int {
return a + b
}
// Shortened — shared type
func add(a, b int) int {
return a + b
}
fmt.Println(add(3, 4)) // 7
Multiple Return Values
Functions can return more than one value — no need to wrap in a struct or use output parameters. The convention is to return (result, error) as the last pair.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
result, err := divide(10, 2)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 5
Blank Identifier _
Use _ to discard return values you don't need — Go requires all declared variables to be used, so _ is the escape hatch.
result, _ := divide(10, 2) // ignore error (only when you're certain it won't error)
// Also works with range
for _, v := range nums { fmt.Println(v) }
Named Return Values
Named returns declare variables at the function signature level. A bare return (naked return) sends back their current values. Useful for short functions; hurts readability in long ones.
// Named returns: x and y are pre-declared
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return // returns x and y — naked return
}
a, b := split(17) // a=7, b=10
Rule: Only use naked returns in very short functions. In longer functions, name the returns for documentation but use explicit
return x, y.
Anonymous Functions
A function without a name. Used as a value — assign to a variable, pass as argument, or invoke immediately.
// Assign to variable
double := func(n int) int {
return n * 2
}
fmt.Println(double(5)) // 10
// Pass as argument
apply := func(nums []int, fn func(int) int) []int {
result := make([]int, len(nums))
for i, v := range nums {
result[i] = fn(v)
}
return result
}
fmt.Println(apply([]int{1, 2, 3}, double)) // [2 4 6]
// Immediately Invoked Function Expression (IIFE)
result := func(a, b int) int {
return a + b
}(3, 4)
fmt.Println(result) // 7
Closures
A closure is a function that captures variables from its surrounding scope. The captured variables persist and are shared between the closure and the outer function — they're not copied.
// Counter factory — each call to makeCounter returns an independent counter
func makeCounter() func() int {
count := 0 // captured by the closure
return func() int {
count++
return count
}
}
c1 := makeCounter()
c2 := makeCounter()
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c2()) // 1 — independent from c1
// Adder factory
func makeAdder(x int) func(int) int {
return func(y int) int {
return x + y // x is captured from makeAdder's scope
}
}
addFive := makeAdder(5)
fmt.Println(addFive(3)) // 8
fmt.Println(addFive(10)) // 15
Variadic Functions
A variadic function accepts any number of arguments of a given type using .... Inside the function, the variadic parameter is a slice.
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
sum(1, 2, 3) // 6
sum(1, 2, 3, 4, 5) // 15
// Spread a slice into a variadic call
nums := []int{1, 2, 3}
sum(nums...) // 6
fmt.Printlnis variadic:func Println(a ...any) (n int, err error)
defer — Delayed Execution
defer schedules a function call to run just before the surrounding function returns, regardless of how it returns (normal, error, panic). Arguments are evaluated immediately, but execution is delayed.
Typical uses: closing files, releasing locks, cleanup, logging.
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // guaranteed to run when readFile returns
// ... read from f
return nil
}
func main() {
ans := 10
defer fmt.Println("deferred:", ans) // ans captured = 10 right now
ans = 100
fmt.Println("main:", ans)
}
// main: 100
// deferred: 10
Rule 1 — Arguments evaluated immediately
func a() {
i := 0
defer fmt.Println(i) // captures i=0 now
i++
// prints 0, not 1
}
Rule 2 — LIFO (last in, first out)
Multiple defers run in reverse order — like a stack:
func b() {
defer fmt.Print("1 ") // runs third
defer fmt.Print("2 ") // runs second
defer fmt.Print("3 ") // runs first
}
// Output: 3 2 1
// Practical: acquire in order, release in reverse
defer mu.Unlock() // runs first (last deferred)
defer f.Close() // runs second
defer db.Close() // runs third
Rule 3 — Can read/write named return values
func double(n int) (result int) {
defer func() {
result *= 2 // modifies the named return after return
}()
result = n
return // returns n*2, not n
}
fmt.Println(double(5)) // 10
Functions as Values (First-Class)
Functions have types and can be stored in variables, maps, slices, or passed around like any other value.
// Function type
type MathFunc func(int, int) int
func apply(a, b int, fn MathFunc) int {
return fn(a, b)
}
add := func(a, b int) int { return a + b }
mul := func(a, b int) int { return a * b }
fmt.Println(apply(3, 4, add)) // 7
fmt.Println(apply(3, 4, mul)) // 12
// Dispatch table
ops := map[string]MathFunc{
"+": add,
"*": mul,
}
fmt.Println(ops["+"](10, 5)) // 15
Gotchas
| Gotcha | Detail |
|---|---|
defer LIFO | Last registered defer runs first |
defer args evaluated now | defer fmt.Println(x) captures x's current value |
| Naked return in long functions | Hard to reason about — use explicit returns |
| Closures capture by reference | All iterations of a loop closure share the same loop variable (Go < 1.22) — use v := v to copy |
_ doesn't suppress errors silently | Use it only when you're certain the value is truly irrelevant |