Skip to content

🧪 Testing, Profiling & Error Handling

"Don't guess, measure."
Production-ready code requires rigorous testing và data-driven optimization.

📋 Testing Strategy

Table-Driven Tests: The Go Standard

🎓 Professor Tom's Deep Dive: Why Table-Driven?

Benefits:

  • Add new test cases without new functions
  • Easy to see all edge cases at once
  • DRY (Don't Repeat Yourself)
  • Perfect for code review
go
func Add(a, b int) int {
    return a + b
}

// Table-driven test
func TestAdd(t *testing.T) {
    tests := []struct {
        name     string
        a, b     int
        expected int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -1, -2, -3},
        {"zero", 0, 0, 0},
        {"mixed", -5, 10, 5},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result := Add(tt.a, tt.b)
            if result != tt.expected {
                t.Errorf("Add(%d, %d) = %d; want %d", 
                    tt.a, tt.b, result, tt.expected)
            }
        })
    }
}

Testing with Mocks (Using Interfaces)

go
// Define interface in consumer package
type UserRepository interface {
    GetByID(ctx context.Context, id int64) (*User, error)
    Create(ctx context.Context, user *User) error
}

// Service depends on interface
type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUser(ctx context.Context, id int64) (*User, error) {
    user, err := s.repo.GetByID(ctx, id)
    if err != nil {
        return nil, fmt.Errorf("get user: %w", err)
    }
    return user, nil
}

// Mock for testing
type MockUserRepo struct {
    users map[int64]*User
    err   error
}

func (m *MockUserRepo) GetByID(ctx context.Context, id int64) (*User, error) {
    if m.err != nil {
        return nil, m.err
    }
    user, ok := m.users[id]
    if !ok {
        return nil, sql.ErrNoRows
    }
    return user, nil
}

func (m *MockUserRepo) Create(ctx context.Context, user *User) error {
    if m.err != nil {
        return m.err
    }
    m.users[user.ID] = user
    return nil
}

// Test with mock
func TestUserService_GetUser(t *testing.T) {
    tests := []struct {
        name    string
        userID  int64
        mock    *MockUserRepo
        want    *User
        wantErr bool
    }{
        {
            name:   "user found",
            userID: 1,
            mock: &MockUserRepo{
                users: map[int64]*User{1: {ID: 1, Name: "Raizo"}},
            },
            want:    &User{ID: 1, Name: "Raizo"},
            wantErr: false,
        },
        {
            name:   "user not found",
            userID: 999,
            mock: &MockUserRepo{
                users: map[int64]*User{},
            },
            want:    nil,
            wantErr: true,
        },
        {
            name:   "database error",
            userID: 1,
            mock: &MockUserRepo{
                err: errors.New("db connection lost"),
            },
            want:    nil,
            wantErr: true,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            svc := &UserService{repo: tt.mock}
            got, err := svc.GetUser(context.Background(), tt.userID)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            
            if !reflect.DeepEqual(got, tt.want) {
                t.Errorf("got = %v, want %v", got, tt.want)
            }
        })
    }
}

Benchmarking: testing.B

Basic Benchmark

go
func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(1, 2)
    }
}

// Run: go test -bench=. -benchmem
// Output:
// BenchmarkAdd-8   1000000000   0.29 ns/op   0 B/op   0 allocs/op

Understanding Output

BenchmarkAdd-8   1000000000   0.29 ns/op   0 B/op   0 allocs/op
      │               │          │           │          │
      │               │          │           │          └── Memory allocations per op
      │               │          │           └── Bytes allocated per op
      │               │          └── Time per operation
      │               └── Number of iterations
      └── Number of CPU cores used

Comparing Implementations

go
func ConcatWithPlus(strs []string) string {
    result := ""
    for _, s := range strs {
        result += s
    }
    return result
}

func ConcatWithBuilder(strs []string) string {
    var builder strings.Builder
    for _, s := range strs {
        builder.WriteString(s)
    }
    return builder.String()
}

func BenchmarkConcatPlus(b *testing.B) {
    strs := []string{"hello", "world", "foo", "bar", "baz"}
    for i := 0; i < b.N; i++ {
        ConcatWithPlus(strs)
    }
}

func BenchmarkConcatBuilder(b *testing.B) {
    strs := []string{"hello", "world", "foo", "bar", "baz"}
    for i := 0; i < b.N; i++ {
        ConcatWithBuilder(strs)
    }
}

// Results:
// BenchmarkConcatPlus-8      5000000   380 ns/op   80 B/op   4 allocs/op
// BenchmarkConcatBuilder-8  20000000    62 ns/op   64 B/op   1 allocs/op
//
// Builder is 6x faster with 4x fewer allocations!

Watch Memory Allocations

🔥 Raizo's Rule: Watch allocs/op

High allocations = GC pressure = latency spikes!

