โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 8 ยท Testing & Kualitas Kode

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.

Sumber asli go.dev Resmi Rangkuman ~6 menit baca

Intisari

  • func BenchmarkXxx(b *testing.B) dengan for b.Loop() โ€” bentuk baru sejak Go 1.24, dan diperbaiki lagi di 1.26.
  • -benchmem menampilkan 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 alokasiPerbaikan
Menyambung string dalam loopstrings.Builder dengan Grow
append tanpa kapasitas awalmake([]T, 0, n)
fmt.Sprintf di jalur panasPenyambungan langsung, strconv
Nilai kecil di-boxing ke anyGeneric atau tipe konkret (Fase 1)
Buffer besar per permintaansync.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

LayakKurang berguna
Parser (tanggal, kupon, query filter)Fungsi yang cuma memanggil database
Dekoder format binerKode dengan efek samping
Validasi input penggunaAlur bisnis multi-langkah
Apa pun yang mengindeks slice dari inputKode 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.