Benchmark & fuzzing
Benchmark mengubah "terasa lebih cepat" jadi angka yang bisa dibandingkan. Fuzzing membangkitkan input aneh secara otomatis dan menemukan panic yang tidak terpikirkan siapa pun.
Intisari
func BenchmarkXxx(b *testing.B)denganfor b.Loop()โ bentuk baru sejak Go 1.24, dan diperbaiki lagi di 1.26.-benchmemmenampilkan alokasi; sering ia yang menjelaskan perbedaan waktu.- Bandingkan hasil dengan
benchstat, bukan dengan mata โ variasi antar-jalan itu besar. func FuzzXxx(f *testing.F)membangkitkan input otomatis; input yang membuat gagal disimpan sebagai kasus uji.- Fuzz paling berharga untuk parser, dekoder, validator, dan apa pun yang menerima byte dari luar.
Benchmark
func BenchmarkRender(b *testing.B) {
data := dataContoh(100)
b.ReportAllocs()
b.ResetTimer()
for b.Loop() { // Go 1.24+: menggantikan for i := 0; i < b.N; i++
_ = Render(data)
}
}
go test -bench=. -benchmem ./internal/web
go test -bench=Render -benchtime=5s ./internal/web
go test -bench=Render -count=10 ./internal/web > baru.txt
BenchmarkRender-8 3412 349821 ns/op 184320 B/op 2103 allocs/op
โ โ โ โ
โ โ โ โโ alokasi per operasi
โ โ โโ byte dialokasikan per operasi
โ โโ nanodetik per operasi
โโ berapa kali dijalankan
b.Loop() lebih dari sekadar sintaks yang lebih rapi. Ia menjaga parameter, hasil,
dan variabel yang ditugaskan tetap "hidup", sehingga compiler tidak mengoptimalkan habis badan
benchmark-mu โ masalah klasik yang membuat benchmark melaporkan 0,3 ns/op untuk pekerjaan nyata. Di
Go 1.26, b.Loop juga tidak lagi menghalangi inlining, sehingga hasilnya lebih
mendekati kode sungguhan.
Membandingkan dengan benar
go install golang.org/x/perf/cmd/benchstat@latest
go test -bench=. -count=10 ./... > lama.txt
# ... lakukan optimasi ...
go test -bench=. -count=10 ./... > baru.txt
benchstat lama.txt baru.txt
โ lama.txt โ baru.txt โ
โ sec/op โ sec/op vs base โ
Render-8 349.8ยต ยฑ 2% 131.2ยต ยฑ 1% -62.49% (p=0.000 n=10)
โ lama.txt โ baru.txt โ
โ B/op โ B/op vs base โ
Render-8 180.0Ki ยฑ 0% 42.1Ki ยฑ 0% -76.61% (p=0.000 n=10)
Satu jalan benchmark tidak berarti apa-apa. Variasi antar-jalan pada mesin biasa bisa 10โ20%
karena penjadwalan OS, frekuensi CPU, dan cache. -count=10 plus benchstat
memberi nilai p โ bukti bahwa perbedaannya nyata, bukan derau. Di CI berbagi (termasuk GitHub Actions),
perlakukan angka absolut dengan curiga; yang berguna hanya perbandingan pada mesin yang sama.
Alokasi biasanya yang menentukan
// โ 2103 alokasi
func Gabung(bagian []string) string {
hasil := ""
for _, b := range bagian {
hasil += b // string baru setiap iterasi (Fase 0)
}
return hasil
}
// โ
2 alokasi
func Gabung(bagian []string) string {
var b strings.Builder
b.Grow(perkiraanPanjang(bagian)) // sekali alokasi
for _, s := range bagian {
b.WriteString(s)
}
return b.String()
}
| Penyebab alokasi | Perbaikan |
|---|---|
| Menyambung string dalam loop | strings.Builder dengan Grow |
append tanpa kapasitas awal | make([]T, 0, n) |
fmt.Sprintf di jalur panas | Penyambungan langsung, strconv |
Nilai kecil di-boxing ke any | Generic atau tipe konkret (Fase 1) |
| Buffer besar per permintaan | sync.Pool โ setelah profiling |
Fuzzing
func FuzzParseKupon(f *testing.F) {
// Benih: contoh masukan yang valid dan yang sudah pernah bermasalah
f.Add("DISKON10")
f.Add("HEMAT-2026-XYZ")
f.Add("")
f.Fuzz(func(t *testing.T, s string) {
k, err := ParseKupon(s)
if err != nil {
return // menolak input buruk itu benar
}
// Invarian yang harus SELALU berlaku untuk input yang diterima
if k.Kode == "" {
t.Errorf("ParseKupon(%q) sukses tapi kodenya kosong", s)
}
if k.Diskon < 0 || k.Diskon > 100 {
t.Errorf("ParseKupon(%q) diskon = %d, di luar 0โ100", s, k.Diskon)
}
// Round-trip: format lalu parse lagi harus menghasilkan yang sama
lagi, err := ParseKupon(k.String())
if err != nil || lagi != k {
t.Errorf("round-trip gagal: %q โ %v โ %v", s, k, lagi)
}
})
}
go test -fuzz=FuzzParseKupon -fuzztime=60s ./internal/promo
fuzz: elapsed: 12s, gathering baseline coverage: 3/3 completed
--- FAIL: FuzzParseKupon (12.31s)
panic: runtime error: index out of range [1] with length 1
Failing input written to testdata/fuzz/FuzzParseKupon/a1b2c3d4
Input yang membuat gagal otomatis disimpan di testdata/fuzz/ dan ikut dijalankan
oleh go test biasa selamanya. Jadi fuzzing tidak hanya menemukan bug โ ia sekaligus menulis
tes regresinya untukmu. Commit berkas itu.
Apa yang layak di-fuzz
| Layak | Kurang berguna |
|---|---|
| Parser (tanggal, kupon, query filter) | Fungsi yang cuma memanggil database |
| Dekoder format biner | Kode dengan efek samping |
| Validasi input pengguna | Alur bisnis multi-langkah |
| Apa pun yang mengindeks slice dari input | Kode yang lambat per pemanggilan |
| Encode/decode yang harus round-trip | โ |
Urutan yang benar saat mengoptimalkan: ukur dengan profil di beban nyata (Fase 9) โ temukan
di mana waktunya benar-benar habis โ tulis benchmark untuk bagian itu โ optimalkan โ buktikan dengan
benchstat. Melewati langkah pertama menghasilkan kode yang lebih rumit tanpa lebih cepat.
Latihan: tulis benchmark untuk fungsi serialisasi responsmu, catat allocs/op-nya,
lalu kurangi dengan Grow atau prealokasi dan buktikan dengan benchstat. Lalu
tulis FuzzXxx untuk satu parser di kodemu dan jalankan 60 detik โ kalau ia menemukan
sesuatu, commit berkas testdata/fuzz/-nya.
Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.