Giao diện
🧪 Testing, Profiling & Error Handling
"Don't guess, measure."
Production-ready code requires rigorous testing và data-driven optimization.
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/opUnderstanding 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 usedComparing 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:
| Situation | Use |
|---|---|
| Invalid config at startup | log.Fatal or panic |
| Database temporarily down | return error |
| Nil pointer dereference bug | panic (fix the bug!) |
| User not found | return ErrNotFound |
| Index out of bounds | panic (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
Hoặc ủng hộ project để giúp chúng tôi tạo thêm nhiều content chất lượng! ☕
📊 Module 3 Summary
| Topic | Key Takeaway |
|---|---|
| CSP | Communicate via channels, not shared memory |
| Goroutines | 2KB stack, millions possible |
| Channels | Unbuffered = sync, Buffered = async |
| Patterns | Worker pool protects resources |
| Networking | Goroutine-per-connection scales |
| Testing | Table-driven + interfaces for mocks |
| Benchmarks | Watch ns/op AND allocs/op |
| Profiling | pprof: don't guess, measure |
| Errors | Return errors, panic is for bugs |