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 plain10 - 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 BTCError 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
erroras the last return value - Return
nilif no error occurred - Check
if err != nilafter function calls nilis Go’s equivalent ofnull
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
}