Lesson 9 of 12
Errors, Panics & Testing
Learn how Go reports as ordinary return values, when panic is appropriate, and how to write tests with the built-in testing .
package main
import (
"errors"
"fmt"
"os"
)
// Sentinel errors — comparable values callers can test for
var ErrNotFound = errors.New("not found")
// Custom error types carry structured detail
type ValidationError struct {
Field string
Reason string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("field %q is invalid: %s", e.Field, e.Reason)
}
func findUser(id int) (string, error) {
users := map[int]string{1: "Ada"}
name, ok := users[id]
if !ok {
return "", fmt.Errorf("findUser %d: %w", id, ErrNotFound) // %w wraps
}
return name, nil
}
func main() {
_, err := findUser(99)
// errors.Is unwraps the chain looking for a specific value
if errors.Is(err, ErrNotFound) {
fmt.Println("no such user")
}
// errors.As unwraps looking for a specific TYPE, and binds it
var verr *ValidationError
if errors.As(err, &verr) {
fmt.Println("bad field:", verr.Field)
}
if _, err := os.Open("missing.txt"); errors.Is(err, os.ErrNotExist) {
fmt.Println("file is not there")
}
}package main
import (
"fmt"
"os"
)
// defer runs when the function returns — even on panic. LIFO order.
func processFile(path string) (err error) {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open %s: %w", path, err)
}
defer f.Close() // guaranteed cleanup
// Named return + recover turns a panic back into an error
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered: %v", r)
}
}()
// ... work that might panic ...
return nil
}
// Panic is for programmer error and unrecoverable state, NOT control flow.
func mustPositive(n int) int {
if n <= 0 {
panic("n must be positive") // a bug in the caller, not a runtime condition
}
return n
}
func main() {
if err := processFile("nope.txt"); err != nil {
fmt.Println("error:", err)
}
}// calc.go
package calc
import "errors"
func Add(a, b int) int { return a + b }
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// calc_test.go — file name must end in _test.go
package calc
import "testing"
func TestAdd(t *testing.T) {
if got := Add(2, 3); got != 5 {
t.Errorf("Add(2,3) = %d, want 5", got)
}
}
// Table-driven tests are the Go idiom
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b float64
want float64
wantErr bool
}{
{"simple", 10, 2, 5, false},
{"fraction", 1, 4, 0.25, false},
{"by zero", 1, 0, 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // subtest: named, isolated
got, err := Divide(tt.a, tt.b)
if (err != nil) != tt.wantErr {
t.Fatalf("unexpected error state: %v", err)
}
if !tt.wantErr && got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ { Add(1, 2) }
}
// go test ./... run everything
// go test -v -run Divide verbose, only matching tests
// go test -cover coverage
// go test -race race detectorFinished reading?Mark it done to track your progress.
Tip: use the ← and → keys to move between lessons.