← Back to Index

Chapter 3: Functions & Structs

Functions, methods, interfaces & struct composition

1. Functions

Basic Definition

// Simple function
func add(a int, b int) int {
    return a + b
}

// Same-type params can be grouped
func multiply(a, b float64) float64 {
    return a * b
}

Multiple Return Values

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

result, err := divide(10, 3)
if err != nil {
    log.Fatal(err)
}

Named Return Values

func swap(a, b string) (first, second string) {
    first = b
    second = a
    return // naked return, returns named values
}

Variadic Parameters

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([]int{4, 5}...)) // 9, spread a slice

First-Class Functions & Closures

// Functions are first-class values
var op func(int, int) int = add

// Anonymous function
square := func(x int) int { return x * x }
fmt.Println(square(5)) // 25

// Closure: captures outer variable
func counter() func() int {
    count := 0
    return func() int {
        count++
        return count
    }
}

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

// Higher-order function
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
}

doubled := apply([]int{1, 2, 3}, func(n int) int { return n * 2 })
// [2 4 6]

2. Structs

Definition & Fields

type User struct {
    ID       int
    Name     string
    Email    string
    IsActive bool
}

// Create instances
u1 := User{ID: 1, Name: "Alice", Email: "alice@example.com", IsActive: true}
u2 := User{Name: "Bob"} // Other fields get zero values

// Access fields
fmt.Println(u1.Name) // "Alice"
u1.Email = "alice@new.com"

Constructor Convention (NewXxx)

func NewUser(name, email string) *User {
    return &User{
        ID:       generateID(),
        Name:     name,
        Email:    email,
        IsActive: true,
    }
}

user := NewUser("Alice", "alice@example.com")

Struct Embedding (Composition over Inheritance)

type Address struct {
    City    string
    Country string
}

type Employee struct {
    User           // Embedded struct (promoted fields)
    Address        // Another embedded struct
    Department string
}

emp := Employee{
    User:       User{Name: "Alice"},
    Address:    Address{City: "Tokyo", Country: "Japan"},
    Department: "Engineering",
}

// Access promoted fields directly
fmt.Println(emp.Name)    // "Alice" (from User)
fmt.Println(emp.City)    // "Tokyo" (from Address)

Struct Tags

type Product struct {
    ID    int    `json:"id" db:"product_id"`
    Name  string `json:"name" validate:"required"`
    Price float64 `json:"price,omitempty"`
}

// Tags are used by encoding/json, ORMs, validators, etc.

πŸ”„ Go has no classes or inheritance

Go uses struct + methods + interfaces instead of class hierarchies. Composition (embedding) replaces inheritance. This leads to simpler, more flexible designs.

3. Methods

type Rect struct {
    Width, Height float64
}

// Value receiver: operates on a copy
func (r Rect) Area() float64 {
    return r.Width * r.Height
}

// Pointer receiver: can modify the original
func (r *Rect) Scale(factor float64) {
    r.Width *= factor
    r.Height *= factor
}

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

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

πŸ’‘ When to use pointer receiver?

  • When the method needs to modify the receiver
  • When the struct is large (avoids copying)
  • For consistency β€” if one method uses a pointer receiver, all should

4. Interfaces

Implicit Implementation

// Define an interface
type Shape interface {
    Area() float64
    Perimeter() float64
}

type Circle struct {
    Radius float64
}

// Circle implements Shape implicitly β€” no "implements" keyword
func (c Circle) Area() float64 {
    return math.Pi * c.Radius * c.Radius
}

func (c Circle) Perimeter() float64 {
    return 2 * math.Pi * c.Radius
}

// Use the interface
func printShape(s Shape) {
    fmt.Printf("Area: %.2f, Perimeter: %.2f\n", s.Area(), s.Perimeter())
}

printShape(Circle{Radius: 5}) // Works!

Empty Interface & any

// any is an alias for interface{} (since Go 1.18)
func printAnything(v any) {
    fmt.Printf("Type: %T, Value: %v\n", v, v)
}

printAnything(42)
printAnything("hello")
printAnything([]int{1, 2, 3})

Type Assertion & Type Switch

var val any = "hello"

// Type assertion
s, ok := val.(string)
if ok {
    fmt.Println("String:", s)
}

// Type switch
func describe(i any) string {
    switch v := i.(type) {
    case int:
        return fmt.Sprintf("integer: %d", v)
    case string:
        return fmt.Sprintf("string: %q", v)
    case bool:
        return fmt.Sprintf("boolean: %t", v)
    default:
        return fmt.Sprintf("unknown: %T", v)
    }
}

Common Interfaces: io.Reader & io.Writer

// io.Reader and io.Writer are the most important interfaces in Go
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Files, HTTP bodies, buffers, network connections all implement these.
// This is the power of Go interfaces: write once, works with everything.

πŸ’‘ Go interfaces are implicitly satisfied. A type implements an interface simply by having all required methods β€” no declaration needed. This enables powerful decoupling.

5. Generics (Go 1.18+)

Generic Functions

// Type parameter with constraint
func Min[T int | float64 | string](a, b T) T {
    if a < b {
        return a
    }
    return b
}

fmt.Println(Min(3, 7))         // 3
fmt.Println(Min(3.14, 2.71))   // 2.71
fmt.Println(Min("apple", "banana")) // "apple"

Constraints

import "golang.org/x/exp/constraints"

// Using built-in constraint interface
func Sum[T constraints.Integer | constraints.Float](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Custom constraint
type Number interface {
    int | int32 | int64 | float32 | float64
}

func Double[T Number](n T) T {
    return n * 2
}

Generic Structs

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Usage
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
val, _ := intStack.Pop() // 2

πŸ“‹ Chapter Summary

Multiple Returns

Functions can return multiple values. The (result, error) pattern is idiomatic Go.

Composition > Inheritance

Struct embedding promotes fields and methods. No class hierarchies.

Pointer vs Value Receiver

Use pointer receivers to mutate or for large structs. Value receivers for read-only operations.

Implicit Interfaces

Types satisfy interfaces automatically. No implements keyword. Enables loose coupling.

io.Reader/Writer

The most common interfaces. Files, HTTP, buffers β€” all share these interfaces.

Generics

Type parameters with constraints. Available since Go 1.18 for type-safe generic code.