Clean Code dengan Go — Part 4: Komentar yang Tepat Guna
Komentar yang baik menjelaskan MENGAPA, bukan APA. Pelajari kapan harus komentar dan kapan membiarkan kode berbicara sendiri.
Bayangkan kamu baru bergabung di sebuah tim dan harus memperbaiki bug di modul pembayaran. Kamu membuka file-nya, dan disambut oleh baris seperti ini:
// increment i by 1
i++
// get user
user, err := getUser(id)
// this is the user service
type UserService struct { ... }
Komentar di mana-mana. Tapi semuanya hanya mengulang apa yang sudah jelas dari kode. Kamu menggulir lebih jauh, mencari penjelasan tentang kenapa kalkulasi diskon terasa aneh — kok bisa 0% untuk pelanggan baru tapi 15% untuk pelanggan lama yang baru satu transaksi?
Tidak ada komentar sama sekali di sana.
Inilah ironi terbesar dalam dunia komentar kode: programmer menulis komentar untuk hal-hal yang tidak perlu dijelaskan, dan membiarkan hal-hal yang benar-benar perlu dijelaskan tanpa komentar apapun. Komentar yang baik bukan tentang kuantitas — melainkan tentang tepat sasaran.
🗺️ Diagram: Kapan Harus Menulis Komentar?
flowchart TD
A[Mau menulis komentar?] --> B{Apakah kode sudah self-explanatory?}
B -- Ya --> C["❌ Jangan tulis komentar\nRename jika nama kurang jelas"]
B -- Tidak --> D{Kenapa tidak jelas?}
D -- Nama buruk --> E["✏️ Rename variabel/fungsi\nlalu cek lagi"]
D -- Logika kompleks --> F["✅ Tulis komentar WHY\nbukan WHAT"]
D -- Business rule --> G["✅ Tulis komentar\n+ link ke docs/ticket"]
E --> B
🧠 Konsep Inti
Komentar Harus Menjelaskan MENGAPA, Bukan APA
Kode yang bagus sudah menjelaskan apa yang dilakukannya — nama fungsi, nama variabel, dan struktur kode adalah dokumentasi itu sendiri. Yang tidak bisa dijelaskan oleh kode adalah mengapa keputusan tertentu diambil.
// ❌ BAD: komentar mengulang kode
// loop through users and check if active
for _, u := range users {
if u.IsActive { // check if active
send(u) // send to user
}
}
// ✅ GOOD: kode berbicara sendiri, komentar hanya di tempat yang perlu
for _, u := range users {
if u.IsActive {
send(u)
}
}
Konvensi GoDoc
Go punya konvensi komentar khusus untuk dokumentasi publik. Setiap exported identifier harus diawali dengan nama identifier itu sendiri.
// ✅ GOOD: GoDoc convention
// UserService mengelola operasi bisnis terkait pengguna.
type UserService struct {
repo UserRepository
}
// FindByEmail mencari pengguna berdasarkan alamat email.
// Mengembalikan ErrNotFound jika pengguna tidak ditemukan.
func (s *UserService) FindByEmail(email string) (*User, error) {
return s.repo.FindByEmail(email)
}
TODO/FIXME dengan Konteks
Jangan tinggalkan TODO tanpa pemilik atau tiket. TODO tanpa konteks adalah utang teknis yang tidak pernah dibayar.
// ❌ BAD: TODO tanpa konteks
// TODO: fix this later
// ✅ GOOD: TODO dengan pemilik dan referensi
// TODO(muklis): Ganti dengan rate limiter per-tenant setelah
// migrasi database selesai. Ref: JIRA-1234
Kode yang Di-comment Out Harus Dihapus
Kode yang di-comment out adalah noise. Gunakan Git untuk sejarah kode — bukan komentar.
// ❌ BAD: kode lama yang tidak dihapus
// func oldCalculate(price float64) float64 {
// return price * 0.9
// }
func calculate(price float64) float64 {
return price * getDiscount(price)
}
❌ Implementasi yang Salah
Perhatikan kode berikut — penuh komentar, tapi semuanya tidak berguna. Yang justru butuh penjelasan (aturan bisnis diskon) tidak ada komentarnya sama sekali.
// PaymentService is the payment service
type PaymentService struct {
db *sql.DB // database
}
// ProcessPayment processes a payment
func (s *PaymentService) ProcessPayment(userID string, amount float64) error {
// get user from database
user, err := s.getUser(userID)
if err != nil {
// return error if error
return err
}
// check if user is nil
if user == nil {
return errors.New("user not found")
}
// calculate discount
discount := 0.0
if user.TotalOrders > 10 {
discount = 0.15
} else if user.TotalOrders == 0 {
discount = 0.0
}
// apply discount to amount
finalAmount := amount - (amount * discount)
// charge the user
return s.charge(user, finalAmount)
}
Mengapa ini salah?
- Komentar seperti
// get user from databasedan// return error if errortidak menambah informasi apapun. - Logika diskon yang benar-benar butuh penjelasan — kenapa 10 order? kenapa 15%? — dibiarkan tanpa komentar.
// PaymentService is the payment serviceadalah komentar yang mengulang nama itu sendiri.
✅ Implementasi yang Benar
// PaymentService menangani proses transaksi pembayaran pelanggan.
type PaymentService struct {
db *sql.DB
}
// ProcessPayment memproses pembayaran untuk pengguna dengan ID tertentu.
// amount dalam satuan rupiah (IDR), sebelum diskon diterapkan.
func (s *PaymentService) ProcessPayment(userID string, amount float64) error {
user, err := s.getUser(userID)
if err != nil {
return fmt.Errorf("mengambil data pengguna: %w", err)
}
finalAmount := s.applyLoyaltyDiscount(user, amount)
return s.charge(user, finalAmount)
}
// applyLoyaltyDiscount menerapkan diskon berdasarkan program loyalitas.
// Diskon 15% diberikan setelah 10 transaksi berhasil — sesuai kebijakan
// retensi pelanggan Q3 2025. Ref: PRD-442, JIRA-1891.
// Pelanggan baru (0 transaksi) tidak mendapat diskon karena program
// ini dirancang untuk retensi, bukan akuisisi.
func (s *PaymentService) applyLoyaltyDiscount(user *User, amount float64) float64 {
if user.TotalOrders <= 10 {
return amount
}
const loyaltyDiscountRate = 0.15
return amount - (amount * loyaltyDiscountRate)
}
Mengapa ini benar?
- GoDoc pada
PaymentServicedanProcessPaymentmenjelaskan apa dan untuk siapa. - Komentar di
applyLoyaltyDiscountmenjelaskan mengapa angka 10 dan 15% dipilih — hal yang tidak bisa dipahami hanya dari kode. - Kode yang jelas dibiarkan berbicara sendiri tanpa komentar tambahan.
🏗️ Studi Kasus: Payment Processing Service
Dalam sistem pembayaran nyata, ada banyak aturan bisnis yang tidak intuitif. Komentar justru paling berharga di sini — bukan untuk menjelaskan kode, tapi untuk menjelaskan konteks bisnis di balik kode.
// ChargeWithRetry mencoba memproses pembayaran hingga maxRetries kali.
//
// Retry hanya dilakukan untuk error jaringan (network timeout, 503).
// Error pembayaran seperti kartu ditolak (4xx) TIDAK di-retry karena
// akan menghasilkan hasil yang sama. Ref: RFC payment gateway v2.3.
func (s *PaymentService) ChargeWithRetry(ctx context.Context, user *User, amount float64) error {
const maxRetries = 3
for attempt := range maxRetries {
err := s.gateway.Charge(ctx, user.PaymentMethodID, amount)
if err == nil {
return nil
}
// Hentikan retry jika error bersifat terminal (bukan masalah jaringan).
// Melanjutkan retry pada kartu yang ditolak bisa memicu fraud detection.
if isTerminalError(err) {
return err
}
backoff := time.Duration(attempt+1) * 500 * time.Millisecond
time.Sleep(backoff)
}
return ErrMaxRetriesExceeded
}
📋 Ringkasan
Prinsip komentar yang baik:
- ✅ Tulis komentar untuk menjelaskan MENGAPA, bukan APA
- ✅ Gunakan GoDoc untuk semua exported identifier (
// NamaFungsi ...)- ✅ TODO/FIXME harus menyertakan pemilik dan referensi tiket
- ✅ Kode yang di-comment out → hapus, andalkan Git
- ❌ Hindari komentar yang hanya mengulang apa yang kode sudah jelaskan
- ❌ Jangan tinggalkan komentar yang sudah tidak relevan (misleading comments)
💪 Tantangan
Buka file terakhir yang kamu kerjakan. Temukan semua komentar yang hanya mengulang kode (// get user, // return error, // loop through list). Hapus semuanya. Lalu cek — apakah ada logika atau aturan bisnis yang benar-benar perlu penjelasan dan belum ada komentarnya? Tambahkan di sana.
Komentar yang benar adalah yang tidak ada pun kamu menyesal karena membutuhkannya.
🇮🇩 Versi Indonesia | 🇬🇧 English version