Go Guide

Go, chapter by chapter · Lesson 6 of 62 · about 50 min

Functions & pointers

Parameters, multiple results, variadic calls, closures, recursion, defer, panic and recover, and pointers, with what happens on the call stack.

On this page · 13 sections
  1. 01Declaring and calling
  2. 02Multiple results
  3. 03Variadic functions
  4. 04Functions are values
  5. 05Closures
  6. 06Recursion
  7. 07defer, panic and recover
  8. 08Pointers
  9. 09More examples
  10. 10The full program
  11. 11Summary
  12. 12Check your understanding
  13. 13Practice in Go

Declaring and calling#

Go
func add(a int, b int) int {
	return a + b
}
 
func add2(a, b int) int { return a + b } // same type: list once

When you call add(2, 3), Go creates a new stack frame for add, copies 2 and 3 into its parameters a and b, runs the body, and hands the result back. When the function returns, its frame and local variables disappear (unless something still refers to them, as you will see with closures).

Multiple results#

Functions can return several values. This is how Go reports errors: the error comes last.

Go
func divmod(a, b int) (int, int) {
	return a / b, a % b
}
 
q, r := divmod(17, 5) // 3, 2
q, _ = divmod(9, 4)   // ignore the remainder with _

You may name the results. They start at zero values and a bare return returns them. Named results help document short functions and are required for one trick with recover below. Avoid bare returns in long functions; they make it hard to see what is returned.

Variadic functions#

A final parameter written ...T accepts any number of T values, which arrive as a []T:

Go
func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}
 
sum()            // 0
sum(1, 2, 3)     // 6
xs := []int{4, 5}
sum(xs...)       // spread a slice: 9

fmt.Println and append are variadic, which is why append(a, b...) concatenates slices.

Functions are values#

You can store a function in a variable, pass it as an argument, and return it from another function:

Go
apply := func(x int, f func(int) int) int { return f(x) }
double := func(n int) int { return n * 2 }
apply(5, double) // 10

This is how slices.SortFunc takes your comparison and how HTTP handlers are registered.

Closures#

A function literal can use variables from the function around it. It does not copy them: it captures them, so they live as long as the closure does.

Go
func counter() func() int {
	c := 0
	return func() int {
		c++
		return c
	}
}
 
next := counter()
next() // 1
next() // 2
other := counter()
other() // 1: a separate c

Each call to counter creates a fresh c, and each returned closure keeps its own.

In DSA code you will often write a recursive helper as a closure so it can read and update the outer function's variables without globals. A recursive closure must be declared before it is assigned:

Go
var dfs func(i int)
dfs = func(i int) {
	// ... can call dfs(i+1) and use outer variables
}

Recursion#

A recursive function calls itself on a smaller input until it reaches a base case that answers directly.

Go
func power(base, exp int) int {
	if exp == 0 {
		return 1 // base case
	}
	return base * power(base, exp-1)
}

Trace power(2, 3). Each call waits on the stack for the one below it:

CallWaits forReturns
power(2, 3)2 * power(2, 2)2 × 4 = 8
power(2, 2)2 * power(2, 1)2 × 2 = 4
power(2, 1)2 * power(2, 0)2 × 1 = 2
power(2, 0)nothing: base case1

Without a base case the calls never stop, and the program crashes with a stack overflow. Go has no tail-call optimization, so very deep recursion (millions of levels) should be rewritten as a loop.

defer, panic and recover#

defer schedules a call to run when the surrounding function returns, however it returns. It is perfect for cleanup:

Go
f, err := os.Open(path)
if err != nil {
	return err
}
defer f.Close() // runs at return, even on early returns

Several defers run in reverse order (last in, first out), and their arguments are evaluated when the defer line runs, not later.

panic stops normal execution: the current function stops, its deferred calls run, then its caller's, and so on up the stack. If nothing stops it, the program crashes with a stack trace. Index out of range and nil map writes cause panics automatically.

recover, called inside a deferred function, stops a panic and returns the panic value:

Go
func safeDiv(a, b int) (q int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()
	return a / b, nil
}

This is why the results are named: the deferred function sets err after the panic, and that value is what the caller receives.

