Unit test menjawab apakah kode berperilaku benar, tetapi tidak menjawab pertanyaan lain yang sama pentingnya: seberapa cepat kode tersebut berjalan? Ketika dua implementasi menghasilkan keluaran sama, pilihan sebaiknya didasarkan pada pengukuran, bukan dugaan. Go menyediakan benchmark bawaan melalui tipe testing.B dan flag -bench milik go test, tanpa library pihak ketiga. Pada chapter ini kita akan mengukur dua cara menggabungkan potongan string, membaca kolom hasil pengukuran, lalu mengenali flag benchmark yang paling sering dipakai.

Fungsi Benchmark dan testing.B #

Benchmark ditulis sebagai fungsi bernama BenchmarkXxx pada berkas _test.go, dengan satu parameter bertipe *testing.B. Tipe testing.B mewarisi kemampuan testing.T seperti Errorf dan Fatalf, ditambah penghitung iterasi yang mengatur berapa kali kode terukur diulang. Sejak Go 1.24 bentuk idiomatiknya memakai for b.Loop(): framework menentukan jumlah iterasi dan mengukur total waktu di dalam loop tersebut.

Kode di luar for b.Loop() dihitung sebagai setup dan tidak ikut diukur, sehingga penyiapan data sebaiknya diletakkan sebelum loop. Bentuk lama sebelum Go 1.24 memakai field b.N secara manual:

go
for i := 0; i < b.N; i++ {
	GabungDenganPlus(kataBench)
}

Perintah go test biasa tidak menjalankan benchmark; flag -bench mengaktifkannya dengan pola nama fungsi.

Kode yang Akan Diukur #

Kita membandingkan dua fungsi yang tugasnya sama: menggabungkan potongan string menjadi satu. GabungDenganPlus memakai operator += yang membuat string baru pada setiap penyambungan, sedangkan GabungDenganBuilder memakai strings.Builder yang menampung data dalam satu buffer. Simpan kode berikut sebagai penggabung.go:

go
package main

import (
	"fmt"
	"strings"
)

// kataBench adalah data tetap yang dipakai benchmark agar kedua fungsi
// diukur dengan input yang sama dan hasilnya dapat dibandingkan.
var kataBench = []string{"belajar ", "pengujian ", "di ", "bahasa ", "go"}

// GabungDenganPlus menggabungkan potongan kata memakai operator += .
// Setiap penyambungan membuat string baru, sehingga alokasi memori bertambah.
func GabungDenganPlus(kata []string) string {
	hasil := ""
	for _, k := range kata {
		hasil += k
	}
	return hasil
}

// GabungDenganBuilder menggabungkan potongan kata memakai strings.Builder
// yang menampung data di satu buffer sebelum dialokasikan menjadi string final.
func GabungDenganBuilder(kata []string) string {
	var b strings.Builder
	for _, k := range kata {
		b.WriteString(k)
	}
	return b.String()
}

func main() {
	fmt.Println(GabungDenganPlus(kataBench))
	fmt.Println(GabungDenganBuilder(kataBench))
}

Jalankan berkas tersebut dari folder chapter:

bash
go run penggabung.go

Output yang Dihasilkan:

text
belajar pengujian di bahasa go
belajar pengujian di bahasa go

Mari kita telaah baris demi baris kode di atas:

  1. Baris 10 (var kataBench = ...): mendefinisikan data uji tetap di level package, dipakai bersama oleh benchmark dan unit test.
  2. Baris 14-19 (func GabungDenganPlus): menggabungkan string dengan operator +=. Setiap iterasi mengalokasikan string baru karena string di Go bersifat tidak dapat diubah (immutable).
  3. Baris 24-29 (func GabungDenganBuilder): memakai strings.Builder yang menyimpan data di buffer internal, lalu memanggil b.String() sekali di akhir untuk menghasilkan string final.
  4. Baris 32-34 (func main): mencetak hasil kedua fungsi sebagai bukti keduanya identik, sehingga pembeda tinggal performa.

Berkas penggabung_test.go berisi dua benchmark dan satu unit test yang memastikan kedua fungsi setara:

go
package main

import "testing"

func TestGabung(t *testing.T) {
	naif := GabungDenganPlus(kataBench)
	builder := GabungDenganBuilder(kataBench)
	if naif != builder {
		t.Errorf("hasil kedua fungsi berbeda: %q vs %q", naif, builder)
	}
	if naif != "belajar pengujian di bahasa go" {
		t.Errorf("hasil tidak sesuai harapan: %q", naif)
	}
}

func BenchmarkGabungDenganPlus(b *testing.B) {
	b.ReportAllocs()
	for b.Loop() {
		GabungDenganPlus(kataBench)
	}
}

func BenchmarkGabungDenganBuilder(b *testing.B) {
	b.ReportAllocs()
	for b.Loop() {
		GabungDenganBuilder(kataBench)
	}
}

Poin penting pada berkas tersebut:

  1. Baris 5-14 (func TestGabung): unit test biasa yang memastikan kedua fungsi menghasilkan keluaran sama. Benchmark tanpa kebenaran hasil tidak berarti, karena implementasi tercepat yang salah tetap salah.
  2. Baris 16 (func BenchmarkGabungDenganPlus(b *testing.B)): nama fungsi benchmark wajib diawali Benchmark agar ditemukan oleh flag -bench.
  3. Baris 17 (b.ReportAllocs()): meminta framework melaporkan jumlah byte dan alokasi memori per iterasi, bukan hanya waktu.
  4. Baris 18-20 (for b.Loop()): hanya panggilan fungsi di dalam loop ini yang diukur; jumlah iterasi ditentukan oleh framework.

