Skip to main content
Go: Pointers & Errors
  1. Posts/

Go: Pointers & Errors

·636 words·3 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

Following the Learn Go with Tests guide, diving into Go’s memory model and error handling with pointers and errors.

Wallet Example
#

Let’s build a Bitcoin wallet using TDD to explore pointers and error handling:

func TestWallet(t *testing.T) {
    wallet := Wallet{}
    wallet.Deposit(10)

    got := wallet.Balance()
    want := 10

    if got != want {
        t.Errorf("got %d want %d", got, want)
    }
}
type Wallet struct {
    balance int
}

func (w Wallet) Deposit(amount int) {
    w.balance += amount
}

func (w Wallet) Balance() int {
    return w.balance
}

Test fails: got 0 want 10 - Why?

Copy Problem
#

The problem is Go copies values when calling methods. Let’s look at memory addresses:

func TestWallet(t *testing.T) {
    wallet := Wallet{}
    wallet.Deposit(10)
    
    fmt.Printf("address of balance in test is %p \n", &wallet.balance)
    // Output: address of balance in test is 0xc420012260
}

func (w Wallet) Deposit(amount int) {
    fmt.Printf("address of balance in Deposit is %p \n", &w.balance)
    // Output: address of balance in Deposit is 0xc420012268
    w.balance += amount
}

Different addresses! The Deposit method works on a copy, not the original.

Pointers
#

Pointers let us reference the original value’s memory location:

func (w *Wallet) Deposit(amount int) {
    w.balance += amount
}

func (w *Wallet) Balance() int {
    return w.balance
}
  • Receiver type: *Wallet (pointer to wallet)
  • & gets a memory address: &wallet
  • * dereferences a pointer: *w.balance (but Go auto-dereferences struct pointers)

Auto-dereferencing: Go automatically converts w.balance to (*w).balance.


Use pointer receivers when:

  • Method needs to modify the receiver
  • Receiver is a large struct (avoid copying)
  • Consistency with other pointer receiver methods

Use value receivers when:

  • Method doesn’t modify the receiver
  • Receiver is a small, immutable value
  • You want a copy for safety

Custom Types
#

Instead of generic int, let’s create a Bitcoin type:

type Bitcoin int

type Wallet struct {
    balance Bitcoin
}

func (w *Wallet) Deposit(amount Bitcoin) {
    w.balance += amount
}

func (w *Wallet) Balance() Bitcoin {
    return w.balance
}
  • Domain clarity: Bitcoin(10) vs plain 10
  • Method attachment: Can add behavior to the type
  • Type safety: Can’t accidentally mix different units

Stringer Interface
#

Make our Bitcoin type print nicely:

func (b Bitcoin) String() string {
    return fmt.Sprintf("%d BTC", b)
}

The Stringer interface (from fmt package):

type Stringer interface {
    String() string
}

Usage in tests:

if got != want {
    t.Errorf("got %s want %s", got, want)
}
// Output: got 10 BTC want 20 BTC

Error Handling
#

In Go, functions return errors to signal problems:

wallet := Wallet{startingBalance}
err := wallet.Withdraw(Bitcoin(100))

if err == nil {
    t.Error("wanted an error but didn't get one")
}
func (w *Wallet) Withdraw(amount Bitcoin) error {
    if amount > w.balance {
        return errors.New("cannot withdraw, insufficient funds")
    }

    w.balance -= amount
    return nil
}

Error handling conventions:

  • Return error as the last return value
  • Return nil if no error occurred
  • Check if err != nil after function calls
  • nil is Go’s equivalent of null

Error Values
#

Instead of comparing error strings (brittle), use error values:

var ErrInsufficientFunds = errors.New(
    "cannot withdraw, insufficient funds")

func (w *Wallet) Withdraw(amount Bitcoin) error {
    if amount > w.balance {
        return ErrInsufficientFunds
    }

    w.balance -= amount
    return nil
}

Testing with error values:

func assertError(t testing.TB, got error, want error) {
    t.Helper()
    if got == nil {
        t.Fatal("didn't get an error but wanted one")
    }

    if !errors.Is(got, want) {
        t.Errorf("got %q, want %q", got, want)
    }
}

// Usage
assertError(t, err, ErrInsufficientFunds)

Why errors.Is() over direct comparison?

  • Wrapped errors: Works with error wrapping (fmt.Errorf("context: %w", err))
  • Error chains: Traverses the error chain to find matches

Error Checking
#

Use errcheck to find missed error handling:

go install github.com/kisielk/errcheck@latest
errcheck .

Always check errors from functions that return them:

// Bad: ignoring error
wallet.Withdraw(Bitcoin(10))

// Good: checking error
err := wallet.Withdraw(Bitcoin(10))
if err != nil {
    // handle error
}