Measure First, Optimize Later
Prinsip utama performance optimization: jangan pernah optimize tanpa data. Banyak developer Indonesia yang langsung optimize kode yang belum tentu jadi bottleneck. Dari pengalaman saya, bottleneck yang paling sering ternyata bukan di tempat yang kita duga.
Go menyediakan built-in profiling tools yang sangat powerful. Mari kita pelajari cara menggunakannya.
Benchmarking dengan Go
Go punya built-in benchmarking framework yang sudah terintegrasi dengan package testing:
func BenchmarkUserLookup(b *testing.B) {
db := setupTestDB()
repo := NewUserRepository(db)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = repo.GetUserByID(context.Background(), "user-123")
}
}
func BenchmarkUserLookupParallel(b *testing.B) {
db := setupTestDB()
repo := NewUserRepository(db)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = repo.GetUserByID(context.Background(), "user-123")
}
})
}
// Run: go test -bench=. -benchmem -count=3
// Output:
// BenchmarkUserLookup-8 50000 23456 ns/op 1234 B/op 12 allocs/op
// BenchmarkUserLookupParallel-8 100000 12345 ns/op 1234 B/op 12 allocs/opPerhatikan kolom allocs/op - ini memberitahu kita berapa kali Go allocator bekerja. Mengurangi allocations adalah salah satu cara paling efektif untuk meningkatkan performa.
Memory Optimization
Berikut beberapa techniques yang saya gunakan untuk mengurangi memory allocation:
// ❌ Bad: membuat string baru setiap iteration
func BadConcat(items []string) string {
result := ""
for _, item := range items {
result += item // allocation baru setiap kali
}
return result
}
// ✅ Good: menggunakan strings.Builder
func GoodConcat(items []string) string {
var builder strings.Builder
builder.Grow(estimateSize(items)) // pre-allocate
for _, item := range items {
builder.WriteString(item)
}
return builder.String()
}
// ❌ Bad: membuat slice baru
func BadFilter(items []int) []int {
var result []int
for _, item := range items {
if item > 0 {
result = append(result, item) // reallocation
}
}
return result
}
// ✅ Good: pre-allocate dengan capacity
func GoodFilter(items []int) []int {
result := make([]int, 0, len(items)) // pre-allocate
for _, item := range items {
if item > 0 {
result = append(result, item)
}
}
return result
}Profiling Aplikasi Running
Untuk aplikasi yang sudah berjalan di production, Go menyediakan pprof endpoint yang bisa di-akses secara runtime:
import _ "net/http/pprof"
// Tambahkan di main.go
func main() {
// Start pprof server di port terpisah
go func() {
log.Println(http.ListenAndServe("localhost:6060", nil))
}()
// ... rest of your application
}
// Analisis dengan:
// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/cpu?seconds=30
// go tool pprof http://localhost:6060/debug/pprof/goroutineCaching Strategy
Kadang, performance terbaik datang dari tidak mengeksekusi sama sekali. Implement caching yang tepat bisa mengurangi response time dari seconds ke milliseconds. Saya biasanya gunakan multi-level caching: in-memory (untuk hot data), Redis (untuk shared cache), dan HTTP cache headers (untuk client-side caching).
Hasil Nyata
Dari beberapa optimization yang saya lakukan di production: response time turun dari 200ms ke 15ms (93% improvement), memory usage turun dari 512MB ke 128MB (75% reduction), dan throughput naik dari 1000 req/s ke 8000 req/s. Semua dengan perubahan kode yang relatively kecil karena kita fokus di bottleneck yang tepat.