Functions & Multiple Return Values Beginner¶
Functions are the primary building block in Go. Unlike most languages, Go functions natively support multiple return values, which powers Go's distinctive error-handling pattern. Functions are also first-class values -- they can be assigned to variables, passed as arguments, and returned from other functions.
Basic Function Syntax¶
func add(a int, b int) int {
return a + b
}
// Shorthand: same-type parameters share the type
func add(a, b int) int {
return a + b
}
// No return value
func logMessage(msg string) {
fmt.Println(msg)
}
Multiple Return Values¶
Go functions can return any number of values. This is not syntax sugar -- it's built into the language.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 3)
if err != nil {
log.Fatal(err)
}
The (value, error) Pattern¶
This is Go's most important convention. Almost every function that can fail returns (result, error).
// Standard library examples
f, err := os.Open("config.json")
data, err := io.ReadAll(f)
n, err := fmt.Println("hello")
val, err := strconv.Atoi("42")
conn, err := net.Dial("tcp", "localhost:8080")
Interview Tip
"Go uses multiple return values instead of exceptions. The convention is (value, error) where error is the last return value. This makes error paths explicit and impossible to accidentally ignore -- the compiler forces you to handle or explicitly discard the returned error."
Named Return Values¶
Return values can be named, giving them zero-value initialization and enabling "naked returns."
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = errors.New("division by zero")
return // naked return: returns result=0.0, err=<error>
}
result = a / b
return // naked return: returns result=<value>, err=nil
}
When Named Returns Are Useful¶
Named returns serve two legitimate purposes:
// 1. Documentation -- names appear in godoc
func ParseDuration(s string) (duration time.Duration, err error)
// 2. Deferred modification -- only way to modify returns in defer
func readFile(path string) (data []byte, err error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer func() {
if closeErr := f.Close(); closeErr != nil && err == nil {
err = closeErr // modifies the named return
}
}()
return io.ReadAll(f)
}
Avoid naked returns in long functions
Naked returns hurt readability. Use them only in short functions (< 10 lines). In longer functions, return values explicitly even if they're named.
// BAD: reader has to track what 'result' and 'err' are set to
func process(input string) (result string, err error) {
// ... 30 lines of code ...
return // what is being returned?
}
// GOOD: explicit return even with named values
func process(input string) (result string, err error) {
// ... 30 lines of code ...
return result, nil
}
Variadic Functions¶
A function can accept a variable number of arguments of the same type using ....
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
sum() // 0 (empty is valid)
Passing a Slice to a Variadic Function¶
Use ... to expand a slice into variadic arguments.
Variadic Must Be Last Parameter¶
func format(prefix string, values ...int) string { // OK
// ...
}
// func bad(values ...int, suffix string) // COMPILE ERROR
Real-World Variadic Patterns¶
// Functional options pattern
type Option func(*Server)
func NewServer(addr string, opts ...Option) *Server {
s := &Server{addr: addr}
for _, opt := range opts {
opt(s)
}
return s
}
srv := NewServer(":8080",
WithTimeout(30*time.Second),
WithMaxConns(100),
)
The Blank Identifier _¶
Use _ to explicitly discard return values you don't need.
// Discard the index
for _, v := range items {
process(v)
}
// Discard the value, keep the error
_, err := fmt.Println("hello")
// Discard all returns (rare, but valid)
_ = someFunction()
Don't silently discard errors
// BAD: error ignored
data, _ := os.ReadFile("config.json")
// GOOD: handle or at minimum log
data, err := os.ReadFile("config.json")
if err != nil {
return fmt.Errorf("reading config: %w", err)
}
_ is acceptable only when the function's documentation guarantees no meaningful error (rare).
First-Class Functions¶
Functions are values in Go. They can be assigned to variables, stored in data structures, and passed as arguments.
Functions as Values¶
op := func(a, b int) int { return a + b }
fmt.Println(op(3, 4)) // 7
op = func(a, b int) int { return a * b }
fmt.Println(op(3, 4)) // 12
Functions as Parameters¶
func apply(nums []int, transform func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = transform(n)
}
return result
}
doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2, 4, 6]
Function Types¶
Define named function types for clearer signatures.
type Predicate func(int) bool
type Transformer func(string) string
func filter(items []int, pred Predicate) []int {
var result []int
for _, item := range items {
if pred(item) {
result = append(result, item)
}
}
return result
}
evens := filter([]int{1, 2, 3, 4, 5}, func(n int) bool {
return n%2 == 0
})
Functions as Return Values¶
func multiplier(factor int) func(int) int {
return func(n int) int {
return n * factor
}
}
double := multiplier(2)
triple := multiplier(3)
fmt.Println(double(5)) // 10
fmt.Println(triple(5)) // 15
Closures
Functions returned from other functions capture variables from their enclosing scope. This is covered in depth in the intermediate topic on Closures & Anonymous Functions.
defer (Brief Overview)¶
defer schedules a function call to run when the enclosing function returns. It's essential for cleanup.
func readConfig(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close() // runs when readConfig returns
return io.ReadAll(f)
}
Key behaviors:
- Deferred calls execute in LIFO order (last deferred runs first).
- Arguments are evaluated immediately, but the call is delayed.
- Deferred functions can read and modify named return values.
Deep dive
defer, panic, and recover are covered in detail in the intermediate topic on Defer, Panic & Recover.
Quick Reference¶
| Feature | Syntax | Notes |
|---|---|---|
| Basic function | func f(a int) int { } |
Types after parameter names |
| Multiple returns | func f() (int, error) { } |
Parenthesized return types |
| Named returns | func f() (n int, err error) { } |
Enables naked return and defer modification |
| Naked return | return (no values) |
Only with named returns; avoid in long functions |
| Variadic | func f(args ...int) { } |
Must be last parameter; receives as []int |
| Expand slice | f(slice...) |
Passes slice elements as individual args |
| Discard return | _, err := f() |
Blank identifier |
| Function value | fn := func(x int) int { return x } |
Anonymous function assigned to variable |
| Function type | type Op func(int, int) int |
Named function type |
| Defer | defer f() |
Runs on function exit, LIFO order |
Best Practices¶
- Always return
erroras the last value -- this is the universal Go convention. - Handle every error -- use
_only when you're certain the error is irrelevant. - Keep functions short -- if a function needs named returns for readability, it might be too long.
- Use named returns for documentation, not for naked returns in complex functions.
- Use function types to simplify signatures with callback parameters.
- Prefer small, focused functions -- Go's multi-return makes it easy to compose.
- Use
deferfor cleanup immediately after acquiring a resource.
Common Pitfalls¶
Ignoring errors
Named return shadowing
func fetch(url string) (body []byte, err error) {
resp, err := http.Get(url)
if err != nil {
return // returns nil, err -- OK
}
// SUBTLE BUG: := creates a NEW err, shadows named return
body, err := io.ReadAll(resp.Body)
// ^ should be = not := (if err is already declared)
// Actually this is fine since body is new, but watch for cases
// where all vars on the left are already declared
}
Variadic nil vs empty
Defer in a loop
// BAD: deferred closes accumulate until function returns
for _, path := range files {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // won't close until outer function returns!
}
// FIX: wrap in an anonymous function
for _, path := range files {
if err := func() error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // closes at end of each iteration
return process(f)
}(); err != nil {
return err
}
}
Performance Considerations¶
- Function call overhead: Go function calls are cheap (~2-5ns). Don't inline manually -- the compiler does it for small functions (use
go build -gcflags="-m"to check). - Defer cost:
deferadds ~35-50ns overhead (significantly improved since Go 1.14). Use it freely for correctness; avoid it only in the tightest hot-path loops. - Closures and allocation: A closure that captures local variables may cause them to escape to the heap. In hot paths, consider passing values as parameters instead of capturing.
- Variadic allocation: Calling a variadic function allocates a slice. For hot paths with known argument counts, consider non-variadic overloads.
- Named returns: No performance difference versus unnamed returns. Choose based on readability.
Key Takeaways¶
- Go functions support multiple return values natively -- this powers the
(value, error)pattern. - Named returns are useful for documentation and
defermodification, but avoid naked returns in long functions. - Variadic functions use
...and must be the last parameter; expand slices withslice.... - Functions are first-class values -- assign, pass, and return them freely.
- Use
_to discard returns, but never silently discard errors in production code. deferensures cleanup runs on function exit -- use it immediately after resource acquisition.- The error return convention is
(result, error)with error always last.