Functions in Go
Go functions with multiple return values, named returns, variadic parameters, defer, closures, and the special init() function.
Basic Functions
Functions are the primary unit of code organization in Go. A function declaration names the function, lists its parameters with types, and declares what it returns. Go requires that every parameter be explicitly typed — there is no type inference for function signatures. This makes function signatures self-documenting and allows the compiler to catch argument mismatches at compile time.
// func name(params) returnType
func add(a, b int) int {
return a + b
}
// Multiple parameters of the same type can share the type annotation
func multiply(a, b int) int {
return a * b
}
// No return value — omit the return type entirely
func greet(name string) {
fmt.Printf("Hello, %s!\n", name)
}
func main() {
fmt.Println(add(3, 4)) // 7
fmt.Println(multiply(3, 4)) // 12
greet("Alice") // Hello, Alice!
}
Multiple Return Values
Multiple return values are one of Go’s most practical features. Rather than using out-parameters, exceptions, or result types, Go functions simply return multiple values. The dominant pattern is returning (result, error) — the caller must explicitly handle both, which makes error handling visible and impossible to accidentally skip.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide %.2f by zero", a)
}
return a / b, nil
}
func main() {
result, err := divide(10, 3)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%.4f\n", result) // 3.3333
_, err = divide(5, 0)
if err != nil {
fmt.Println(err) // cannot divide 5.00 by zero
}
}
Named Return Values
Named returns give the return variables names in the function signature. This serves two purposes: it documents what each return value represents, and it allows a bare return to automatically return the current values of those variables. Named returns work best for short functions where the bare return is obvious — avoid them in long functions where it becomes hard to track what value will be returned.
func minMax(nums []int) (min, max int) {
if len(nums) == 0 {
return // returns 0, 0 — the zero values of min and max
}
min, max = nums[0], nums[0]
for _, n := range nums[1:] {
if n < min {
min = n
}
if n > max {
max = n
}
}
return // bare return — returns the current values of min and max
}
func main() {
lo, hi := minMax([]int{3, 1, 4, 1, 5, 9, 2, 6})
fmt.Println(lo, hi) // 1 9
}
Named returns are best used when they clarify the function’s intent. Avoid them in long functions where the bare return becomes confusing.
Variadic Functions
A variadic function accepts a variable number of arguments for its last parameter. Inside the function, the variadic parameter is a slice of the declared type. This pattern is how fmt.Println accepts any number of arguments — it is not a special language feature but a regular function with a variadic parameter.
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5)) // 15
// Spread an existing slice into a variadic call with the ... operator
nums := []int{10, 20, 30}
fmt.Println(sum(nums...)) // 60
fmt.Println is itself variadic — that’s why it accepts any number of arguments.
Functions as Values
Functions in Go are first-class values: they can be assigned to variables, passed as arguments, returned from other functions, and stored in data structures. This enables powerful patterns like callbacks, middleware, and higher-order functions without needing a class hierarchy.
// Assign a function to a variable
square := func(x int) int { return x * x }
fmt.Println(square(5)) // 25
// Pass a function as an argument — enables generic transformations
func apply(nums []int, fn func(int) int) []int {
result := make([]int, len(nums))
for i, n := range nums {
result[i] = fn(n)
}
return result
}
nums := []int{1, 2, 3, 4, 5}
squared := apply(nums, func(n int) int { return n * n })
fmt.Println(squared) // [1 4 9 16 25]
// Return a function from a function — creates specialized functions from a template
func multiplier(factor int) func(int) int {
return func(x int) int {
return x * factor
}
}
triple := multiplier(3)
fmt.Println(triple(7)) // 21
Closures
A closure is a function value that captures and retains access to variables from its surrounding scope, even after that scope has returned. This is what makes the multiplier example above work — the returned function “closes over” the factor variable. Closures are the foundation for stateful function values, memoization, and many Go patterns.
func counter() func() int {
count := 0 // this variable is captured by the returned function
return func() int {
count++ // modifies the captured variable each time it's called
return count
}
}
c1 := counter()
c2 := counter() // a separate counter with its own independent count
fmt.Println(c1()) // 1
fmt.Println(c1()) // 2
fmt.Println(c1()) // 3
fmt.Println(c2()) // 1 — c2 has its own count, starting from zero
defer
defer schedules a function call to run just before the surrounding function returns, regardless of how it returns — normal completion, early return, or panic. This solves the problem of cleanup code: without defer, you have to remember to close a file or release a lock at every possible return path. With defer, you place the cleanup immediately after the resource is acquired and never have to think about it again.
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // guaranteed to run when readFile returns, no matter what
// read from f — f.Close() will run after this function exits
return nil
}
Multiple defers run in LIFO (last-in, first-out) order — the last deferred call runs first:
// LIFO order
func deferOrder() {
defer fmt.Println("third")
defer fmt.Println("second")
defer fmt.Println("first")
fmt.Println("function body")
}
// Output:
// function body
// first
// second
// third
defer with a lock
defer with mutex unlocks is one of the most important patterns in concurrent Go code. It guarantees the lock is always released, even if the protected code panics.
var mu sync.Mutex
func safeIncrement(counter *int) {
mu.Lock()
defer mu.Unlock() // lock is always released — even if *counter++ panics
*counter++
}
init()
init() is a special function that runs automatically before main(), after all package-level variables have been initialized. It is used for one-time setup that cannot be expressed as a variable initializer — registering drivers, validating configuration, or setting up global state. A package can have multiple init() functions, even across multiple files.
package config
import "os"
var DatabaseURL string
func init() {
// Runs automatically before main() — good for reading environment variables
DatabaseURL = os.Getenv("DATABASE_URL")
if DatabaseURL == "" {
DatabaseURL = "postgres://localhost:5432/mydb"
}
}
Rules for init():
- Takes no parameters
- Returns no values
- Cannot be called explicitly
- Runs after all variable initializations in the package
- Runs in the order the files are presented to the compiler
Recursive Functions
Recursive functions call themselves to solve problems that have a naturally recursive structure — trees, graphs, mathematical sequences, and divide-and-conquer algorithms. Go supports recursion but does not optimize tail calls, so deep recursion can exhaust the stack. For performance-sensitive code, the iterative version is usually preferred.
// Recursive — clean but recomputes subproblems
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
// Iterative — more efficient, avoids stack growth
func fibIterative(n int) int {
if n <= 1 {
return n
}
a, b := 0, 1
for i := 2; i <= n; i++ {
a, b = b, a+b // multiple assignment updates both values simultaneously
}
return b
}
Practical Example — Middleware Pattern
Functions as values enable the middleware pattern: wrapping a handler function with additional behavior (logging, authentication, rate limiting) without modifying the original function. Each wrapper takes a handler and returns a new handler, so wrappers can be composed freely.
package main
import (
"fmt"
"time"
)
type Handler func(string) string
// withLogging wraps a handler to log how long each request takes
func withLogging(h Handler) Handler {
return func(req string) string {
start := time.Now()
result := h(req)
fmt.Printf("handled %q in %v\n", req, time.Since(start))
return result
}
}
// withPrefix wraps a handler to prepend a prefix to every request
func withPrefix(prefix string, h Handler) Handler {
return func(req string) string {
return h(prefix + req)
}
}
func baseHandler(req string) string {
return "response for: " + req
}
func main() {
// Compose wrappers: request goes through prefix, then base, then logging wraps both
handler := withLogging(withPrefix("[v1] ", baseHandler))
fmt.Println(handler("users"))
}
// handled "users" in 1.2µs
// response for: [v1] users