Functions in Go With Multiple Returns and Named Results
Declare Go functions, return several values, use named results and naked returns with care, pass functions around and understand recursion and inlining.

Functions are the first thing taught and the thing that quietly shapes every Go API. The language made three choices that set it apart: functions return multiple values, errors are ordinary return values and not exceptions, and functions are values that you can pass around like integers. Those three explain most of what Go code looks like. This guide goes through declaring functions, the multiple-return convention, named results and their traps, function values and function types, recursion and its limits, and what the compiler does with small functions. It sits in the Go basics path, and four of the topics here have their own articles: variadic functions, closures, defer and error handling.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
Declaring a function and returning several values
A function declaration lists its parameters with types and its results. Adjacent parameters of the same type can share one type name, and results can be several types in parentheses:
package main
import (
"errors"
"fmt"
)
func divmod(a, b int) (int, int, error) {
if b == 0 {
return 0, 0, errors.New("division by zero")
}
return a / b, a % b, nil
}
func main() {
q, r, err := divmod(17, 5)
fmt.Println(q, r, err)
q, r, err = divmod(1, 0)
fmt.Println(q, r, err)
quotient, _, _ := divmod(100, 7)
fmt.Println(quotient)
}3 2 <nil>
0 0 division by zero
14The convention is strong enough to treat as part of the language: when a function can fail, the last result is an error, and a value of nil means success. The caller checks it on the next line, and the other results are meaningful only if the error is nil. When the function fails, return the zero values for the other results, as divmod does, so a caller who ignores the error gets a harmless value instead of garbage. The blank identifier _ discards a result you do not need. Using it for an error is a decision that deserves a comment, because a discarded error is the commonest cause of "it silently did nothing".
Several return values have a cost in readability once there are more than three. A function returning (string, int, bool, error) is telling you to define a struct. Two results and an error is usually the edge of comfortable.
Why errors are values
Go's designers chose return values over exceptions so that the failure path is visible in the code. Every call that can fail shows an if err != nil right below it, and no call can skip past the handler by throwing. The price is the repetition, and the benefit is that you can read a function top to bottom and see every exit. A function that returns only an error is commonly called in an if header, if err := save(item); err != nil { ... }, which keeps the variable scoped to the check and out of the rest of the function. When a call returns a value you need afterwards, declare the variables before the if, or use the ordinary two-line form, so the result is still in scope when you need it. Handling errors in Go covers wrapping, checking and logging them, and why Go returns errors instead of throwing exceptions is the design argument.
Named results
Results can be given names in the signature. They are declared as variables at the top of the function, set to their zero values, and a return with no operands returns whatever they hold at that moment:
package main
import (
"errors"
"fmt"
"strconv"
)
func split(sum int) (x, y int) {
x = sum * 4 / 9
y = sum - x
return
}
func parsePort(s string) (port int, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("parsePort %q: %w", s, err)
}
}()
port, err = strconv.Atoi(s)
if err != nil {
return 0, err
}
if port < 1 || port > 65535 {
return 0, errors.New("out of range")
}
return port, nil
}
func main() {
fmt.Println(split(17))
fmt.Println(parsePort("8080"))
fmt.Println(parsePort("http"))
fmt.Println(parsePort("70000"))
}7 10
8080 <nil>
0 parsePort "http": strconv.Atoi: parsing "http": invalid syntax
0 parsePort "70000": out of rangesplit uses the form most style guides discourage: a naked return. In a four line function it is readable. In a forty line one the reader has to scroll to find out what return means. My rule is to use named results for documentation, where the names clarify a pair of same-typed values such as (lat, lng float64), and to write the values in every return.
parsePort shows the one use I consider essential. Because err is a named result, a deferred function can read and change it after the return statement runs. The deferred function wraps every error from this function with the input that caused it, in a single place, instead of at each return. This is the idiom behind a lot of error annotation in real code, and the interaction with defer has a trap, covered in named return values and defer. The trap is that a shadowed err inside the function, declared with := in a nested block, is a different variable, and the deferred function never sees it. The shadowing article shows exactly how.
Parameters are copies
Everything passed to a function is copied. For basic values, structs and arrays, that makes the callee's variable independent. For slices, maps, channels and pointers, the copy refers to the same data, so changes to elements are visible to the caller. The mental model and the rules for each type are in pointers in Go and slices from the header to the backing array. The practical test for any parameter: if the function changes it, can the caller see the change? The answer is yes only through a pointer, a map, a channel or a slice's elements, and no for the slice's length and for everything else.
Functions are values
A function can be assigned to a variable, stored in a map, passed as an argument and returned from another function. A named function type gives the signature a name and makes code that uses it shorter:
package main
import (
"fmt"
"sort"
)
type Op func(a, b float64) float64
func apply(op Op, a, b float64) float64 { return op(a, b) }
func main() {
ops := map[string]Op{
"add": func(a, b float64) float64 { return a + b },
"sub": func(a, b float64) float64 { return a - b },
"mul": func(a, b float64) float64 { return a * b },
"pow": func(a, b float64) float64 {
r := 1.0
for range int(b) {
r *= a
}
return r
},
}
names := make([]string, 0, len(ops))
for name := range ops {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s(6, 3) = %g\n", name, apply(ops[name], 6, 3))
}
}add(6, 3) = 9
mul(6, 3) = 18
pow(6, 3) = 216
sub(6, 3) = 3A dispatch table like ops is a common alternative to a long switch. The names are sorted before printing, because ranging over a map is deliberately unordered. A function literal, the unnamed func(a, b float64) float64 {...}, can also be called immediately, func() { ... }(), which is how you create a scope or start a goroutine: go func() { ... }().
Function values have one restriction. They cannot be compared except to nil, because two function values may be the same code with different captured variables and the language does not define equality for that:
package main
import "fmt"
func a() {}
func b() {}
func main() {
f, g := a, b
fmt.Println(f == nil)
fmt.Println(f == g)
}# example.test/lab
./main.go:11:14: invalid operation: f == g (func can only be compared to nil)The compiler rejects the second comparison and says a func can only be compared to nil. The practical effect is that functions cannot be map keys, and that you cannot de-duplicate a list of callbacks by equality.
Giving a function type methods
A named function type can have methods, which sounds odd until you meet the standard library's use of it. It lets a plain function satisfy an interface:
package main
import "fmt"
type Greeter interface {
Greet(name string) string
}
type GreeterFunc func(name string) string
func (f GreeterFunc) Greet(name string) string { return f(name) }
func welcome(g Greeter, name string) {
fmt.Println(g.Greet(name))
}
type formal struct{}
func (formal) Greet(name string) string { return "Good day, " + name }
func main() {
welcome(formal{}, "Ada")
welcome(GreeterFunc(func(n string) string { return "hi " + n }), "Grace")
}Good day, Ada
hi GraceGreeterFunc is the adapter: it converts a function into something with a Greet method by calling itself. http.HandlerFunc is the best known case, and the same trick turns a closure into a sort.Interface-like value or a Reader. It is the reason a one-method interface and a function parameter are almost interchangeable, and it lets you offer both to callers without writing two APIs. The decision of which to expose is in accept interfaces, return structs.
Methods are functions with a receiver
A method is a function whose first parameter is written before the name, and Go lets you treat it as a function value in two ways. A method value binds a specific receiver, and a method expression leaves the receiver as an explicit first argument:
package main
import "fmt"
type Counter struct{ n int }
func (c *Counter) Inc() { c.n++ }
func (c Counter) Value() int { return c.n }
func main() {
c := &Counter{}
inc := c.Inc
inc()
inc()
fmt.Println(c.n)
incExpr := (*Counter).Inc
incExpr(c)
fmt.Println(c.Value())
value := Counter.Value
fmt.Println(value(*c))
}2
3
3c.Inc is a method value: the receiver c is captured when the expression is evaluated, so inc() increments that counter whenever it is called. (*Counter).Inc is a method expression with the signature func(*Counter), and it works for any receiver you pass. Method values are handy as callbacks, for instance button.OnClick(handler.Handle). The one trap is that the receiver is evaluated at the moment you write c.Inc, and for a value receiver it is copied then, so a method value bound to a value receiver does not see later changes to the original. For choosing between the receiver kinds, see pointer or value receiver.
Functions that take functions
Passing behavior as a parameter is how Go avoids a lot of class hierarchies. A retry helper is a good example because the function being retried can be anything, and a closure carries the state it needs:
package main
import (
"errors"
"fmt"
)
func retry(attempts int, fn func() error) error {
var err error
for i := 1; i <= attempts; i++ {
if err = fn(); err == nil {
return nil
}
fmt.Printf("attempt %d failed: %v\n", i, err)
}
return fmt.Errorf("after %d attempts: %w", attempts, err)
}
func main() {
calls := 0
flaky := func() error {
calls++
if calls < 3 {
return errors.New("temporary failure")
}
return nil
}
fmt.Println(retry(5, flaky), calls)
err := retry(2, func() error { return errors.New("permanent failure") })
fmt.Println(err)
}attempt 1 failed: temporary failure
attempt 2 failed: temporary failure
<nil> 3
attempt 1 failed: permanent failure
attempt 2 failed: permanent failure
after 2 attempts: permanent failureThe helper knows nothing about what it retries, and the caller's closure counts calls through a captured variable, which is the use of closures that the closures article digs into. A production retry would add a delay with jitter and stop on errors that will never succeed, topics covered in retry with exponential backoff and jitter.
Functions that produce sequences
Since Go 1.23 a function can also be the thing you loop over. A function with the shape func(yield func(T) bool) is an iterator, and for ... range accepts it directly. The standard library names the type iter.Seq[T]:
package main
import (
"fmt"
"iter"
)
func countdown(n int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := n; i > 0; i-- {
if !yield(i) {
return
}
}
}
}
func main() {
for v := range countdown(5) {
if v == 2 {
break
}
fmt.Print(v, " ")
}
fmt.Println()
}5 4 3The loop body becomes the yield function, and break makes yield return false so the iterator stops cleanly. The advantage over returning a slice is laziness: nothing is allocated for elements you never ask for. The details, and the Go 1.22 change that lets you range over an integer, are in range over integers and functions.
Testing a function
A function with clear inputs and outputs is the easiest thing in Go to test, and the table-driven style fits it exactly. This test pins the behavior of divmod from the top of the article, including the failing case:
package divmod
import "errors"
func Divmod(a, b int) (int, int, error) {
if b == 0 {
return 0, 0, errors.New("division by zero")
}
return a / b, a % b, nil
}package divmod
import "testing"
func TestDivmod(t *testing.T) {
cases := []struct {
name string
a, b int
q, r int
wantErr bool
}{
{"exact", 10, 5, 2, 0, false},
{"remainder", 17, 5, 3, 2, false},
{"negative", -17, 5, -3, -2, false},
{"zero divisor", 1, 0, 0, 0, true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
q, r, err := Divmod(c.a, c.b)
if (err != nil) != c.wantErr {
t.Fatalf("error = %v, wantErr %v", err, c.wantErr)
}
if q != c.q || r != c.r {
t.Errorf("got (%d, %d), want (%d, %d)", q, r, c.q, c.r)
}
})
}
}=== RUN TestDivmod
=== RUN TestDivmod/exact
=== RUN TestDivmod/remainder
=== RUN TestDivmod/negative
=== RUN TestDivmod/zero_divisor
--- PASS: TestDivmod (0.00s)
--- PASS: TestDivmod/exact (0.00s)
--- PASS: TestDivmod/remainder (0.00s)
--- PASS: TestDivmod/negative (0.00s)
--- PASS: TestDivmod/zero_divisor (0.00s)
PASS
ok example.test/lab 1.267sThe negative case documents something worth knowing: Go truncates integer division toward zero, so -17 / 5 is -3 and the remainder takes the sign of the dividend, -2. Languages that floor the division, such as Python, give different numbers, and a test is the quickest way to pin the behavior you rely on.
Recursion and the stack
A function can call itself, and Go's stacks start small and grow, so a deep recursion that would overflow a C stack works. The runtime allows a goroutine stack to grow to 1 GB by default. Here is a recursion a million frames deep, and one that never stops:
package main
import "fmt"
func down(n int) int {
var pad [64]byte
pad[n%64] = byte(n)
if n == 0 {
return int(pad[0])
}
return down(n-1) + int(pad[n%64]&1)
}
func forever(n int) int {
return forever(n+1) + 1
}
func main() {
fmt.Println(down(1_000_000))
fmt.Println(forever(0))
}500000
runtime: goroutine stack exceeds 1000000000-byte limit
runtime: sp=0x140200e03a0 stack=[0x140200e0000, 0x140400e0000]
...The first call, a million frames of about a hundred bytes each, finished and printed its answer. The second grew the stack until it hit the limit and ended the process with fatal error: stack overflow. Unlike a panic, that error cannot be recovered, because the runtime has nowhere safe left to run a handler. The takeaway is that recursion depth is not usually the problem in Go, memory is. Recursing over untrusted input, such as a deeply nested JSON document or a user-supplied tree, still needs an explicit depth limit, because an attacker controls how deep it goes. And the runtime has no tail call elimination, so a loop is always cheaper than a recursive function that could have been one.
Small functions are cheap
A common worry is that splitting code into many small functions slows it down. The compiler inlines small functions, replacing the call with the body. You can ask it what it did:
package main
import "fmt"
func add(a, b int) int { return a + b }
func clamp(x, lo, hi int) int {
if x < lo {
return lo
}
if x > hi {
return hi
}
return x
}
func main() {
fmt.Println(clamp(add(40, 5), 0, 42))
}# example.test/lab
example.test/lab/main.go:5:6: can inline add
example.test/lab/main.go:7:6: can inline clamp
example.test/lab/main.go:18:23: inlining call to add
example.test/lab/main.go:18:19: inlining call to clamp
example.test/lab/main.go:18:13: inlining call to fmt.Println
example.test/lab/main.go:18:13: ... argument does not escape
example.test/lab/main.go:18:19: ~r0 escapes to heapThe compiler's output lists can inline add and can inline clamp, and the calls inside main are reported as inlined. The budget is about 80 nodes of the syntax tree, and a function that contains a loop with a for statement used to be excluded, though recent releases allow more. The consequence is that a helper like add costs nothing and a hot path built from small functions runs as fast as the hand-merged version. The rule I follow is to extract a function whenever it gives a piece of logic a name, and to measure only if a profile points at a call.
Reading signatures: the standard library's naming habits
The names in the standard library follow patterns that tell you what a function does before you read its documentation. Learning them makes unfamiliar packages easier to read, and following them makes your own packages feel familiar.
| Prefix or shape | What it signals | Example |
|---|---|---|
New | Builds and returns a ready value | errors.New, bufio.NewReader |
Parse | Text in, value and error out | strconv.ParseInt, time.Parse |
Must | Panics instead of returning an error; for setup code | template.Must, regexp.MustCompile |
Is / Has | A boolean question | errors.Is, strings.HasPrefix |
With | Returns a modified copy or derived value | context.WithTimeout |
Append | Appends to the slice you pass and returns it | strconv.AppendInt |
Marshal / Unmarshal | Value to bytes and back | json.Marshal |
The Must convention deserves a rule of its own: use it only for values that are fixed when the program is written, like a compiled regular expression at package level, where a failure is a bug you want to see at startup. Never use a Must function on input that came from a user. Must functions and where they belong covers the idea in full.
Boolean parameters read badly
One last piece of API advice that is purely about function signatures. A call like render(page, true, false) is unreadable, since nothing at the call site says what the two booleans mean, and an editor tooltip is not documentation. A named type for the choice turns the call into a sentence:
type Align int
const (
Left Align = iota
Right
)
func pad(s string, width int, align Align) string { /* ... */ return s }
// pad("go", 8, Right) reads better than pad("go", 8, true)For several independent switches, a struct of options is clearer than a row of booleans, and it grows without breaking callers. If a function takes a boolean that changes its basic behavior, it is probably two functions. Splitting Close(force bool) into Close and Kill costs one extra name and removes a whole class of wrong-way arguments.
A function field is the lightest seam
Because functions are values, a struct can hold one, and that is often a simpler test seam than an interface. The classic case is the clock. Code that calls time.Now() directly cannot be tested for what happens after an expiry, unless the test sleeps. Hold the function instead:
type Session struct {
now func() time.Time
expires time.Time
}
func NewSession(ttl time.Duration) *Session {
s := &Session{now: time.Now}
s.expires = s.now().Add(ttl)
return s
}
func (s *Session) Expired() bool { return s.now().After(s.expires) }Production code gets time.Now, and a test replaces s.now with a closure returning a fixed instant, then moves it forward. No interface, no mock library, one field. The same approach works for random number sources, ID generators and anything else that should be deterministic under test. I would switch to an interface only when the dependency has several methods that belong together.
Writing good functions
A few habits matter more than the mechanics.
Return early. The happy path stays at the left margin, and each error is handled where it arises. Deep nesting is usually a function that did not return early.
Keep parameter lists short. Beyond four or five parameters, a struct or the options pattern is clearer, and functional options show the usual solution for optional settings. Boolean parameters are especially hard to read at the call site: render(page, true, false) says nothing.
Do one thing, and name it for what it returns. A function named process has no contract. A function named parseConfig that returns (Config, error) does.
Put the context.Context first. Any function that does I/O, blocks or can be cancelled takes a ctx as its first parameter, by convention, and the next sections of this site build on that.
When these rules bend
Named results with naked returns are fine in tiny functions and in a few standard library idioms. Multiple results beyond two are reasonable in internal helpers that no one else calls. A function that returns a function, a closure factory, is often the neatest way to hold configuration without a struct. And a very long function is acceptable when it is a flat table, such as a big switch that maps codes to messages, where splitting only adds indirection. Use judgement, and prefer code that a new teammate can read in one pass. The specification on function declarations and function types hold the exact rules, and Effective Go on functions is where multiple returns, named results and defer are introduced together.


