Go Error Handling Packages Testing Project Structure
Definisi
Di Go, kualitas project sering kelihatan dari empat hal yang sangat dasar tetapi sangat menentukan:
error handling: bagaimana fungsi mengembalikan kegagalan secara eksplisit
packages: bagaimana kode dipisah supaya dependency antarbagian tetap sehat
testing: bagaimana perilaku diverifikasi dengan test yang mudah dibaca dan mudah di-maintain
project structure: bagaimana folder dan boundary disusun supaya project tetap tumbuh tanpa jadi berantakan
Topik ini penting karena Go sengaja tidak menyembunyikan banyak hal di balik abstraksi berat. Struktur codebase yang rapi, error yang jelas, dan test yang konsisten akan langsung terasa dampaknya di debugging harian.
Prinsip utama
Go biasanya mendorong gaya kerja yang sederhana:
fungsi mengembalikan hasil dan error
pemanggil memeriksa error segera
error yang ditambah konteks tetap bisa ditelusuri
package hanya bertanggung jawab pada domain yang jelas
test ditulis sebagai spesifikasi perilaku, bukan sekadar eksekusi kode
Kalau pola ini konsisten, codebase jauh lebih mudah dirawat daripada project yang terlalu cepat membesar tapi boundary-nya kabur.
Error return: idiom dasar di Go
Di Go, kegagalan biasanya tidak dilempar sebagai exception. Pola yang umum adalah mengembalikan error sebagai nilai kedua.
Aturan praktisnya sederhana: cek error sesegera mungkin. Jangan menunda terlalu jauh karena jarak antara sumber error dan penanganannya akan membuat debugging lebih sulit.
Jebakan umum
error diabaikan karena dianggap kecil
fungsi terlalu panjang sehingga error check menumpuk tanpa struktur
caller tidak tahu apakah error harus di-retry, di-log, atau langsung dibawa ke user
Kalau fungsi mulai terlalu ramai, pecah jadi langkah yang lebih kecil supaya error handling tetap lokal dan jelas.
Wrapping error: menambah konteks tanpa kehilangan asal masalah
Saat error naik ke layer yang lebih atas, biasanya error perlu diberi konteks baru. Di Go modern, ini dilakukan dengan wrapping.
Error dari layer bawah sering terlalu teknis untuk langsung dipakai di layer atas. Wrapping membantu dua hal sekaligus:
menambahkan konteks yang relevan untuk caller
mempertahankan sumber error asli agar masih bisa ditelusuri dengan errors.Is atau errors.As
Contoh pengecekan error yang dibungkus
go
if errors.Is(err, os.ErrNotExist) { // file memang tidak ada}var pathErr *os.PathErrorif errors.As(err, &pathErr) { // ambil detail dari tipe error asli}
Aturan praktis wrapping
tambahkan konteks di boundary antar-layer
jangan wrap berulang kali tanpa alasan
gunakan %w kalau error asli masih perlu diinspeksi
gunakan message yang menjelaskan operasi yang gagal, bukan sekadar berkata "failed"
Contoh konteks yang bagus:
load user 42
parse config file config.json
save invoice to database
Contoh yang terlalu samar:
failed
something went wrong
error happened
Kapan tidak perlu wrap
Kalau error memang sudah cukup jelas dan tidak ada konteks tambahan yang berguna, return error apa adanya masih valid. Yang penting adalah jangan kehilangan informasi yang dibutuhkan untuk diagnosis.
Flow error dari layer bawah ke layer atas
Loading diagram...
Alur ini menjaga dua hal: pesan error tetap manusiawi, tetapi jejak asal masalah tidak hilang.
Packages: boundary yang sehat
Package di Go sebaiknya diperlakukan sebagai boundary yang nyata, bukan hanya folder tempat menaruh file.
Apa yang harus dijaga
package punya tanggung jawab yang jelas
dependency mengalir ke arah yang masuk akal
package tidak saling bergantung secara silang tanpa alasan
API package cukup kecil untuk dipahami dari luar
Prinsip package boundary
ekspos seperlunya
identifier yang tidak perlu dipakai dari luar sebaiknya tetap unexported
pakai huruf kecil untuk detail internal
jangan taruh terlalu banyak peran dalam satu package
domain logic, transport, dan persistence sebaiknya tidak bercampur tanpa alasan
hindari package yang terlalu generik
nama seperti utils, helpers, atau common sering jadi tempat buangan kalau tidak dipakai dengan disiplin
jadikan package sebagai unit tanggung jawab
kalau suatu package sulit dijelaskan dalam satu kalimat, kemungkinan boundary-nya kurang jelas
Contoh struktur boundary yang wajar
internal/user untuk logika user
internal/userrepo atau internal/store untuk akses data
cmd/api untuk entrypoint HTTP
pkg/ hanya untuk API yang memang ingin dipakai project lain
Kapan memakai internal
Pakai internal kalau package hanya boleh dipakai oleh code dalam repository yang sama. Ini bagus untuk menjaga boundary agar tidak terlalu cepat menjadi publik.
test lebih mirip script manual daripada verifikasi perilaku
Susunan folder yang sehat
Project structure yang sehat bukan soal mengikuti satu format baku untuk semua kasus. Yang penting adalah boundary-nya masuk akal dan mudah dipahami saat project tumbuh.
cmd/: entrypoint aplikasi yang dijalankan sebagai binary
internal/: kode inti yang tidak ingin dibuka sebagai API publik
pkg/: library yang memang ingin dipakai lintas project, kalau benar-benar perlu
testdata/: fixture dan file pendukung test
scripts/: helper script build, deploy, atau maintenance
Cara berpikir saat menaruh code
kalau ini hanya untuk menjalankan aplikasi, taruh di cmd/
kalau ini inti bisnis aplikasi, taruh di internal/
kalau ini memang reusable dan stabil, baru pertimbangkan pkg/
kalau ini hanya data test, taruh di testdata/
Boundary yang biasanya sehat
Urutan dependency yang sering nyaman dibaca:
transport / handler
service / use case
repository / storage
external client / infrastructure
Dengan urutan ini, logic inti tidak terlalu bergantung pada detail transport atau storage.
Tanda struktur mulai tidak sehat
folder utils membesar tanpa definisi tanggung jawab
main.go terlalu tebal
package saling impor bolak-balik
detail database bocor ke layer handler
test dan code produksi bercampur tanpa batas yang jelas
Contoh cara membagi tanggung jawab
Misalnya ada fitur create user:
transport/http menerima request dan parse payload
service/user memvalidasi aturan bisnis dan orchestration
repository/user menyimpan data ke database
internal/domain memuat tipe dan rule inti kalau memang diperlukan
Pembagian seperti ini membuat error handling, testing, dan boundary package lebih mudah dirawat karena tiap layer punya tugas yang spesifik.
Catatan implementasi
Untuk error handling
return error sebagai nilai biasa, bukan kejadian luar biasa
wrap error saat naik layer agar konteks bertambah
cek error tepat setelah pemanggilan
pilih pesan error yang informatif
Untuk packages
jaga API package tetap kecil
export hanya yang memang dipakai dari luar
hindari package buangan
pertimbangkan internal untuk menahan boundary
Untuk testing
jadikan table-driven test sebagai default
pakai subtest ketika skenario mulai banyak
simpan fixture di testdata/
pastikan assertion memeriksa perilaku penting, bukan detail implementasi yang rapuh
Untuk project structure
pisahkan entrypoint, domain, dan detail infrastruktur
jangan biarkan main.go menjadi tempat semua logic
rancang folder berdasarkan tanggung jawab, bukan berdasarkan kebiasaan menumpuk file
Kesalahan umum
menganggap error handling Go itu lambat hanya karena terlihat berulang
mengabaikan wrapping sehingga root cause hilang saat debugging
membuat package yang terlalu besar dan terlalu publik
menulis test sekali jalan yang sulit ditambah case baru
memakai struktur folder yang tidak memberi boundary nyata
Referensi terkait
errors.Is dan errors.As
fmt.Errorf dengan %w
testing.T dan t.Run
internal package pattern
table-driven tests
layout cmd/ dan internal/
Kalau diimplementasikan konsisten, empat hal ini biasanya cukup untuk membuat codebase Go tetap rapi saat project bertumbuh: error jelas, package terjaga, test mudah dibaca, dan struktur folder tidak liar.