Functions
📋 Jump to Takeaways🎁 Go functions can return multiple values — and that one feature is the reason Go has no exceptions, no try/catch, and no hidden error paths. How does that work?
Go functions can return multiple values. That single design decision eliminated the need for exceptions, try/catch blocks, and the entire class of "forgot to handle the error" bugs. It's also why Go code tends to be explicit about what can go wrong, and that's a feature, not a burden.
Basic Functions
func add(a, b int) int {
return a + b
}
fmt.Println(add(3, 5)) // 8When consecutive parameters share a type, you declare the type once.
Multiple Return Values
Functions can return multiple values. This is how Go handles errors instead of using exceptions.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
result, err := divide(10, 3)
if err != nil {
fmt.Println(err)
}
fmt.Println(result) // 3.3333333333333335Named Return Values
Named returns act as pre-declared variables. A bare return sends them back.
func divide(a, b float64) (result float64, err error) {
if b == 0 {
err = fmt.Errorf("division by zero")
return
}
result = a / b
return
}Use named returns sparingly. They help in short functions but hurt readability in long ones.
Variadic Functions
The ... syntax accepts zero or more arguments of the same type.
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()) // 0
nums := []int{4, 5, 6}
fmt.Println(sum(nums...)) // 15 — spread a slice with ...Functions as Values
Functions are first-class values. You can assign them to variables and call them through that variable.
multiply := func(a, b int) int {
return a * b
}
fmt.Println(multiply(3, 4)) // 12You can also pass functions as arguments to other functions. The parameter type describes the function's signature.
func apply(a, b int, fn func(int, int) int) int {
return fn(a, b)
}
fmt.Println(apply(5, 3, add)) // 8 — passing the add function from earlier
fmt.Println(apply(5, 3, multiply)) // 15 — passing the variable holding a functionThis pattern shows up everywhere in Go: sorting with custom comparators, HTTP handlers, middleware chains.
Anonymous Functions and Closures
An anonymous function is just a function without a name. A closure is a function that captures variables from its outer scope. They're different things, but in Go they almost always appear together.
// Anonymous function — no name, no captured variables
func() { fmt.Println("hello") }()
// Closure — captures `count` from outer scope
count := 0
increment := func() int {
count++
return count
}
fmt.Println(increment()) // 1
fmt.Println(increment()) // 2Closures are useful when you need a function that remembers state between calls, like counters, iterators, or middleware, without creating a struct.
func counter() func() int {
count := 0
return func() int {
count++
return count
}
}
next := counter()
fmt.Println(next()) // 1
fmt.Println(next()) // 2
fmt.Println(next()) // 3The returned function closes over count, keeping it alive between calls. Each call to counter() creates a new, independent count.
defer
defer says "run this later, right before the function exits." If you stack multiple defers, they run in reverse order: last one deferred runs first.
func readFile(path string) error {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close() // guaranteed cleanup
// ... read file
return nil
}Multiple defers in the same function run in reverse order — last in, first out.
func main() {
defer fmt.Println("first")
defer fmt.Println("second")
defer fmt.Println("third")
// Output:
// third
// second
// first
}Common uses: closing files, unlocking mutexes, flushing buffers.
init() Function
init() runs automatically before main() starts. Any package can have an init() function, not just main. When you import a package, its init() runs before main() begins — not at the point where you first call something from that package.
Common uses: loading config, registering drivers, validating environment.
var config map[string]string
func init() {
config = map[string]string{
"env": "development",
}
fmt.Println("config loaded") // runs before main
}
func main() {
fmt.Println(config["env"]) // development
}A package can have multiple init() functions. They run in the order they appear in the file.
Key Takeaways
- Functions can return multiple values — the standard pattern for error handling
- Named returns are pre-declared variables; use sparingly
- Variadic functions accept zero or more args with
... - Functions are first-class: assign to variables, pass as arguments
- Closures capture and retain access to outer variables
deferruns on function exit in LIFO order — use for cleanupinit()runs beforemain()for package-level setup
🎁 Next up: Go has no while, no do...while, no forEach — just one loop keyword that handles everything. Let's explore control flow.