On this page
Functions and Multiple Returns
Functions in Go: Flexible and Explicit
Functions are the fundamental building blocks of every Go program. Unlike Python or JavaScript, Go functions are more predictable: the static type system guarantees that you always know exactly what each function receives and returns.
The most important and distinguishing feature of Go functions is the ability to return multiple values. This feature is at the heart of the language's design and is fundamental to understanding idiomatic error handling.
Basic Function Declaration
// Simple function
func greet(name string) string {
return "Hello, " + name + "!"
}
// No parameters or return
func cleanup() {
fmt.Println("Cleaning up...")
}
// Same-type parameters — shorthand form
func add(a, b int) int {
return a + b
}
// Multiple parameter types
func format(name string, age int, active bool) string {
return fmt.Sprintf("%s (%d years, active: %v)", name, age, active)
}Multiple Return Values
This is the feature that makes Go so expressive for error handling. A function can return any number of values:
// Returns two values: result and error
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
// Returns three values
func minMax(nums []int) (int, int, error) {
if len(nums) == 0 {
return 0, 0, errors.New("empty slice")
}
min, max := nums[0], nums[0]
for _, n := range nums[1:] {
if n < min {
min = n
}
if n > max {
max = n
}
}
return min, max, nil
}
func main() {
// You must handle all return values
result, err := divide(10, 3)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Printf("Result: %.2f\n", result)
// Ignore a return with _ (generally not recommended)
min, max, _ := minMax([]int{5, 2, 8, 1, 9, 3})
fmt.Println("Min:", min, "Max:", max)
}Named Returns
Named returns let you assign names to return values. This has two benefits: it documents the meaning of each value, and it enables the "naked return":
// The names min, max, average are documentation AND variables
func analyze(data []float64) (min, max, average float64, err error) {
if len(data) == 0 {
err = errors.New("data required")
return // naked return: returns current state of min, max, average, err
}
min, max = data[0], data[0]
sum := 0.0
for _, d := range data {
sum += d
if d < min {
min = d
}
if d > max {
max = d
}
}
average = sum / float64(len(data))
return // naked return: returns current values
}Named returns are useful for functions that compute multiple related results, but avoid overusing naked returns in long functions — they can reduce readability.
Variadic Functions
Variadic functions 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
}
func log(level string, parts ...any) {
fmt.Printf("[%s] ", level)
fmt.Println(parts...)
}
func main() {
fmt.Println(sum()) // 0
fmt.Println(sum(1)) // 1
fmt.Println(sum(1, 2, 3)) // 6
fmt.Println(sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) // 55
// Pass a slice to a variadic function with ...
numbers := []int{10, 20, 30, 40}
fmt.Println(sum(numbers...)) // 100
log("INFO", "Server", "started", "on port", 8080)
}Inside the function, the variadic parameter behaves as a slice. The ... operator when calling the function expands a slice into individual arguments.
Functions as First-Class Values
In Go, functions are values. You can assign them to variables, pass them as arguments, and return them:
// Function type
type Transformer func(int) int
// Higher-order function
func applyTwice(f Transformer, x int) int {
return f(f(x))
}
func double(n int) int {
return n * 2
}
// Anonymous function assigned to a variable
triple := func(n int) int {
return n * 3
}
func main() {
fmt.Println(applyTwice(double, 3)) // 12
fmt.Println(applyTwice(triple, 2)) // 18
// Immediately invoked function expression (IIFE)
result := func(a, b int) int {
return a + b
}(5, 3)
fmt.Println(result) // 8
}Closures: Functions with State
A closure is a function that captures variables from its outer scope. The function "closes over" those variables, keeping them alive even after the outer scope has returned:
// Counter factory — each call creates an independent counter
func newCounter() func() int {
count := 0 // this variable lives as long as the closure exists
return func() int {
count++
return count
}
}
// Closure for memoization
func memoize(f func(int) int) func(int) int {
cache := make(map[int]int)
return func(n int) int {
if result, ok := cache[n]; ok {
return result
}
result := f(n)
cache[n] = result
return result
}
}
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}
func main() {
c1 := newCounter()
c2 := newCounter() // independent counter
fmt.Println(c1(), c1(), c1()) // 1 2 3
fmt.Println(c2(), c2()) // 1 2 (independent of c1)
fibMemo := memoize(fibonacci)
fmt.Println(fibMemo(40)) // fast thanks to the cache
}`defer`: Guaranteed Resource Cleanup
defer postpones the execution of a function until the surrounding function returns. It is the idiomatic way to guarantee that resources are released correctly, regardless of how the function exits (normal return, error, or panic):
import "os"
func readFile(name string) (string, error) {
f, err := os.Open(name)
if err != nil {
return "", err
}
defer f.Close() // guaranteed: runs when the function exits, no matter what
buf := make([]byte, 1024)
n, err := f.Read(buf)
if err != nil {
return "", err // f.Close() will still run
}
return string(buf[:n]), nil
}LIFO Order of defer
func demoDefer() {
defer fmt.Println("third") // runs first (LIFO)
defer fmt.Println("second") // runs second
defer fmt.Println("first") // runs last declared, runs first
fmt.Println("function running")
}
// Output:
// function running
// first
// second
// thirdRecursion
Go supports recursion with the same behavior as any language:
func factorial(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}
func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}For deep recursion, consider using iteration or accumulator techniques, since Go does not optimize tail calls automatically.
With a solid mastery of Go functions, in the next lesson we will learn structs — Go's way of organizing data and behavior to model the real world.
Sign in to track your progress