go
// ❌ BAD: Creates new slice every call
func ProcessBad(data []int) []int {
    result := []int{}  // Allocation!
    for _, v := range data {
        result = append(result, v*2)  // More allocations!
    }
    return result
}

// ✅ GOOD: Pre-allocate
func ProcessGood(data []int) []int {
    result := make([]int, 0, len(data))  // Pre-allocate!
    for _, v := range data {
        result = append(result, v*2)  // No reallocation
    }
    return result
}

🔬 Performance Analysis: pprof

Enable pprof

go
import (
    "net/http"
    _ "net/http/pprof"  // Import for side effects
)

func main() {
    // pprof endpoint auto-registered at /debug/pprof/
    go func() {
        log.Println(http.ListenAndServe(":6060", nil))
    }()
    
    // Your main application...
}

Collect and Visualize

bash
# CPU profile (30 seconds)
$ go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# Memory profile
$ go tool pprof http://localhost:6060/debug/pprof/heap

# Interactive shell
(pprof) top10          # Top 10 CPU consumers
(pprof) list funcName  # Source view
(pprof) web            # Visual graph (needs graphviz)

Common Commands

bash
# Run benchmark with profile
$ go test -bench=. -cpuprofile=cpu.prof -memprofile=mem.prof

# Analyze CPU
$ go tool pprof cpu.prof
(pprof) top10

# Analyze memory
$ go tool pprof mem.prof
(pprof) top10 -cum

⚠️ Error Handling Patterns

Custom Errors with Context

go
// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error: %s - %s", e.Field, e.Message)
}

// Sentinel errors
var (
    ErrNotFound     = errors.New("resource not found")
    ErrUnauthorized = errors.New("unauthorized")
    ErrForbidden    = errors.New("forbidden")
)

// Rich error with stack trace
type AppError struct {
    Op      string // Operation
    Code    string // Error code
    Message string
    Err     error  // Wrapped error
}

func (e *AppError) Error() string {
    if e.Err != nil {
        return fmt.Sprintf("%s: %s: %v", e.Op, e.Message, e.Err)
    }
    return fmt.Sprintf("%s: %s", e.Op, e.Message)
}

func (e *AppError) Unwrap() error {
    return e.Err
}

// Usage
func GetUser(id int64) (*User, error) {
    user, err := db.QueryUser(id)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, ErrNotFound
        }
        return nil, &AppError{
            Op:      "GetUser",
            Code:    "DB_ERROR",
            Message: "failed to query user",
            Err:     err,
        }
    }
    return user, nil
}

Panic vs Error: Decision Tree

💥 Panic is for UNRECOVERABLE situations only!

Is this a programmer bug (should never happen)?
├── YES → panic("invariant violated: ...")
└── NO

    Can the caller handle this gracefully?
    ├── YES → return error
    └── NO

        Is this application startup?
        ├── YES → log.Fatal (or panic)
        └── NO → return error (let caller decide)

Examples:

SituationUse
Invalid config at startuplog.Fatal or panic
Database temporarily downreturn error
Nil pointer dereference bugpanic (fix the bug!)
User not foundreturn ErrNotFound
Index out of boundspanic (fix the bug!)

Panic Recovery (HTTP Middleware)

go
func RecoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // Log with stack trace
                log.Printf("PANIC: %v\n%s", err, debug.Stack())
                
                // Return 500 to client
                http.Error(w, "Internal Server Error", 500)
                
                // Optional: Send to error tracking (Sentry, etc.)
                // sentry.CaptureException(fmt.Errorf("%v", err))
            }
        }()
        
        next.ServeHTTP(w, r)
    })
}

🎉 Module 3 Complete!

🎉 Chúc mừng! Module 3 Hoàn thành!

Bạn đã master Concurrency & High-Performance Systems:

  • CSP Model — Share memory by communicating
  • Goroutines — Lightweight concurrency
  • Channels — Safe data exchange
  • Patterns — Worker pools, fan-out/fan-in
  • Networking — TCP/HTTP engineering
  • Testing — Table-driven, benchmarks, pprof

You now have the power to build systems that handle millions of requests.


🎁 Take Your Skills Further

Get the HPN Ultimate Starter Kit with:

  • 📋 Raizo's strict linting rules (golangci-lint config)
  • 🏗️ Production project templates
  • 📊 Benchmark collection từ real projects
  • 🔒 Security checklist cho Go services

Get Starter Kit →


📊 Module 3 Summary

TopicKey Takeaway
CSPCommunicate via channels, not shared memory
Goroutines2KB stack, millions possible
ChannelsUnbuffered = sync, Buffered = async
PatternsWorker pool protects resources
NetworkingGoroutine-per-connection scales
TestingTable-driven + interfaces for mocks
BenchmarksWatch ns/op AND allocs/op
Profilingpprof: don't guess, measure
ErrorsReturn errors, panic is for bugs