Function basics

func add(a, b int) int {
    return a + b
}

When consecutive parameters share a type, you can write the type once (a, b int).

Multiple return values

Idiomatic Go uses multiple returns instead of out-parameters or exceptions:

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("divide by zero")
    }
    return a / b, nil
}

q, err := divide(10, 0)
if err != nil { ... }

Named return values

func split(sum int) (x, y int) {
    x = sum * 4 / 9
    y = sum - x
    return         // "naked" return: returns named values
}

Named returns can clarify intent, especially in defer-based error wrapping (see below). Don't overuse them — they hurt readability in long functions.

Variadic functions

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

sum(1, 2, 3)
nums := []int{1, 2, 3}
sum(nums...)         // spread a slice into variadic args

fmt.Printf is the canonical example.

First-class functions and closures

Functions are values:

add := func(a, b int) int { return a + b }
fmt.Println(add(2, 3))

A closure captures variables from its enclosing scope by reference:

func counter() func() int {
    n := 0
    return func() int {
        n++
        return n
    }
}

c := counter()
c(); c(); fmt.Println(c()) // 3

Each call to counter() creates a fresh n.

Methods

A method is a function with a receiver declared between func and the method name:

type Rectangle struct {
    W, H float64
}

func (r Rectangle) Area() float64 {
    return r.W * r.H
}

r := Rectangle{3, 4}
fmt.Println(r.Area())

You can declare methods only on named types defined in the same package. You cannot add a method to int directly, but you can to type Celsius float64.

Value vs pointer receiver

func (r Rectangle) AreaV() float64    { return r.W * r.H }   // value receiver
func (r *Rectangle) Scale(f float64)  { r.W *= f; r.H *= f } // pointer receiver

Rules of thumb:

  1. If the method mutates the receiver, use a pointer receiver.
  2. If the receiver is a large struct, prefer a pointer to avoid copies.
  3. If any method on the type uses a pointer receiver, all methods should typically use a pointer receiver (consistency, and for interface satisfaction — see Chapter 7).
  4. For small immutable types (time.Time, image.Point), value receivers are fine and idiomatic.

Calling a pointer method on an addressable value automatically takes the address:

r := Rectangle{3, 4}
r.Scale(2)            // shorthand for (&r).Scale(2)

Methods on non-struct types

type ID int
func (id ID) Valid() bool { return id > 0 }

Constructors

Go has no constructors. Convention is New<Type> returning a value or pointer:

func NewRectangle(w, h float64) *Rectangle {
    return &Rectangle{W: w, H: h}
}

For zero-value-friendly types you don't need a constructor at all (bytes.Buffer, sync.Mutex work zero-initialized).

Errors as values

import "errors"

var ErrNotFound = errors.New("not found")

func lookup(k string) (string, error) {
    ...
    return "", ErrNotFound
}

if v, err := lookup("x"); err != nil {
    if errors.Is(err, ErrNotFound) {
        ...
    }
    return err
}

You'll see error handling in nearly every Go function. Embrace it.

Wrapping errors

return fmt.Errorf("read config %q: %w", path, err)

%w wraps; errors.Is and errors.As walk the chain.

defer-based error wrapping with named returns

func process(p string) (err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf("process %s: %w", p, err)
        }
    }()
    ...
    return doWork()
}

Panic, recover, and when not to use them

Use panic only for programming errors that should crash the program (an impossible state, an out-of-range constant). Use recover only at the boundary of a long-running goroutine to keep a server alive. For normal business errors, return error.

Run the example

go run ./part1-language/examples/06_funcs