See it: the defer stack

Pointers#

Go passes every argument by value: the function gets a copy. So this does nothing to the caller:

Go
func reset(n int) { n = 0 }

A pointer holds the memory address of a variable. Two operators work with it:

OperatorRead it asExample
&x"the address of x"p := &x makes p a *int
*p"the value p points to"*p = 0 sets x to 0
Go
func reset(p *int) { *p = 0 }
 
x := 5
reset(&x) // x is now 0

Trace it:

Stepxp (inside reset)
x := 55
reset(&x)5address of x
*p = 00address of x
return0gone

new(T) allocates a zero T and returns a *T. The zero value of any pointer is nil, and following a nil pointer panics.

When to use pointers:

  • The function must change the caller's value.
  • The value is a large struct and copying it would be wasteful.
  • You need to express "no value" with nil.

Slices and maps already contain a pointer internally, so you usually pass them directly. Go has no pointer arithmetic, so pointers are safe references, not raw memory addresses you can walk.

See it: copies and addresses

More examples#

The book's functions and exercises in one program

The chapter's examples (average, generators, factorial) and its exercises (half, a variadic max, an odd-number generator, Fibonacci, swap) are short enough to see together:

functions.go
Go
package main
 
import "fmt"
 
func average(xs []float64) float64 {
	total := 0.0
	for _, x := range xs {
		total += x
	}
	return total / float64(len(xs))
}
 
// half returns n/2 and whether n was even.
func half(n int) (int, bool) { return n / 2, n%2 == 0 }
 
// greatest is variadic: it accepts any number of ints.
func greatest(first int, rest ...int) int {
	best := first
	for _, v := range rest {
		best = max(best, v)
	}
	return best
}
 
// makeOddGenerator returns a closure that remembers i between calls.
func makeOddGenerator() func() uint {
	i := uint(1)
	return func() uint {
		ret := i
		i += 2
		return ret
	}
}
 
func factorial(x uint) uint {
	if x == 0 {
		return 1
	}
	return x * factorial(x-1)
}
 
func fib(n int) int {
	if n < 2 {
		return n
	}
	return fib(n-1) + fib(n-2)
}
 
func swap(x, y *int) { *x, *y = *y, *x }
 
// safeDivide turns a panic into an error with recover.
func safeDivide(a, b int) (q int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()
	return a / b, nil
}
 
func main() {
	fmt.Println("average:", average([]float64{98, 93, 77, 82, 83}))
	fmt.Println(half(1))
	fmt.Println(half(2))
	fmt.Println("greatest:", greatest(3, 9, 2), greatest(5))
	nums := []int{4, 8, 1}
	fmt.Println("spread a slice into ...:", greatest(nums[0], nums[1:]...))
 
	nextOdd := makeOddGenerator()
	fmt.Println("odds:", nextOdd(), nextOdd(), nextOdd())
 
	fmt.Println("5! =", factorial(5), " fib(10) =", fib(10))
 
	x, y := 1, 2
	swap(&x, &y)
	fmt.Println("after swap:", x, y)
 
	p := new(int) // new allocates a zero value and returns its address
	*p = 7
	fmt.Println("new(int):", *p)
 
	_, err := safeDivide(1, 0)
	fmt.Println(err)
}
Output
average: 86.6
0 false
1 true
greatest: 9 5
spread a slice into ...: 8
odds: 1 3 5
5! = 120  fib(10) = 55
after swap: 2 1
new(int): 7
recovered: runtime error: integer divide by zero

safeDivide uses a named result (err) so the deferred function can change what the function returns after a panic. That pattern is the main legitimate use of recover: turning a panic at a boundary (an HTTP handler, a plugin call) into an ordinary error.

Method values and function types

Functions are values, and so are methods bound to a value. A named function type documents intent and can even have its own methods; http.HandlerFunc in the standard library is exactly that.

functypes.go
Go
package main
 
import (
	"fmt"
	"slices"
	"strings"
)
 
type Transform func(string) string
 
// Then composes two transforms: a method on a function type.
func (t Transform) Then(next Transform) Transform {
	return func(s string) string { return next(t(s)) }
}
 