Menjalankan Benchmark #

Jalankan benchmark dengan pola . agar semua fungsi Benchmark ikut:

bash
go test -bench=. -benchtime=10x

Output yang Dihasilkan:

text
goos: linux
goarch: amd64
pkg: tutorial-website/content/go/testing/benchmark
cpu: Intel(R) Xeon(R) Processor
BenchmarkGabungDenganPlus-2      	      10	       344.2 ns/op	     112 B/op	       4 allocs/op
BenchmarkGabungDenganBuilder-2   	      10	       310.0 ns/op	      80 B/op	       3 allocs/op
PASS
ok  	tutorial-website/content/go/testing/benchmark	0.003s

Flag -benchtime=10x membatasi setiap benchmark menjadi tepat 10 iterasi sehingga perintah selesai hampir seketika; angka waktunya menjadi sangat kasar. Tanpa flag ini, go test -bench memakai -benchtime=1s bawaan: framework menaikkan jumlah iterasi bertahap sampai total waktu sekitar satu detik, lalu membagi total waktu dengan jumlah iterasi untuk mendapat ns/op yang jauh lebih stabil.

Membaca Hasil Benchmark #

Setiap baris hasil terdiri atas kolom-kolom berikut:

KolomArti
BenchmarkGabungDenganPlus-2Nama fungsi; angka 2 adalah nilai GOMAXPROCS saat pengukuran
10Jumlah iterasi yang dijalankan (b.N)
344.2 ns/opRata-rata waktu per iterasi dalam nanodetik
112 B/opByte memori yang dialokasikan per iterasi
4 allocs/opJumlah alokasi memori per iterasi

Pada hasil di atas, GabungDenganPlus melakukan 4 alokasi sedangkan GabungDenganBuilder hanya 3. Dengan lima potongan kata selisihnya memang tipis, tetapi operator += menyalin seluruh isi string lama pada setiap penyambungan, sehingga biayanya membesar cepat pada data panjang. Perbesar kataBench menjadi ratusan potongan lalu ukur ulang untuk melihat jurangnya.

Flag Benchmark yang Sering Dipakai #

FlagKegunaan
-bench=.Pola nama benchmark yang dijalankan (. berarti semua)
-benchtime=10xBatas iterasi tetap; ganti dengan 1s atau 100ms untuk batas waktu
-benchmemTampilkan B/op dan allocs/op untuk semua benchmark sekaligus
-count=5Ulangi seluruh benchmark 5 kali untuk melihat variabilitas
-cpu=1,2,4Ulangi benchmark dengan beberapa nilai GOMAXPROCS
-run=^$Lewati semua unit test agar hanya benchmark yang berjalan

Dengan menguasai pola BenchmarkXxx, b.Loop(), dan b.ReportAllocs(), kita kini punya alat untuk membandingkan implementasi berdasarkan data. Selanjutnya kita akan bertanya arah sebaliknya: bagaimana menemukan input yang membuat fungsi gagal, tanpa harus menebak-nebak sendiri — melalui fuzz testing.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

penggabung_test.go

Unduh berkas
go
package main

import "testing"

func TestGabung(t *testing.T) {
	naif := GabungDenganPlus(kataBench)
	builder := GabungDenganBuilder(kataBench)
	if naif != builder {
		t.Errorf("hasil kedua fungsi berbeda: %q vs %q", naif, builder)
	}
	if naif != "belajar pengujian di bahasa go" {
		t.Errorf("hasil tidak sesuai harapan: %q", naif)
	}
}

func BenchmarkGabungDenganPlus(b *testing.B) {
	b.ReportAllocs()
	for b.Loop() {
		GabungDenganPlus(kataBench)
	}
}

func BenchmarkGabungDenganBuilder(b *testing.B) {
	b.ReportAllocs()
	for b.Loop() {
		GabungDenganBuilder(kataBench)
	}
}

penggabung.go

Unduh berkas
go
package main

import (
	"fmt"
	"strings"
)

// kataBench adalah data tetap yang dipakai benchmark agar kedua fungsi
// diukur dengan input yang sama dan hasilnya dapat dibandingkan.
var kataBench = []string{"belajar ", "pengujian ", "di ", "bahasa ", "go"}

// GabungDenganPlus menggabungkan potongan kata memakai operator += .
// Setiap penyambungan membuat string baru, sehingga alokasi memori bertambah.
func GabungDenganPlus(kata []string) string {
	hasil := ""
	for _, k := range kata {
		hasil += k
	}
	return hasil
}

// GabungDenganBuilder menggabungkan potongan kata memakai strings.Builder
// yang menampung data di satu buffer sebelum dialokasikan menjadi string final.
func GabungDenganBuilder(kata []string) string {
	var b strings.Builder
	for _, k := range kata {
		b.WriteString(k)
	}
	return b.String()
}

func main() {
	fmt.Println(GabungDenganPlus(kataBench))
	fmt.Println(GabungDenganBuilder(kataBench))
}