Bug pasti muncul di setiap proyek, dan perbedaan antar-programmer bukan jumlah bug melainkan cara menanganinya. Debugging tanpa metode biasanya berubah menjadi “coba ubah sesuatu, jalankan lagi, berharap berhasil” — lambat, melelahkan, dan sering menambal gejala bukan penyebab. Chapter ini menyusun alur kerja sistematis tujuh langkah, membandingkan printf debugging dengan debugger delve, lalu memetakan alat diagnosis untuk setiap jenis bug.

Alur Kerja Tujuh Langkah #

Alur ini memaksa kita memahami bug sebelum menyentuh kodenya. Setiap langkah punya bukti selesai yang jelas sehingga tidak ada langkah yang dilewati diam-diam:

  1. Reproduksi stabil: temukan cara memunculkan bug secara konsisten, idealnya setiap kali dijalankan. Bug yang hanya muncul sesekali hampir mustahil diverifikasi selesai.
  2. Isolasi: kecilkan cakupan sampai satu unit test gagal yang mereproduksi bug — fungsi tunggal, input tunggal, assertion tunggal.
  3. Hipotesis tertulis: rumuskan kalimat “bug terjadi karena X”. Hipotesis yang tidak ditulis cenderung bergeser diam-diam saat pengujian berlangsung.
  4. Uji hipotesis: kumpulkan bukti dengan printf debugging, debugger, atau git bisect untuk menemukan commit pemuncul.
  5. Perbaiki akar masalah: ubah penyebab, bukan gejalanya.
  6. Tulis test regresi: test dari langkah 2 dijadikan pengaman permanen.
  7. Refleksi: tanyakan mengapa bug bisa lolos — pertahanan apa yang layak ditambah.

Demonstrasi: printf Debugging #

Berkas debug-demo.go mencontohkan langkah 1 sampai 5 pada satu bug nyata: fungsi penjumlahan yang hasilnya meleset. Berikut isi lengkap berkas tersebut:

go
package main

import "fmt"

func main() {
        nilai := []int{70, 80, 90, 100}

        // Langkah 1 (reproduksi): hasil meleset dari dugaan manual 340.
        fmt.Println("hasil awal :", totalBermasalah(nilai), "(dugaan 340)")

        // Langkah 5 (perbaikan): akar masalah len(nilai)-1 dihapus.
        fmt.Println("perbaikan  :", total(nilai))
}

// totalBermasalah versi awal: pengulangan berhenti satu elemen terlalu cepat.
// printf debugging sengaja dibiarkan di dalam untuk mengamati jalannya loop.
func totalBermasalah(nilai []int) int {
        total := 0
        for i := 0; i < len(nilai)-1; i++ {
                total += nilai[i]
                fmt.Printf("iterasi i=%d nilai=%d total=%d\n", i, nilai[i], total)
        }
        return total
}

// total versi diperbaiki: batasan pengulangan i < len(nilai).
func total(nilai []int) int {
        total := 0
        for i := 0; i < len(nilai); i++ {
                total += nilai[i]
        }
        return total
}

Jalankan dari direktori chapter:

bash
go run debug-demo.go

Output yang Dihasilkan:

text
iterasi i=0 nilai=70 total=70
iterasi i=1 nilai=80 total=150
iterasi i=2 nilai=90 total=240
hasil awal : 240 (dugaan 340)
perbaikan  : 340

Mari kita telaah bagian-bagian penting berkas tersebut:

  1. Baris 6 (nilai := []int{70, 80, 90, 100}): data uji dengan jumlah yang mudah dihitung manual — 340 — sehingga penyimpangan langsung terlihat.
  2. Baris 9 (fmt.Println("hasil awal :", ...)): langkah reproduksi; hasil 240 terbukti meleset, bug dikonfirmasi nyata.
  3. Baris 19 (for i := 0; i < len(nilai)-1; i++): hipotesis setelah membaca output: hanya tiga iterasi terjadi untuk empat elemen, jadi batasan len(nilai)-1 mencurigakan.
  4. Baris 21 (fmt.Printf("iterasi i=%d ...")): printf debugging — cetak kondisi tiap iterasi membuktikan loop berhenti di i=2 dan elemen keempat (100) tidak pernah dijumlahkan.
  5. Baris 29 (for i := 0; i < len(nilai); i++): perbaikan akar — hapus -1; hasil kembali 340 sesuai dugaan manual.