type Counter struct{ n int }
 
func (c *Counter) Inc() { c.n++ }
 
func main() {
	shout := Transform(strings.ToUpper).Then(func(s string) string { return s + "!" })
	fmt.Println(shout("halo"))
 
	var c Counter
	inc := c.Inc // a method value: bound to &c
	for range 3 {
		inc()
	}
	fmt.Println("count:", c.n)
 
	words := []string{"pear", "fig", "banana"}
	slices.SortFunc(words, func(a, b string) int { return len(a) - len(b) }) // a function literal as an argument
	fmt.Println(words)
}
Output
HALO!
count: 3
[fig pear banana]

The full program#

functions.go
Go
package main
 
import "fmt"
 
func divmod(a, b int) (q, r int) {
	return a / b, a % b
}
 
func sum(nums ...int) int {
	total := 0
	for _, n := range nums {
		total += n
	}
	return total
}
 
func counter() func() int {
	c := 0
	return func() int {
		c++
		return c
	}
}
 
func power(base, exp int) int {
	if exp == 0 {
		return 1
	}
	return base * power(base, exp-1)
}
 
func safeDiv(a, b int) (q int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("recovered: %v", r)
		}
	}()
	return a / b, nil
}
 
func resetCopy(n int)                  { n = 0 }
func resetPtr(p *int)                  { *p = 0 }
func apply(x int, f func(int) int) int { return f(x) }
 
func main() {
	q, r := divmod(17, 5)
	fmt.Println(q, r, sum(), sum(1, 2, 3), sum([]int{4, 5}...))
 
	next, other := counter(), counter()
	next()
	fmt.Println(next(), other())
 
	fmt.Println(power(2, 10), apply(7, func(n int) int { return n * n }))
 
	_, err := safeDiv(1, 0)
	fmt.Println(err)
 
	x := 5
	resetCopy(x)
	fmt.Println("after resetCopy:", x)
	resetPtr(&x)
	fmt.Println("after resetPtr:", x)
 
	p := new(int)
	*p = 42
	fmt.Println(*p, p != nil)
 
	for i := range 3 {
		defer fmt.Print(i, " ")
	}
	fmt.Print("deferred: ")
}
Output
3 2 0 6 9
2 1
1024 49
recovered: runtime error: integer divide by zero
after resetCopy: 5
after resetPtr: 0
42 true
deferred: 2 1 0

Summary#

  • Parameters are copies. Multiple results are normal, with error last.
  • ...T makes a function variadic; s... spreads a slice into it.
  • Functions are values; closures capture variables, which live as long as the closure.
  • Recursion needs a base case, and each call takes a stack frame.
  • defer runs at return in LIFO order; recover works only inside a deferred function.
  • &x takes an address, *p follows it; pass pointers when a function must change the caller's data.

Check your understanding

Answer before scrolling back up. If you miss one, reread the matching section, then try again.

  1. Q1.for i := range 3 { defer fmt.Print(i) } prints:

  2. Q2.A function receives an int parameter and sets it to 0. What happens to the caller's variable?

  3. Q3.What does p := new(int) give you?

  4. Q4.Where does recover stop a panic?

  5. Q5.What does counter() keep between calls in func counter() func() int { c := 0; return func() int { c++; return c } }?

Practice in Go

Write a table test first, then the solution. On LeetCode, choose Go as the language. Open a hint only after 15 minutes stuck.

  • This one is JavaScript-only on LeetCode. Write the Go version: a function returning a closure that returns n, n+1, n+2…

    Show hint

    Exactly the counter pattern from this lesson, with a starting value.

  • Show hint

    x^n = (x^(n/2))². Recurse on n/2 so the depth is log n, and handle negative n with 1/x.

  • Show hint

    Recursive: n == 1, or n divisible by 3 and n/3 is a power of three.

  • Safe divisionbuild

    Write safeDiv(a, b int) (int, error) that never panics, first with an explicit check, then with defer and recover. Explain which is better and why.

    Show hint

    The explicit check is better: panics are for bugs, errors for expected failures.

Finished this lesson?

Mark it done to track your progress. It is saved in this browser only.