Functions in Go

Functions are first-class citizens in Go. They can be assigned to variables, passed as arguments, and returned from other functions.

Basic Function Syntax

func functionName(param1 type1, param2 type2) returnType {
    // function body
    return value
}

Simple Examples

// No parameters, no return
func sayHello() {
    fmt.Println("Hello!")
}

// With parameters
func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

// With return value
func add(a int, b int) int {
    return a + b
}

// Shortened parameter declaration (same type)
func multiply(a, b int) int {
    return a * b
}

Multiple Return Values

Go functions can return multiple values. This is idiomatic and heavily used:

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

// Calling
result, err := divide(10, 2)
if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println("Result:", result)

Named Return Values

You can name return values, which acts as documentation and allows "naked returns":

func rectangle(width, height float64) (area, perimeter float64) {
    area = width * height
    perimeter = 2 * (width + height)
    return  // Naked return (returns named values)
}

// Call
a, p := rectangle(5, 3)

Note: Naked returns can reduce readability in long functions. Use them sparingly.

Ignoring Return Values

Use _ to ignore values you don't need:

result, _ := divide(10, 2)  // Ignore error (not recommended for errors!)

// Common with maps
value, ok := myMap[key]
if !ok {
    // key not found
}

_, exists := myMap[key]  // Just check existence

Variadic Functions

Functions that accept a variable number of arguments:

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

// Calling
sum(1, 2, 3)        // 6
sum(1, 2, 3, 4, 5)  // 15
sum()               // 0

// Passing a slice (spread operator)
numbers := []int{1, 2, 3, 4}
sum(numbers...)  // 10

Variadic with Other Parameters

Variadic parameter must be last:

func printf(format string, args ...interface{}) {
    // format is required, args is variadic
}

Anonymous Functions

Functions without a name, often used inline:

// Assigned to a variable
add := func(a, b int) int {
    return a + b
}
result := add(3, 4)  // 7

// Immediately invoked
result := func(a, b int) int {
    return a + b
}(3, 4)  // 7

// Common with goroutines
go func() {
    fmt.Println("Running in a goroutine")
}()

Closures

A closure is a function that references variables from outside its body:

func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

c := counter()
fmt.Println(c())  // 1
fmt.Println(c())  // 2
fmt.Println(c())  // 3

c2 := counter()
fmt.Println(c2()) // 1 (separate counter)

Closure Gotcha with Loops

// WRONG - all goroutines share the same 'i'
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)  // Probably prints 3, 3, 3
    }()
}

// CORRECT - pass 'i' as a parameter
for i := 0; i < 3; i++ {
    go func(n int) {
        fmt.Println(n)  // Prints 0, 1, 2 (order may vary)
    }(i)
}

// Also correct in Go 1.22+ - loop variable is now per-iteration
for i := 0; i < 3; i++ {
    go func() {
        fmt.Println(i)  // Works correctly in Go 1.22+
    }()
}

Functions as Values

Functions are first-class values:

// Function type
type MathFunc func(int, int) int

func apply(fn MathFunc, a, b int) int {
    return fn(a, b)
}

add := func(a, b int) int { return a + b }
mul := func(a, b int) int { return a * b }

fmt.Println(apply(add, 3, 4))  // 7
fmt.Println(apply(mul, 3, 4))  // 12

Function as Parameter

// Higher-order function
func filter(nums []int, test func(int) bool) []int {
    var result []int
    for _, n := range nums {
        if test(n) {
            result = append(result, n)
        }
    }
    return result
}

// Usage
numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

evens := filter(numbers, func(n int) bool {
    return n%2 == 0
})
// evens: [2, 4, 6, 8, 10]

greaterThan5 := filter(numbers, func(n int) bool {
    return n > 5
})
// greaterThan5: [6, 7, 8, 9, 10]

Function as Return Value

func makeMultiplier(factor int) func(int) int {
    return func(n int) int {
        return n * factor
    }
}

double := makeMultiplier(2)
triple := makeMultiplier(3)

fmt.Println(double(5))  // 10
fmt.Println(triple(5))  // 15

Methods

Methods are functions with a receiver. Covered in detail in structs, but here's a preview:

type Rectangle struct {
    Width, Height float64
}

// Method with value receiver
func (r Rectangle) Area() float64 {
    return r.Width * r.Height
}

// Method with pointer receiver
func (r *Rectangle) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

rect := Rectangle{Width: 10, Height: 5}
fmt.Println(rect.Area())  // 50

rect.Scale(2)
fmt.Println(rect.Area())  // 200

Init Functions

Special functions that run before main():

package main

import "fmt"

var message string

func init() {
    // Runs before main
    message = "Hello from init!"
}

func main() {
    fmt.Println(message)
}
  • Each file can have multiple init functions
  • They run in the order they appear
  • Package init functions run after their imports' init functions
  • Use for initialization that can't be done with simple assignment
// Common use: register database drivers
import _ "github.com/lib/pq"  // Just runs init() to register driver

Recursion

Go supports recursive functions:

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

Note: Go doesn't guarantee tail call optimization, so deep recursion can cause stack overflow. For performance-critical code, prefer iteration.

Best Practices

Keep Functions Focused

// Good: single responsibility
func validateEmail(email string) bool { ... }
func sendEmail(to, subject, body string) error { ... }

// Bad: doing too much
func validateAndSendEmail(to, subject, body string) error { ... }

Accept Interfaces, Return Structs

// Good: accepts interface (flexible)
func process(r io.Reader) error { ... }

// Good: returns concrete type
func newBuffer() *bytes.Buffer { ... }

Use Meaningful Parameter Names

// Good
func transfer(from, to Account, amount Money) error

// Less clear
func transfer(a, b Account, m Money) error

Error Returns Last

// Idiomatic
func Read(filename string) ([]byte, error)

// Not idiomatic
func Read(filename string) (error, []byte)

Avoid Global State

// Bad: uses global state
var db *sql.DB

func GetUser(id int) (*User, error) {
    return db.Query(...)
}

// Good: explicit dependency
func GetUser(db *sql.DB, id int) (*User, error) {
    return db.Query(...)
}

// Or use a struct with methods
type UserStore struct {
    db *sql.DB
}

func (s *UserStore) GetUser(id int) (*User, error) {
    return s.db.Query(...)
}

Next Steps

Continue to 05-collections.md to learn about arrays, slices, and maps.