printf Debugging vs delve #

Dua teknik pada langkah 4 saling melengkapi. printf debugging menambah cetakan sementara ke kode; delve adalah debugger interaktif yang menjalankan program sambil kita memeriksa keadaan.

Aspekprintf debuggingdelve
PersiapanTambah fmt.Printf, jalankan ulangdlv debug, pasang breakpoint
Kecepatan mulaiSangat cepat, tanpa alat tambahanPerlu instalasi dan latihan perintah
Mengubah kodeYa, jejak harus dibersihkanTidak, program disusupi via binary
Inspeksi runtimeHanya nilai yang sengaja dicetakSemua variabel, stack, goroutine
Jeda eksekusiTidak bisa berhenti kondisionalBreakpoint, watchpoint, step per baris
Paling pas untukBug alur data sederhana, log produksiBug dalam panggilan bersarang, panic misterius

Aturan praktisnya: mulai dengan printf untuk konfirmasi cepat, naik ke delve ketika pertanyaan berubah menjadi “bagaimana program sampai ke baris ini”. Peralatan delve — breakpoint kondisional sampai inspeksi goroutine — kita bahas tuntas di chapter Delve Debugger .

Alat untuk Tiap Jenis Bug #

Jenis bug menentukan alat yang paling menghemat waktu. Tabel berikut merangkum pemetaannya:

Jenis bugGejala umumAlat utamaRujukan chapter
LogikaHasil salah, kondisi terbalik, off-by-oneprintf debugging, delve, testPanduan ini
DataNilai korup, isi struct tak sesuai dugaanfmt.Printf("%#v"), watchpoint delveLogging untuk Debugging
ConcurrencyRace, deadlock, nilai hilang tanpa jejakgo test -race, trace, stack dumpRace Detector
PerformaLambat, CPU atau memori membengkakpprof, testing.B benchmarkCPU Profiling
Regresi perilaku“Dulu jalan, sekarang tidak”git bisect, test regresi—

Regresi, git bisect, dan Rubber Duck #

Langkah 6 mengubah pemahaman menjadi perlindungan permanen: test yang tadinya gagal kini menjadi penjaga agar bug yang sama tidak kembali:

go
func TestTotal(t *testing.T) {
        got := total([]int{70, 80, 90, 100})
        if got != 340 {
                t.Errorf("total() = %d, ingin 340", got)
        }
}

Untuk bug “dulu jalan, sekarang tidak”, git bisect membagi dua riwayat commit secara otomatis sampai commit pemuncul ditemukan — kita hanya menandai tiap commit “baik” atau “buruk”. Teknik pelengkap yang mengejutkan efektifnya adalah rubber duck debugging: menjelaskan bug lisan baris demi baris ke objek apa pun sering membuat otak menemukan ketidaksesuaian sebelum kalimat selesai.

Debugging menjaga program yang sudah jalan; kesehatan jangka panjangnya justru ditentukan sebelum bug lahir — saat kode ditinjau ulang oleh rekan. Daftar periksa untuk momen itu kita susun di chapter Checklist Code Review .

Referensi #

Berkas Sumber

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

debug-demo.go

Unduh berkas
go
package main

import "fmt"

func main() {
	nilai := []int{70, 80, 90, 100}

	// Langkah 1 (reproduksi): hasil meleset dari dugaan manual 340.
	fmt.Println("hasil awal :", totalBermasalah(nilai), "(dugaan 340)")

	// Langkah 5 (perbaikan): akar masalah len(nilai)-1 dihapus.
	fmt.Println("perbaikan  :", total(nilai))
}

// totalBermasalah versi awal: pengulangan berhenti satu elemen terlalu cepat.
// printf debugging sengaja dibiarkan di dalam untuk mengamati jalannya loop.
func totalBermasalah(nilai []int) int {
	total := 0
	for i := 0; i < len(nilai)-1; i++ {
		total += nilai[i]
		fmt.Printf("iterasi i=%d nilai=%d total=%d\n", i, nilai[i], total)
	}
	return total
}

// total versi diperbaiki: batasan pengulangan i < len(nilai).
func total(nilai []int) int {
	total := 0
	for i := 0; i < len(nilai); i++ {
		total += nilai[i]
	}
	return total
}