Functions

📋 Jump to Takeaways

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)) // 8

When 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.3333333333333335

Named 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. Assign them to variables, pass them as arguments.

op := func(a, b int) int {
	return a * b
}
fmt.Println(op(3, 4)) // 12

func apply(a, b int, fn func(int, int) int) int {
	return fn(a, b)
}
fmt.Println(apply(5, 3, add)) // 8

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()) // 2

Closures 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()) // 3

The 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
}

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(). Any package can have an init() function, not just main. When you import a package, its init() runs before your code uses it.

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
  • defer runs on function exit in LIFO order — use for cleanup
  • init() runs before main() for package-level setup

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

© 2026 ByteLearn.dev. Free courses for developers. · Privacy