Table of contents
- Function Declaration Basics
- Multiple Return Values
- Named Returns
- Variadic Arguments
- First-Class Functions
- Anonymous Functions and Closures
Function Declaration Basics
In Go, functions are declared with the func keyword. Parameter types come after the name, and the return type follows the parameter list.
package main
import "fmt"
func add(a int, b int) int {
return a + b
}
func main() {
result := add(3, 5)
fmt.Println(result) // 8
}
When consecutive parameters share the same type, you only need to write the type on the last one.
func add(a, b int) int {
return a + b
}
In C or Java, types come before names (int add(int a, int b)), but Go reverses this. It might feel awkward at first, but Go’s approach is actually easier to read with complex function signatures.
Multiple Return Values
The most distinctive feature of Go functions is the ability to return multiple values. It’s similar to Python’s tuple returns, but in Go this is a core language pattern.
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 3)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("result: %.2f\n", result) // result: 3.33
}
This pattern pervades all Go code. A function returns (value, error), and the caller checks the error. It’s the opposite philosophy from Java’s approach of throwing exceptions. The key idea is to make it hard to ignore errors, which is covered in more depth in Part 4.
Named Returns
You can give names to return values. They’re initialized to their type’s zero value at the start of the function, and a bare return statement returns those variables.
package main
import "fmt"
func swap(a, b string) (first, second string) {
first = b
second = a
return // Returns first and second
}
func main() {
x, y := swap("hello", "world")
fmt.Println(x, y) // world hello
}
This looks clean in short functions, but in longer ones it becomes hard to track which values are being returned. The Go community consensus is to use named returns only in short functions.
That said, there’s one clearly useful case: documenting the meaning of return values in godoc. A declaration like (quotient, remainder int) serves as documentation by itself.
Variadic Arguments
To let a function accept any number of arguments, use ....
package main
import "fmt"
func sum(nums ...int) int {
total := 0
for _, n := range nums {
total += n
}
return total
}
func main() {
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(10, 20, 30, 40)) // 100
// Spread a slice to pass it
numbers := []int{5, 10, 15}
fmt.Println(sum(numbers...)) // 30
}
nums ...int is treated as a []int slice inside the function. Conversely, to pass a slice to a variadic function, you need to spread it with slice....
Variadic parameters can only appear at the end of the parameter list. fmt.Println is the quintessential variadic function.
First-Class Functions
In Go, functions are first-class citizens. They can be stored in variables, passed as arguments to other functions, and returned as values.
package main
import "fmt"
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
}
func double(n int) int {
return n * 2
}
func main() {
nums := []int{1, 2, 3, 4}
doubled := apply(nums, double)
fmt.Println(doubled) // [2 4 6 8]
}
The apply function takes a slice and a transform function, applying it to each element. func(int) int is the function type. This pattern is used in callbacks, middleware, strategy patterns, and many other places.
When function types get long, you can create an alias with type.
type transformer func(int) int
func apply(nums []int, fn transformer) []int {
// ...
}
This makes the signature much cleaner.
Anonymous Functions and Closures
You can create unnamed functions on the spot. They can be called immediately or stored in a variable.
package main
import "fmt"
func main() {
// Store in a variable
greet := func(name string) string {
return "Hello, " + name
}
fmt.Println(greet("gopher")) // Hello, gopher
// Declare and call immediately
result := func(a, b int) int {
return a + b
}(3, 5)
fmt.Println(result) // 8
}
Anonymous functions truly shine when they work as closures. They can capture and continue using variables from the outer scope.
package main
import "fmt"
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
func main() {
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3
// A new counter is independent
another := counter()
fmt.Println(another()) // 1
}
The function returned by counter() captures the outer count variable. Each call increments count, and this variable persists as long as the function is alive. another is a separate closure, so it has its own count.
Looking at how each closure captures its own environment (count) from a memory perspective, the picture looks like this.
flowchart LR
Call1["counter() call #1"] --> Env1["Environment #1<br/>count = 0,1,2,3..."]
Env1 --> Fn1["next closure<br/>references Env #1"]
Call2["counter() call #2"] --> Env2["Environment #2<br/>count = 0,1..."]
Env2 --> Fn2["another closure<br/>references Env #2"]
Fn1 -.independent state.- Fn2
This pattern is useful when you need to encapsulate state without creating a full struct. It’s frequently used for injecting configuration into HTTP handlers or creating mock functions in tests.
One thing to watch out for: creating closures inside a loop that capture the loop variable can lead to unexpected results.
package main
import "fmt"
func main() {
fns := make([]func(), 3)
for i := 0; i < 3; i++ {
fns[i] = func() {
fmt.Print(i, " ")
}
}
for _, fn := range fns {
fn()
}
// Go 1.22+: 0 1 2
// Go 1.21 and earlier: 3 3 3
}
Starting with Go 1.22, loop variable scoping was changed so that each iteration creates a new scope, fixing this issue. Before that, you had to copy the loop variable to a local variable. If you see the odd-looking pattern i := i in older Go code, this is the reason.
The next part covers Go’s error handling. We’ll explore the error interface, defer, panic/recover, and Go’s unique approach to handling errors.




Loading comments...