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:
- If the method mutates the receiver, use a pointer receiver.
- If the receiver is a large struct, prefer a pointer to avoid copies.
- 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).
- 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