Panduan Penulisan Kode Go oleh Uber
May 27, 2025 · View on GitHub
- Pengenalan
- Panduan
- Pointer ke Interface
- Memverifikasi Kepatuhan Interface
- Receiver dan Interface
- Zero-value Mutex itu Valid
- Salin Slice dan Map dalam Batasan
- Gunakan Defer untuk membersihkan
- Ukuran Channel Sebaiknya Satu atau None
- Mulai Enum dari Satu
- Gunakan
"time"untuk Mengelola Waktu - Error
- Tangani Kegagalan Type Assertion
- Hindari Panic
- Gunakan go.uber.org/atomic
- Hindari Global yang Dapat Dimutasi
- Hindari Meng-embed Tipe di Struct Publik
- Hindari Menggunakan Nama Bawaan (Built-In Names)
- Hindari
init() - Keluar Program di Main
- Gunakan Tag Field pada Struct yang Di-marshal
- Jangan gunakan goroutine secara fire-and-forget
- Performa
- Style
- Hindari Baris yang Terlalu Panjang
- Konsisten
- Gabungkan Deklarasi yang Mirip
- Urutan Mengelompokkan Import
- Nama Package
- Nama Fungsi
- Import Aliasing
- Pengelompokan dan Pengurutan Fungsi
- Kurangi Level Nesting
- Else yang Tidak Perlu
- Deklarasi Variabel Tingkat Atas
- Awali Global yang Tidak Diekspor dengan _
- Menyematkan (Embedding) di dalam Struct
- Deklarasi Variabel Lokal
- nil adalah slice yang valid
- Mengurangi Scope Variabel
- Hindari Naked Parameters
- Gunakan Raw String Literals untuk Menghindari Escaping
- Menginisialisasi Struct
- Menginisialisasi Maps
- Format String di Luar Printf
- Naming Printf-style Functions
- Patterns
- Linting
Pengenalan
Style merujuk pada aturan penulisan kode. Meskipun namanya style, aturan ini mencakup hal-hal yang jauh lebih luas dari sekadar format file—karena formatting sudah di-handle oleh gofmt.
Panduan ini dibuat untuk mengatasi kompleksitas dengan menjelaskan secara detail hal yang boleh dan tidak boleh dilakukan (Dos and Don'ts) dalam penulisan kode Go di Uber. Aturan ini menjaga agar basis kode tetap terorganisir dan engineer bisa memanfaatkan fitur bahasa Go secara maksimal.
Panduan ini awalnya dibuat oleh Prashant Varanasi dan Simon Newton sebagai cara untuk membantu beberapa rekan kerja memahami penggunaan Go dengan cepat. Selama bertahun-tahun, panduan ini telah diperbarui berdasarkan saran dari orang lain.
Dokumen ini menjelaskan aturan idiomatik dalam kode Go yang kami ikuti di Uber. Banyak dari aturan ini adalah pedoman umum untuk Go, sementara beberapa lainnya memperluas sumber eksternal berikut:
Kami berusaha agar contoh kode akurat untuk dua versi minor Go terbaru rilisan.
Semua kode harus bebas dari error saat dijalankan dengan golint dan go vet. Kami menyarankan untuk mengatur editor Anda agar:
- Menjalankan
goimportssaat save - Menjalankan
golintdango vetuntuk memeriksa error
Anda bisa menemukan informasi tentang dukungan editor untuk Go tools di sini:
https://go.dev/wiki/IDEsAndTextEditorPlugins
Panduan
Pointer ke Interface
Anda hampir tidak pernah perlu menggunakan pointer ke interface. Sebaiknya Anda mengirim interface sebagai value, karena data yang mendasarinya tetap bisa berupa pointer.
Interface terdiri dari dua bagian:
- Sebuah pointer ke informasi yang spesifik ke tipe. Anda bisa menganggap ini sebagai "type."
- Pointer data. Jika data yang disimpan adalah pointer, data tersebut disimpan langsung. Jika data yang disimpan adalah value, maka pointer ke value tersebut yang disimpan.
Jika Anda ingin metode pada interface mengubah data yang mendasarinya, Anda harus menggunakan pointer.
Memverifikasi Kepatuhan Interface
Verifikasi kepatuhan interface saat waktu kompilasi jika memungkinkan. Ini meliputi:
- Tipe yang diekspor yang harus mengimplementasikan interface tertentu sebagai bagian dari kontrak API mereka
- Tipe yang diekspor atau tidak diekspor yang termasuk dalam kumpulan tipe yang mengimplementasikan interface yang sama
- Kasus lain di mana pelanggaran terhadap interface dapat merusak pengguna
| Buruk | Bagus |
|---|---|
|
|
Pernyataan var _ http.Handler = (*Handler)(nil) akan gagal dikompilasi jika *Handler tidak lagi sesuai dengan interface http.Handler.
Bagian kanan dari assignment harus berupa nilai nol dari tipe yang di-assert. Ini adalah nil untuk tipe pointer (seperti *Handler), slice, dan map, serta struct kosong untuk tipe struct.
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
Receiver dan Interface
Metode dengan value receiver bisa dipanggil pada pointer maupun value.
Metode dengan pointer receiver hanya bisa dipanggil pada pointer atau nilai yang dapat memiliki alamat (addressable values).
Contohnya,
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
// Kita tidak bisa mendapatkan pointer ke value yang disimpan dalam map,
// karena value tersebut bukan nilai yang dapat memiliki alamat (addressable values).
sVals := map[int]S{1: {"A"}}
// Kita bisa memanggil Read pada nilai yang disimpan dalam map
// karena Read memiliki *value receiver*, yang tidak mengharuskan
// nilai tersebut memiliki alamat.
sVals[1].Read()
// Kita tidak bisa memanggil Write pada nilai yang disimpan dalam map
// karena Write memiliki pointer receiver, dan tidak mungkin mendapatkan pointer ke
// nilai yang disimpan dalam map.
//
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// Anda bisa memanggil Read dan Write jika map menyimpan pointer,
// karena pointer secara inheren dapat diacu alamatnya.
sPtrs[1].Read()
sPtrs[1].Write("test")
Demikian juga, sebuah interface dapat dipenuhi oleh pointer, meskipun metode yang dimiliki menggunakan value receiver.
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}
var i F
i = s1Val
i = s1Ptr
i = s2Ptr
// Berikut ini tidak dapat dikompilasi, karena s2Val adalah sebuah value, dan tidak ada value receiver untuk f.
// i = s2Val
Effective Go memiliki penjelasan yang baik tentang Pointers vs. Values.
Zero-value Mutex itu Valid
Zero-value dari sync.Mutex dan sync.RWMutex itu valid, jadi Anda hampir tidak pernah membutuhkan pointer ke mutex.
| Buruk | Bagus |
|---|---|
|
|
Jika Anda menggunakan sebuah struct melalui pointer, maka mutex sebaiknya menjadi field non-pointer di dalamnya. Jangan menyematkan (embed) mutex ke dalam struct, meskipun struct tersebut tidak diekspor.
| Buruk | Bagus |
|---|---|
|
|
|
Field |
Mutex dan metode-metodenya adalah detail implementasi dari |
Salin Slice dan Map dalam Batasan
Slice dan map berisi pointer ke data dasarnya, jadi berhati-hatilah dalam situasi ketika data tersebut perlu disalin.
Menerima Slice dan Map
Perlu diingat bahwa pengguna dapat memodifikasi map atau slice yang Anda terima sebagai argumen jika Anda menyimpan referensinya.
| Bad | Good |
|---|---|
|
|
Mengembalikan Slice dan Map
Demikian juga, berhati-hatilah terhadap modifikasi pengguna pada map atau slice yang dapat mengekspos status internal.
| Buruk | Bagus |
|---|---|
|
|
Gunakan defer untuk Membersihkan
Gunakan defer untuk membersihkan sumber daya seperti file dan kunci (locks).
| Buruk | Bagus |
|---|---|
|
|
Defer memiliki overhead yang sangat kecil dan sebaiknya dihindari hanya jika Anda bisa membuktikan bahwa waktu eksekusi fungsi Anda dalam orde nanodetik. Keuntungan dari segi keterbacaan saat menggunakan defer lebih berharga daripada biaya kecil yang timbul dari penggunaannya. Ini terutama berlaku untuk metode yang lebih besar dengan operasi yang lebih kompleks daripada sekadar akses memori, di mana komputasi lain lebih signifikan dibandingkan defer.
Ukuran Channel Sebaiknya Satu atau None
Channel biasanya sebaiknya berukuran satu atau tidak berbuffer. Secara default, channel tidak berbuffer dan berukuran nol. Ukuran lain harus diperiksa dengan sangat teliti. Pertimbangkan bagaimana ukuran tersebut ditentukan, apa yang mencegah channel penuh saat beban tinggi dan memblokir penulis, serta apa yang terjadi ketika hal ini terjadi.
| Buruk | Bagus |
|---|---|
|
|
Mulai Enum dari Satu
Cara standar membuat enumerasi di Go adalah dengan mendeklarasikan tipe kustom dan grup const menggunakan iota. Karena variabel memiliki nilai default 0, biasanya enum sebaiknya dimulai dari nilai selain nol.
| Buruk | Bagus |
|---|---|
|
|
Ada kasus di mana menggunakan nilai nol masuk akal, misalnya ketika nilai nol merupakan perilaku bawaan yang diinginkan.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Gunakan "time" untuk Mengelola Waktu
Waktu itu rumit. Kesalahan asumsi yang sering dibuat tentang waktu meliputi:
- Sehari itu 24 jam
- Satu jam itu 60 menit
- Seminggu itu 7 hari
- Setahun itu 365 hari
- Dan masih banyak lagi
Sebagai contoh, 1 berarti menambahkan 24 jam ke suatu titik waktu tidak selalu menghasilkan hari kalender baru.
Oleh karena itu, selalu gunakan paket "time" saat menangani waktu karena paket ini membantu mengatasi asumsi-asumsi yang salah tersebut dengan cara yang lebih aman dan akurat.
Gunakan time.Time untuk Titik Waktu
Gunakan time.Time saat menangani titik waktu, serta metode pada time.Time saat membandingkan, menambah, atau mengurangi waktu.
| Buruk | Bagus |
|---|---|
|
|
Gunakan time.Duration untuk Periode Waktu
Gunakan time.Duration saat menangani periode waktu.
| Buruk | Bagus |
|---|---|
|
|
Kembali ke contoh menambahkan 24 jam ke sebuah titik waktu, metode yang digunakan untuk menambah waktu tergantung pada maksudnya. Jika kita menginginkan waktu yang sama pada hari berikutnya di kalender, kita harus menggunakan Time.AddDate. Namun, jika kita menginginkan titik waktu yang pasti 24 jam setelah waktu sebelumnya, kita harus menggunakan Time.Add.
newDay := t.AddDate(0 /* tahun */, 0 /* bulan */, 1 /* hari */)
maybeNewDay := t.Add(24 * time.Hour)
Gunakan time.Time dan time.Duration dengan Sistem Eksternal
Gunakan time.Duration dan time.Time saat berinteraksi dengan sistem eksternal bila memungkinkan. Contohnya:
- Flag baris perintah:
flagmendukungtime.Durationlewattime.ParseDuration - JSON:
encoding/jsonmendukung encodingtime.Timesebagai string RFC 3339 lewat metodeUnmarshalJSON - SQL:
database/sqlmendukung konversi kolomDATETIMEatauTIMESTAMPmenjaditime.Timedan sebaliknya jika driver yang digunakan mendukung - YAML:
gopkg.in/yaml.v2mendukungtime.Timesebagai string RFC 3339, dantime.Durationlewattime.ParseDuration
Jika tidak memungkinkan menggunakan time.Duration dalam interaksi ini, gunakan tipe int atau float64 dan sertakan satuan waktu pada nama field.
Contohnya, karena encoding/json tidak mendukung time.Duration, satuan waktu disertakan di nama field.
| Buruk | Bagus |
|---|---|
|
|
Jika tidak memungkinkan menggunakan time.Time dalam interaksi ini, kecuali ada alternatif yang disepakati, gunakan string dan format timestamp sesuai dengan RFC 3339. Format ini digunakan secara default oleh Time.UnmarshalText dan tersedia untuk digunakan di Time.Format dan time.Parse lewat time.RFC3339.
Meskipun ini biasanya bukan masalah dalam praktik, perlu diingat bahwa paket "time" tidak mendukung parsing timestamp dengan detik kabisat (8728), dan juga tidak memperhitungkan detik kabisat dalam perhitungan (15190). Jika Anda membandingkan dua titik waktu, perbedaan waktu tersebut tidak akan memasukkan detik kabisat yang mungkin terjadi di antara kedua titik waktu tersebut.
Error
Tipe Error
Ada beberapa opsi untuk mendeklarasikan error. Pertimbangkan hal berikut sebelum memilih opsi yang paling sesuai dengan kasus penggunaan Anda.
- Apakah pemanggil perlu mencocokkan error agar dapat menanganinya?
Jika iya, kita harus mendukung fungsierrors.Isatauerrors.Asdengan mendeklarasikan variabel error tingkat atas atau tipe khusus. - Apakah pesan error berupa string statis,
atau string dinamis yang membutuhkan informasi konteks?
Untuk yang pertama, kita bisa menggunakanerrors.New, tapi untuk yang kedua harus menggunakanfmt.Errorfatau tipe error khusus. - Apakah kita meneruskan error baru yang dikembalikan oleh fungsi downstream?
Jika iya, lihat bagian tentang pembungkusan error.
| Error Cocok? | Pesan Error | Panduan |
|---|---|---|
| Tidak | statis | errors.New |
| Tidak | dinamis | fmt.Errorf |
| Ya | statis | variabel var tingkat atas dengan errors.New |
| Ya | dinamis | tipe error khusus |
Contohnya, gunakan errors.New untuk error dengan pesan string statis.
Export error ini sebagai variabel agar bisa dicocokkan dengan errors.Is jika pemanggil perlu mencocokkan dan menangani error tersebut.
| Tidak ada error cocok | Error cocok |
|---|---|
|
|
Untuk error dengan string dinamis, gunakan fmt.Errorf jika pemanggil tidak perlu mencocokkannya, dan gunakan tipe error kustom jika pemanggil perlu mencocokkannya.
| Tidak ada error cocok | Error cocok |
|---|---|
|
|
Perlu diperhatikan bahwa jika Anda mengekspor variabel atau tipe error dari sebuah package, maka hal tersebut akan menjadi bagian dari API publik package tersebut.
Pembungkusan Error
Ada tiga opsi utama untuk meneruskan error jika suatu pemanggilan gagal:
- mengembalikan error asli apa adanya
- menambahkan konteks menggunakan
fmt.Errorfdengan verb%w - menambahkan konteks menggunakan
fmt.Errorfdengan verb%v
Kembalikan error asli jika tidak ada konteks tambahan yang perlu ditambahkan. Ini mempertahankan tipe dan pesan error asli. Cocok untuk kasus di mana pesan error bawaan sudah cukup untuk melacak sumber masalahnya.
Jika tidak, tambahkan konteks pada pesan error agar pesan seperti "connection refused" menjadi lebih informatif seperti "memanggil layanan foo: connection refused".
Gunakan fmt.Errorf untuk menambahkan konteks pada error, pilih antara verb %w atau %v tergantung apakah pemanggil perlu mencocokkan dan mengekstrak penyebab dasarnya.
- Gunakan
%wjika pemanggil perlu mengakses error yang dibungkus. Ini adalah pilihan default yang baik untuk sebagian besar kasus wrapping error. Namun, hati-hati karena pemanggil bisa mulai mengandalkan perilaku ini. Jika error yang dibungkus merupakanvaratau tipe yang dikenal, dokumentasikan dan uji sebagai bagian dari kontrak fungsi. - Gunakan
%vuntuk menyembunyikan error yang dibungkus. Pemanggil tidak dapat mencocokkannya, tetapi Anda bisa menggantinya dengan%wdi masa depan jika dibutuhkan.
Saat menambahkan konteks ke error yang dikembalikan, jaga agar konteks tetap ringkas dengan menghindari frasa seperti "failed to", yang hanya menyatakan hal yang sudah jelas dan dapat menumpuk saat error merambat ke atas dalam stack:
| Buruk | Bagus |
|---|---|
|
|
|
|
Namun, begitu error dikirim ke sistem lain, pesan tersebut harus jelas merupakan sebuah error, misalnya dengan menggunakan tag err atau awalan seperti "Failed" dalam log.
Lihat juga Don't just check errors, handle them gracefully.
Penamaan Error
Untuk nilai error yang disimpan sebagai variabel global, gunakan awalan Err jika diekspor, atau err jika tidak diekspor.
Panduan ini menggantikan aturan Prefix Unexported Globals with _.
var (
// Dua error berikut diekspor
// agar pengguna paket ini bisa mencocokkannya
// dengan errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// Error ini tidak diekspor karena
// kita tidak ingin menjadikannya bagian dari API publik kita.
// Namun, kita tetap bisa menggunakannya di dalam paket
// dengan errors.Is.
errNotFound = errors.New("not found")
)
Untuk tipe error kustom, gunakan akhiran Error.
// Demikian pula, error ini diekspor
// agar pengguna paket ini dapat mencocokkannya
// dengan errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// Dan error ini tidak diekspor karena
// kami tidak ingin menjadikannya bagian dari API publik.
// Namun, error ini tetap bisa digunakan di dalam paket
// dengan errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
Handle Errors Once
Tangani Error Sekali Saja
Saat pemanggil menerima error dari fungsi yang dipanggil,
pemanggil dapat menanganinya dengan berbagai cara
tergantung apa yang diketahui tentang error tersebut.
Cara-cara tersebut antara lain, tapi tidak terbatas pada:
- jika kontrak fungsi yang dipanggil mendefinisikan error spesifik, cocokkan error menggunakan
errors.Isatauerrors.Asdan tangani tiap kasus secara berbeda - jika error dapat dipulihkan, catat error tersebut dan lakukan penurunan layanan secara halus
- jika error mewakili kondisi kegagalan spesifik domain,kembalikan error yang sudah terdefinisi dengan baik
- kembalikan error tersebut, baik dalam bentuk error yang dibungkus atau secara langsung
Terlepas dari bagaimana pemanggil menangani error, biasanya error tersebut hanya perlu ditangani sekali saja. Misalnya, pemanggil sebaiknya tidak mencatat log error lalu mengembalikannya, karena pemanggil dari fungsi ini juga mungkin akan menangani error tersebut kembali.
Misalnya, perhatikan kasus-kasus berikut:
| Deskripsi | Kode |
|---|---|
|
Buruk: Mencatat (log) error dan mengembalikannya Pemanggil (caller) di tingkat yang lebih atas kemungkinan juga akan melakukan tindakan serupa terhadap error tersebut. Melakukan hal ini menyebabkan banyak kebisingan (noise) pada log aplikasi tanpa memberikan nilai tambah yang berarti. |
|
|
Baik: Bungkus (wrap) error dan kembalikan Pemanggil (caller) di tingkat yang lebih atas akan menangani error tersebut. |
|
|
Baik: Log error dan turunkan kualitas layanan dengan baik Jika operasi tersebut tidak mutlak diperlukan, kita dapat memberikan pengalaman yang tetap berfungsi meski kualitasnya menurun dengan melakukan recovery dari error tersebut. |
|
|
Baik: Cocokkan error dan tangani dengan degradasi Jika pemanggil (callee) mendefinisikan error spesifik dalam kontraknyadan kegagalan tersebut bisa dipulihkan, cocokkan error tersebut dan tangani dengan degradasi yang baik. Untuk kasus lain, bungkus error dan kembalikan. Pemanggil (caller) di lapisan atas akan menangani error lainnya. |
|
Tangani Kegagalan Type Assertion
Bentuk pengembalian satu nilai dari type assertion akan menyebabkan panic jika tipe tidak sesuai. Oleh karena itu, selalu gunakan idiom "comma ok".
| Buruk | Bagus |
|---|---|
|
|
Hindari Panic
Kode yang berjalan di environment production harus menghindari panic. Panic merupakan sumber utama dari kegagalan berantai (cascading failures). Jika terjadi error, fungsi harus mengembalikan error tersebut dan membiarkan pemanggilnya yang memutuskan cara penanganannya.
| Buruk | Bagus |
|---|---|
|
|
Panic/recover bukanlah strategi penanganan error. Program harus melakukan panic hanya ketika terjadi hal yang tidak dapat dipulihkan, seperti dereferensi nil. Pengecualian dari ini adalah saat inisialisasi program: masalah serius pada saat startup program yang harus menghentikan program dapat menyebabkan panic.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Bahkan dalam test, sebaiknya gunakan t.Fatal atau t.FailNow daripada panic agar test tersebut ditandai sebagai gagal.
| Buruk | Bagus |
|---|---|
|
|
Gunakan go.uber.org/atomic
Operasi atomik dengan paket sync/atomic bekerja pada tipe dasar (int32, int64, dll.), sehingga mudah untuk lupa menggunakan operasi atomik saat membaca atau memodifikasi variabel.
Paket go.uber.org/atomic menambahkan keamanan tipe pada operasi ini dengan menyembunyikan tipe dasar tersebut. Selain itu, paket ini menyediakan tipe atomic.Bool yang praktis.
| Buruk | Bagus |
|---|---|
|
|
Hindari Global yang Dapat Dimutasi
Hindari memodifikasi variabel global secara langsung, sebaiknya gunakan dependency injection. Ini berlaku untuk pointer fungsi maupun jenis nilai lainnya.
| Buruk | Bagus |
|---|---|
|
|
|
|
Hindari Meng-embed Tipe di Struct Publik
Tipe embedded ini membocorkan detail implementasi, menghambat evolusi tipe, dan membuat dokumentasi menjadi tidak jelas.
Misalnya, jika Anda mengimplementasikan berbagai tipe list menggunakan AbstractList yang
bersama, hindari meng-embed AbstractList dalam implementasi list konkret Anda.
Sebagai gantinya, tulis secara manual hanya metode pada list konkret Anda yang akan mendelegasikan
ke AbstractList.
type AbstractList struct {}
// Add menambahkan sebuah entitas ke dalam list.
func (l *AbstractList) Add(e Entity) {
// ...
}
// Remove menghapus sebuah entitas dari list.
func (l *AbstractList) Remove(e Entity) {
// ...
}
| Buruk | Bagus |
|---|---|
|
|
Go memungkinkan penyisipan tipe (type embedding) sebagai kompromi antara pewarisan (inheritance) dan komposisi. Tipe luar (outer type) mendapatkan salinan implisit dari metode-metode embedded type. Metode-metode ini, secara default, mendelegasikan pemanggilannya ke metode yang sama pada instance yang di-embed.
Struct tersebut juga mendapatkan sebuah field dengan nama yang sama seperti tipe yang di-embed. Jadi, jika tipe yang di-embed bersifat publik, maka field tersebut juga bersifat publik. Untuk menjaga kompatibilitas ke belakang, setiap versi berikutnya dari tipe luar harus mempertahankan tipe yang di-embed tersebut.
Tipe yang di-embed jarang diperlukan. Ini adalah kemudahan yang membantu menghindari penulisan metode delegasi yang membosankan.
Bahkan menyisipkan interface AbstractList yang kompatibel, alih-alih struct, akan memberikan fleksibilitas lebih kepada pengembang untuk mengubahnya di masa depan, namun tetap membocorkan detail bahwa list konkrit menggunakan implementasi abstrak.
| Buruk | Bagus |
|---|---|
|
|
Baik dengan struct yang di-embed maupun interface yang di-embed, tipe yang di-embed tersebut membatasi evolusi tipe tersebut.
- Menambahkan metode ke interface yang di-embed adalah perubahan yang memutus kompatibilitas (breaking change).
- Menghapus metode dari struct yang di-embed adalah perubahan yang memutus kompatibilitas.
- Menghapus tipe yang di-embed adalah perubahan yang memutus kompatibilitas.
- Mengganti tipe yang di-embed, bahkan dengan alternatif yang memenuhi interface yang sama, adalah perubahan yang memutus kompatibilitas.
Meskipun menulis metode delegasi ini melelahkan, usaha tambahan tersebut menyembunyikan detail implementasi, memberi lebih banyak peluang untuk perubahan di masa depan, dan juga menghilangkan indirection dalam menemukan interface List secara lengkap dalam dokumentasi.
Hindari Menggunakan Nama Bawaan (Built-In Names)
Spesifikasi bahasa Go language specification menguraikan beberapa built-in identifiers yang sudah dideklarasikan sebelumnya dan sebaiknya tidak digunakan sebagai nama dalam program Go.
Tergantung pada konteks, penggunaan ulang identifier ini sebagai nama akan mengaburkan (shadow) identifier asli dalam ruang lingkup leksikal saat ini (dan ruang lingkup bersarang), atau membuat kode yang terdampak menjadi membingungkan.
Dalam kasus terbaik, kompiler akan memberikan peringatan; dalam kasus terburuk, kode tersebut bisa memperkenalkan bug laten yang sulit ditemukan dengan pencarian biasa.
| Buruk | Bagus |
|---|---|
|
|
|
|
Perlu diperhatikan bahwa compiler tidak akan menghasilkan error ketika menggunakan identifier yang sudah dideklarasikan sebelumnya (predeclared identifiers), tetapi alat bantu seperti go vet seharusnya dapat menunjuk dengan tepat kasus-kasus shadowing ini dan kasus lainnya.
Hindari init()
Hindari penggunaan init() jika memungkinkan. Jika init() tidak bisa dihindari atau memang diperlukan, kode harus berusaha untuk:
- Bersifat sepenuhnya deterministik, tidak tergantung pada environment program atau cara pemanggilan.
- Menghindari ketergantungan pada urutan atau efek samping dari fungsi
init()lain.
Meskipun urutaninit()sudah diketahui, kode bisa berubah, sehingga hubungan antar fungsiinit()bisa membuat kode rentan dan mudah error. - Menghindari akses atau manipulasi global state atau environment satet, seperti informasi mesin, variabel environment, direktori kerja, argumen/masukan program, dll.
- Menghindari I/O, termasuk sistem file, jaringan, dan panggilan sistem.
Kode yang tidak bisa memenuhi persyaratan ini kemungkinan besar lebih tepat dijadikan helper yang dipanggil di dalam main() (atau di bagian lain siklus hidup program), atau ditulis langsung di dalam main().
Terutama, library yang ditujukan untuk digunakan oleh program lain harus berhati-hati agar benar-benar deterministik dan tidak melakukan "init magic".
| Buruk | Bagus |
|---|---|
|
|
|
|
Dengan mempertimbangkan hal-hal di atas, ada beberapa situasi di mana penggunaan init() mungkin lebih disukai atau memang diperlukan, seperti:
- Ekspresi kompleks yang tidak dapat direpresentasikan sebagai assignment tunggal.
- Hook yang dapat dipasang secara plug-and-play, seperti dialek
database/sql, registries tipe encoding, dan sejenisnya. - Optimalisasi untuk Google Cloud Functions dan bentuk prekomputasi deterministik lainnya.
Keluar Program di Main
Program Go menggunakan os.Exit atau log.Fatal* untuk keluar secara langsung. (Menggunakan panic bukanlah cara yang baik untuk keluar dari program, hindari panic.)
Panggil salah satu dari os.Exit atau log.Fatal* hanya di dalam main(). Semua fungsi lainnya harus mengembalikan error untuk memberi sinyal kegagalan.
| Buruk | Bagus |
|---|---|
|
|
Alasan: Program yang memiliki beberapa fungsi yang keluar (exit) langsung memiliki beberapa masalah:
- Alur kontrol yang tidak jelas: Karena setiap fungsi bisa langsung keluar dari program, maka akan sulit untuk memahami alur kontrol secara keseluruhan.
- Sulit untuk diuji: Fungsi yang keluar dari program juga akan menghentikan proses test yang memanggilnya. Ini membuat fungsi sulit untuk diuji dan bisa menyebabkan test lain yang belum sempat dijalankan oleh
go testterlewat. - Proses cleanup terabaikan: Saat sebuah fungsi keluar dari program, semua pemanggilan fungsi yang dijadwalkan dengan
defertidak akan dijalankan. Ini meningkatkan risiko proses cleanup penting tidak dijalankan.
Exit Sekali
Jika memungkinkan, sebaiknya panggilan ke os.Exit atau log.Fatal hanya dilakukan sekali di dalam main() Anda. Jika ada beberapa skenario kesalahan yang menyebabkan program berhenti, tempatkan logika tersebut di dalam fungsi terpisah dan kembalikan error dari sana.
Hal ini akan membuat fungsi main() Anda lebih ringkas dan memindahkan seluruh logika bisnis utama ke dalam fungsi terpisah yang bisa diuji.
| Buruk | Bagus |
|---|---|
|
|
Contoh di atas menggunakan log.Fatal, tetapi pedoman ini juga berlaku untuk
os.Exit atau kode library yang memanggil os.Exit.
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Anda dapat mengubah signature dari run() sesuai kebutuhanmu.
Misalnya, jika programmu harus keluar dengan kode exit spesifik untuk kesalahan,
run() mungkin mengembalikan kode exit daripada error.
Hal ini memungkinkan unit tests untuk memverifikasi perilaku ini secara langsung.
func main() {
os.Exit(run(args))
}
func run() (exitCode int) {
// ...
}
Secara umum, perlu dicatat bahwa fungsi run() yang digunakan dalam contoh-contoh ini
tidak dimaksudkan untuk menjadi aturan baku. Ada fleksibilitas dalam penamaan, tanda tangan (signature), dan pengaturan fungsi run().
Beberapa hal yang dapat Anda lakukan antara lain:
- menerima argumen command line yang belum diparsing (misalnya,
run(os.Args[1:])) - memparsing argumen command line di
main()dan meneruskannya kerun - menggunakan tipe error khusus untuk membawa exit code kembali ke
main() - menempatkan logika bisnis pada lapisan abstraksi yang berbeda dari
package main
Panduan ini hanya mengharuskan agar ada satu tempat saja di main() yang bertanggung jawab
untuk benar-benar menghentikan (exit) proses.
Gunakan Tag Field pada Struct yang Di-marshal
Setiap field struct yang di-marshal ke dalam JSON, YAML, atau format lain yang mendukung penamaan field berbasis tag harus diberi anotasi dengan tag yang relevan.
| Buruk | Bagus |
|---|---|
|
|
Alasan:
Bentuk ter-serialisasi dari struktur adalah kontrak antara sistem yang berbeda.
Perubahan pada struktur bentuk ter-serialisasi—termasuk nama field—akan memecahkan kontrak ini.
Menentukan nama field di dalam tag membuat kontrak menjadi eksplisit,
dan melindungi dari kesalahan yang tidak disengaja dalam memecahkan kontrak akibat refaktorisasi atau penggantian nama field.
Jangan gunakan goroutine secara fire-and-forget
Goroutine itu ringan, tapi tidak gratis: paling tidak, mereka membutuhkan memori untuk stack-nya dan CPU untuk penjadwalan. Meskipun biaya ini kecil untuk penggunaan goroutine yang biasa,
biaya tersebut bisa menyebabkan masalah performa signifikan jika goroutine dibuat dalam jumlah besar tanpa pengelolaan waktu hidup yang baik. Goroutine dengan waktu hidup yang tidak terkelola juga bisa menimbulkan masalah lain, seperti mencegah objek yang tidak terpakai dikumpulkan oleh garbage collector dan menahan sumber daya yang seharusnya sudah tidak digunakan lagi.
Jangan membiarkan goroutine lepas di kode production. Gunakan go.uber.org/goleak untuk memeriksa goroutine leaks di dalam package yang mungkin membangkitkan goroutine.
Secara umum, setiap goroutine harus:
- harus memiliki waktu hidup yang predikatibel; atau
- harus ada cara untuk memberikan sinyal ke goroutine bahwa ia harus berhenti
Dalam kedua kasus ini, harus ada cara untuk memblokir dan menunggu goroutine untuk selesai.
Contoh:
| Buruk | Bagus |
|---|---|
|
|
|
Tidak ada cara untuk menghentikan goroutine ini. Goroutine ini akan berjalan sampai aplikasi selesai. |
Goroutine ini dapat dihentikan dengan |
Menunggu Goroutine untuk Selesai
Diberikan sebuah goroutine yang dibuat oleh sistem, harus ada cara untuk menunggu goroutine untuk selesai. Ada dua cara populer untuk melakukan ini:
-
Menggunakan
sync.WaitGroup. Gunakan ini jika ada beberapa goroutine yang harus ditunggu.var wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() // ... }() } // Menunggu semua selesai wg.Wait() -
Menambahkan
chan struct{}lainnya yang goroutine tutup ketika selesai. Gunakan ini jika hanya ada satu goroutine.done := make(chan struct{}) go func() { defer close(done) // ... }() // Menunggu selesai <-done
Hindari goroutine di init()
Fungsi init() tidak boleh membuat goroutine.
Lihat juga Hindari init().
Jika package membutuhkan goroutine background, maka harus mengekspos objek yang bertanggung jawab untuk mengelola waktu hidup goroutine.
Objek tersebut harus menyediakan metode (Close, Stop, Shutdown, dll)
yang memberikan sinyal ke goroutine background untuk berhenti, dan menunggu hingga goroutine tersebut selesai.
| Buruk | Bagus |
|---|---|
|
|
|
Membuat goroutine background tanpa kondisi apapun ketika user mengekspor package ini. User tidak memiliki kontrol atas goroutine atau metode untuk menghentikannya. |
Membuat worker hanya jika user meminta. Memberikan metode untuk menutup worker agar user dapat membersihkan sumber daya yang digunakan oleh worker. Catatan: Jika worker mengelola beberapa goroutine, sebaiknya gunakan |
Performa
Pedoman performa berlaku hanya pada hot path.
Gunakan strconv dibandingkan fmt
Ketika mengkonversi primitif ke/dari string, strconv lebih cepat dari fmt.
| Buruk | Bagus |
|---|---|
|
|
|
|
Hindari konversi string ke byte yang berulang
Jangan membuat byte slices dari string yang tetap berulang. Sebaliknya, lakukan konversi sekali dan simpan hasilnya.
| Buruk | Bagus |
|---|---|
|
|
|
|
Selalu Tentukan Kapasitas Container
Tentukan kapasitas container di awal untuk mengalokasikan memori untuk container. Ini meminimalkan alokasi berikutnya (melalui salinan dan penyesuaian ukuran container) saat elemen ditambahkan.
Spesifikasikan Kapasitas Map
Jika memungkinkan, berikan hint kapasitas ketika membuat map dengan make().
make(map[T1]T2, hint)
Memberikan hint kapasitas ketika membuat map dengan make() mencoba untuk mengatur ukuran map pada saat inisialisasi, yang mengurangi kebutuhan untuk membesarkan map dan alokasi saat elemen ditambahkan ke map.
Perlu dicatat bahwa, tidak seperti slice, petunjuk kapasitas pada map tidak menjamin alokasi penuh dan sebelumnya, melainkan digunakan untuk memperkirakan jumlah bucket hashmap yang diperlukan. Oleh karena itu, alokasi masih dapat terjadi saat menambahkan elemen ke map, bahkan hingga mencapai kapasitas yang ditentukan.
| Buruk | Bagus |
|---|---|
|
|
|
|
|
Spesifikasikan Kapasitas Slice
Jika memungkinkan, berikan petunjuk kapasitas saat menginisialisasi slice dengan make(), terutama saat menggunakan append.
make([]T, length, capacity)
Tidak seperti map, kapasitas slice bukanlah sebuah petunjuk: compiler akan mengalokasikan memori yang cukup untuk kapasitas slice sesuai yang diberikan pada make(). Ini berarti operasi append() selanjutnya tidak akan menimbulkan alokasi tambahan (sampai panjang slice mencapai kapasitasnya, setelah itu setiap penambahan akan memerlukan resize untuk menampung elemen tambahan).
| Buruk | Bagus |
|---|---|
|
|
|
|
Style
Hindari Baris yang Terlalu Panjang
Hindari baris kode yang mengharuskan pembaca scroll horizontal atau memiringkan kepala terlalu banyak.
Kami merekomendasikan batas panjang baris lunak sebesar 99 karakter. Penulis sebaiknya membungkus baris sebelum mencapai batas ini, namun ini bukan batas aturan. Kode diperbolehkan melebihi batas ini.
Konsisten
Beberapa pedoman yang dijelaskan dalam dokumen ini dapat dievaluasi secara objektif; sedangkan yang lain bersifat situasional, kontekstual, atau subjektif.
Yang terpenting, konsisten.
Kode yang konsisten lebih mudah untuk dipelihara, lebih mudah dipahami, membutuhkan beban kognitif yang lebih rendah, dan lebih mudah untuk migrasi atau pembaruan seiring munculnya konvensi baru atau perbaikan kelas bug.
Sebaliknya, memiliki beberapa gaya yang berbeda atau bertentangan dalam satu basis kode menyebabkan beban pemeliharaan, ketidakpastian, dan disonansi kognitif, yang semuanya dapat langsung berkontribusi pada penurunan kecepatan pengembangan, mengurangi kecepatan dalam mengevaluasi kode, dan munculnya bug.
Saat menerapkan pedoman ini ke sebuah basis kode, disarankan agar perubahan dilakukan pada tingkat package (atau lebih besar): penerapan pada tingkat sub-package melanggar kekhawatiran di atas karena memperkenalkan banyak gaya ke dalam kode yang sama.
Gabungkan Deklarasi yang Mirip
Go mendukung pengelompokan deklarasi yang mirip.
| Buruk | Bagus |
|---|---|
|
|
Pengelompokan ini juga berlaku untuk konstanta, variabel, dan deklarasi tipe.
| Buruk | Bagus |
|---|---|
|
|
Hanya kelompokkan deklarasi yang berkaitan. Jangan kelompokkan deklarasi yang tidak berkaitan.
| Buruk | Bagus |
|---|---|
|
|
Kelompok tidak terbatas di mana mereka dapat digunakan. Misalnya, Anda bisa menggunakan mereka didalam fungsi.
| Buruk | Bagus |
|---|---|
|
|
Pengecualian: Deklarasi variabel, terutama di dalam fungsi, sebaiknya dikelompokkan bersama jika dideklarasikan berdampingan dengan variabel lain. Lakukan ini untuk variabel yang dideklarasikan bersama meskipun tidak saling terkait.
| Buruk | Bagus |
|---|---|
|
|
Urutan Mengelompokkan Import
Ada dua kelompok import:
- Standard library
- Semuanya
Ini adalah kelompok yang diterapkan oleh goimports secara default.
| Buruk | Bagus |
|---|---|
|
|
Nama Package
Saat menamai package, pilih nama yang:
- Semua huruf kecil. Tidak boleh menggunakan huruf kapital atau underscore.
- Tidak perlu diganti namanya menggunakan named imports di sebagian besar pemanggilan.
- Singkat dan padat. Ingat bahwa nama ini akan disebutkan secara penuh di setiap pemanggilan.
- Tidak dalam bentuk jamak. Contohnya, gunakan
net/url, bukannet/urls. - Bukan "common", "util", "shared", atau "lib". Ini adalah nama yang buruk dan kurang informatif.
Lihat juga Package Names dan Style guideline for Go packages.
Nama Fungsi
Kami mengikuti aturan komunitas Go yang menggunakan MixedCaps untuk penamaan fungsi. Pengecualian dibuat untuk fungsi test, yang boleh mengandung underscore untuk tujuan mengelompokkan kasus uji terkait, misalnya TestMyFunction_WhatIsBeingTested.
Import Aliasing
Penggunaan alias pada import harus dilakukan jika nama package tidak sama dengan elemen terakhir dari path import.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
Penggunaan alias pada import harus dihindari kecuali ada konflik antara import.
| Buruk | Bagus |
|---|---|
|
|
Pengelompokan dan Pengurutan Fungsi
- Fungsi harus disortir dalam urutan panggilan yang sekitar.
- Fungsi dalam file harus dikelompokkan berdasarkan receiver.
Oleh karena itu, fungsi yang diexport harus muncul pertama dalam file, setelah
struct, const, var definitions.
Sebuah fungsi newXYZ()/NewXYZ() bisa muncul setelah tipe didefinisikan, namun sebelum metode-metode lainnya pada receiver tersebut.
Fungsi-fungsi utilitas biasanya muncul di akhir file.
| Buruk | Bagus |
|---|---|
|
|
Kurangi Level Nesting
Kode sebaiknya mengurangi tingkat nesting bila memungkinkan dengan menangani kasus error atau kondisi khusus terlebih dahulu dan melakukan return lebih awal atau melanjutkan loop. Kurangi jumlah kode yang memiliki nesting berlapis-lapis.
| Buruk | Bagus |
|---|---|
|
|
Else yang Tidak Perlu
Jika sebuah variabel diset di kedua cabang dari sebuah if, maka hal tersebut dapat digantikan dengan satu if saja.
| Buruk | Bagus |
|---|---|
|
|
Deklarasi Variabel Tingkat Atas
Di tingkat atas, gunakan kata kunci standar var. Jangan tentukan tipe, kecuali jika tipe tersebut berbeda dari tipe ekspresinya.
| Buruk | Bagus |
|---|---|
|
|
Specify the type if the type of the expression does not match the desired type exactly.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F returns an object of type myError but we want error.
Awali Global yang Tidak Diekspor dengan _
Awali var dan const tingkat atas yang tidak diekspor dengan _ untuk memperjelas bahwa simbol tersebut bersifat global saat digunakan.
Alasan: Variabel dan konstanta tingkat atas memiliki cakupan package. Menggunakan nama yang umum dapat menyebabkan penggunaan nilai yang salah secara tidak sengaja di file lain.
| Buruk | Bagus |
|---|---|
|
|
Pengecualian: Nilai error yang tidak diekspor boleh menggunakan awalan err tanpa garis bawah.
Lihat Penamaan Error.
Menyematkan (Embedding) di dalam Struct
Tipe yang disematkan (embedded types) harus diletakkan di bagian atas daftar field dalam sebuah struct, dan harus ada satu baris kosong yang memisahkan field yang disematkan dari field biasa.
| Buruk | Bagus |
|---|---|
|
|
Penyematan (embedding) harus memberikan manfaat yang nyata, seperti menambahkan atau meningkatkan fungsionalitas dengan cara yang sesuai secara semantik. Hal ini harus dilakukan tanpa efek negatif yang terlihat oleh pengguna (lihat juga: Hindari Meng-embed Tipe di Struct Publik).
Pengecualian: Mutex sebaiknya tidak disematkan (embedded), bahkan pada tipe yang tidak diekspor (unexported). Lihat juga: Zero-value Mutex itu Valid.
Penyematan (embedding) tidak boleh:
- Bersifat kosmetik semata atau hanya demi kenyamanan.
- Membuat outer type menjadi lebih sulit untuk dikonstruksi atau digunakan.
- Mempengaruhi nilai nol (zero value) dari outer type. Jika outer type memiliki nilai nol yang berguna, maka nilai nol tersebut harus tetap berguna setelah menyematkan inner type.
- Mengekspos fungsi atau field yang tidak berhubungan dari outer type sebagai efek samping dari penyematan inner type.
- Mengekspos tipe yang tidak diekspor.
- Mempengaruhi semantik penyalinan (copy semantics) dari outer type.
- Mengubah API atau semantik tipe dari outer type.
- Menyematkan bentuk non-kanonik dari inner type.
- Mengekspos detail implementasi dari outer type.
- Membiarkan pengguna mengamati atau mengontrol internal tipe.
- Mengubah perilaku umum dari fungsi dalam melalui pembungkusan dengan cara yang kemungkinan besar akan mengejutkan pengguna.
Singkatnya, lakukan penyematan dengan sadar dan dengan niat yang jelas. Tes sederhana yang dapat digunakan adalah: "apakah semua method/field dari inner type ini layak diekspor secara langsung ke outer type"; jika jawabannya "hanya sebagian" atau "tidak", maka jangan sematkan inner type tersebut—gunakan sebagai field biasa.
| Buruk | Bagus |
|---|---|
|
|
|
|
|
|
Deklarasi Variabel Lokal
Short variable declarations (:=) harus digunakan jika variabel tersebut diberi nilai secara eksplisit.
| Buruk | Bagus |
|---|---|
|
|
Namun, ada kasus di mana nilai default yang lebih jelas ketika menggunakan var.
Declaring Empty Slices, misalnya.
| Buruk | Bagus |
|---|---|
|
|
nil adalah slice yang valid
nil adalah slice yang valid dengan panjang 0. Ini berarti bahwa,
-
Anda tidak harus mengembalikan slice dengan panjang nol secara eksplisit. Sebaiknya kembalikan
nil.Buruk Bagus if x == "" { return []int{} }if x == "" { return nil } -
Untuk memeriksa apakah slice kosong, selalu gunakan
len(s) == 0. Jangan periksanil.Buruk Bagus func isEmpty(s []string) bool { return s == nil }func isEmpty(s []string) bool { return len(s) == 0 } -
Nilai awal dengan value nol (slice yang dideklarasikan dengan
var) dapat langsung digunakan tanpamake().Buruk Bagus nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
Ingat bahwa, meskipun slice nil adalah slice yang valid, slice tersebut tidak sama dengan slice yang telah dialokasikan dengan panjang 0 — yang satu bernilai nil dan yang lainnya tidak — dan keduanya mungkin diperlakukan berbeda dalam situasi tertentu (seperti saat serialisasi).
Mengurangi Scope Variabel
Jika memungkinkan, kurangi scope dari variabel dan konstanta. Jangan kurangi scope jika hal tersebut konflik dengan Kurangi Level Nesting.
| Buruk | Bagus |
|---|---|
|
|
Jika Anda memerlukan hasil dari pemanggilan fungsi di luar if, maka Anda tidak harus mencoba untuk mengurangi scope.
| Buruk | Bagus |
|---|---|
|
|
Konstanta tidak perlu menjadi global kecuali jika mereka digunakan dalam beberapa fungsi atau file atau merupakan bagian dari kontrak eksternal dari package.
| Buruk | Bagus |
|---|---|
|
|
Hindari Naked Parameters
Naked parameters dalam pemanggilan fungsi dapat mengganggu keterbacaan. Tambahkan komentar C-style
(/* ... */) untuk nama parameter ketika arti mereka tidak jelas.
| Buruk | Bagus |
|---|---|
|
|
Lebih baik lagi, ganti tipe bool yang naked dengan tipe kustom untuk kode yang lebih mudah dibaca dan
type-safe. Ini memungkinkan lebih dari dua state (true/false) untuk parameter tersebut di masa depan.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status = iota + 1
StatusDone
// Mungkin kita akan memiliki StatusInProgress di masa depan.
)
func printInfo(name string, region Region, status Status)
Gunakan Raw String Literals untuk Menghindari Escaping
Go mendukung raw string literals, yang dapat melintasi beberapa baris dan termasuk kutip. Gunakan ini untuk menghindari string yang di-escaped secara manual yang jauh lebih sulit dibaca.
| Buruk | Bagus |
|---|---|
|
|
Menginisialisasi Struct
Gunakan Nama Field untuk Menginisialisasi Struct
Anda sebaiknya selalu menentukan nama field ketika menginisialisasi struct. Ini sekarang di-enforce oleh go vet.
| Buruk | Bagus |
|---|---|
|
|
Pengecualian: Nama field mungkin diabaikan dalam tabel test ketika ada 3 atau lebih sedikit field.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
Lewati Field Bernilai Nol dalam Struct
Saat menginisialisasi struct dengan nama field, lewati field yang memiliki nilai nol kecuali jika field tersebut memberikan konteks yang bermakna. Jika tidak, biarkan Go mengatur nilainya ke nilai nol secara otomatis.
| Buruk | Bagus |
|---|---|
|
|
Ini membantu mengurangi noise untuk pembaca dengan mengabaikan nilai-nilai yang default dalam konteks tersebut. Hanya nilai-nilai yang bermakna yang ditentukan.
Sertakan nilai nol di mana nama field memberikan konteks yang bermakna. Misalnya, test cases di Test Berbasis Tabel dapat menguntungkan dari nama-nama field meskipun mereka memiliki nilai nol.
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
Gunakan var untuk Struct dengan Nilai Nol
Jika semua field dari struct diabaikan dalam deklarasi, gunakan var untuk deklarasi struct.
| Buruk | Bagus |
|---|---|
|
|
Ini membedakan struct dengan nilai nol dari struct dengan field yang tidak nol sebagai bagian dari perbedaan yang dibuat untuk menginisialisasi map, dan sesuai dengan bagaimana kita lebih suka untuk deklarasi slice kosong.
Menginisialisasi Struct References
Gunakan &T{} daripada new(T) saat menginisialisasi referensi struct agar konsisten dengan inisialisasi struct.
| Buruk | Bagus |
|---|---|
|
|
Menginisialisasi Maps
Lebih baik gunakan make(..) untuk map kosong, dan map yang diisi secara programatik. Ini membuat inisialisasi map terlihat jelas berbeda dari deklarasi, dan mempermudah penambahan petunjuk ukuran jika tersedia di kemudian hari.
| Buruk | Bagus |
|---|---|
|
|
|
Deklarasi dan inisialisasi terlihat mirip. |
Deklarasi dan inisialisasi terlihat berbeda. |
Jika mungkin, berikan petunjuk ukuran ketika menginisialisasi map dengan make(). Lihat
Spesifikasikan Kapasitas Map
untuk informasi lebih lanjut.
Jika map berisi daftar elemen tetap, gunakan literal map untuk menginisialisasi map.
| Buruk | Bagus |
|---|---|
|
|
Aturan dasarnya adalah untuk menggunakan literal map ketika menambahkan set elemen tetap pada saat inisialisasi, jika tidak gunakan make (dan berikan petunjuk ukuran jika tersedia).
Format String di Luar Printf
Jika Anda deklarasi format strings untuk Printf-style functions di luar string
literal, buat mereka const values.
Ini membantu go vet melakukan analisis statis string format.
| Buruk | Bagus |
|---|---|
|
|
Penamaan Fungsi Printf
Ketika Anda mendeklarasikan fungsi Printf, pastikan agar go vet dapat mendeteksinya dan memeriksa string formatnya.
Ini berarti Anda sebaiknya menggunakan nama fungsi Printf yang sudah ditentukan sebelumnya jika memungkinkan. go vet akan memeriksa fungsi-fungsi ini secara default. Lihat keluarga Printf untuk informasi lebih lanjut.
Jika penggunaan nama yang sudah ditentukan sebelumnya tidak memungkinkan, akhiri nama fungsi yang Anda pilih dengan huruf f: misalnya Wrapf, bukan Wrap. go vet dapat diminta untuk memeriksa nama-nama fungsi Printf tertentu, tetapi nama tersebut harus diakhiri dengan f.
go vet -printfuncs=wrapf,statusf
Lihat juga go vet: Printf family check.
Patterns
Test Berbasis Tabel
Test berbasis tabel dengan subtest bisa menjadi pola yang berguna untuk menulis test guna menghindari duplikasi kode saat logika inti test bersifat repetitif.
Jika sistem yang diuji perlu diuji terhadap berbagai kondisi di mana bagian tertentu dari input dan output berubah, maka test berbasis tabel sebaiknya digunakan untuk mengurangi redundansi dan meningkatkan keterbacaan.
| Buruk | Bagus |
|---|---|
|
|
Tabel test mempermudah dalam menambahkan konteks ke pesan error, mengurangi logika yang duplikat, dan menambahkan kasus test baru.
Kami mengikuti konvensi bahwa slice dari struct disebut tests dan setiap kasus test disebut tt. Selain itu, kami mendorong untuk menjelaskan nilai input dan output dari setiap kasus test dengan awalan give dan want.
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
Hindari Kompleksitas yang Tidak Perlu dalam Table Test
Table test bisa menjadi sulit dibaca dan dirawat jika subtest-nya mengandung pengecekan bersyarat atau logika bercabang lainnya. Table test sebaiknya TIDAK digunakan apabila dibutuhkan logika kompleks atau bersyarat di dalam subtest (yaitu di dalam for loop).
Table test yang besar dan kompleks merugikan keterbacaan dan pemeliharaan karena pembaca test mungkin kesulitan menelusuri kegagalan test yang terjadi.
Table test seperti ini sebaiknya dibagi menjadi beberapa tabel test atau beberapa fungsi Test... terpisah.
Beberapa prinsip yang perlu diupayakan:
- Fokus pada unit perilaku yang paling sempit
- Minimalkan "kedalaman test", dan hindari pengecekan bersyarat (lihat di bawah)
- Pastikan semua field dalam tabel digunakan dalam semua test
- Pastikan semua logika test dijalankan untuk semua kasus dalam tabel
Dalam konteks ini, "kedalaman test" berarti "dalam suatu test tertentu, jumlah pernyataan berturut-turut yang membutuhkan pernyataan sebelumnya terpenuhi" (mirip dengan kompleksitas siklomatik).
Memiliki test yang "lebih dangkal" berarti ada lebih sedikit hubungan antar pernyataan dan, yang lebih penting, pernyataan-pernyataan tersebut cenderung tidak bersyarat secara default.
Secara konkret, table test bisa menjadi membingungkan dan sulit dibaca jika menggunakan banyak jalur bercabang (misalnya shouldError, expectCall, dll.), banyak pernyataan if untuk ekspektasi mock tertentu (misalnya shouldCallFoo), atau menempatkan fungsi di dalam tabel (misalnya setupMocks func(*FooMock)).
Namun, ketika menguji perilaku yang hanya berubah berdasarkan input yang berbeda, mungkin lebih baik mengelompokkan kasus-kasus serupa dalam table test untuk lebih menggambarkan bagaimana perilaku berubah di seluruh input, daripada membagi unit yang sebanding ke dalam test terpisah sehingga menjadi lebih sulit untuk dibandingkan dan dikontraskan.
Jika isi test singkat dan sederhana, boleh memiliki satu jalur bercabang untuk kasus sukses versus gagal dengan menggunakan field tabel seperti shouldErr untuk menentukan ekspektasi error.
| Buruk | Bagus |
|---|---|
|
|
Kerumitan ini membuat test menjadi lebih sulit untuk diubah, dipahami, dan membuktikan kebenarannya.
Meskipun tidak ada pedoman yang ketat, keterbacaan dan kemudahan pemeliharaan harus selalu menjadi prioritas utama saat memilih antara Table Tests atau test terpisah untuk berbagai input/output pada sebuah sistem.
Test Pararel
Test paralel, seperti beberapa loop khusus (misalnya, yang memicu goroutine atau menangkap referensi sebagai bagian dari isi loop), harus berhati-hati untuk secara eksplisit menetapkan variabel loop di dalam cakupan loop agar variabel tersebut memegang nilai yang diharapkan.
tests := []struct{
give string
// ...
}{
// ...
}
for _, tt := range tests {
tt := tt // untuk t.Parallel
t.Run(tt.give, func(t *testing.T) {
t.Parallel()
// ...
})
}
Dalam contoh di atas, kita harus mendeklarasikan variabel tt yang cakupannya terbatas pada iterasi loop karena penggunaan t.Parallel() di bawah. Jika tidak melakukan itu, sebagian besar atau semua test akan menerima nilai tt yang tidak terduga, atau nilai yang berubah saat test sedang berjalan.
Functional Options
Functional options adalah pola di mana Anda mendeklarasikan tipe Option yang tidak transparan (opaque) yang mencatat informasi dalam sebuah struct internal.
Anda menerima sejumlah variadik dari opsi ini dan bertindak berdasarkan seluruh informasi yang tercatat oleh opsi-opsi tersebut pada struct internal.
Gunakan pola ini untuk argumen opsional dalam konstruktor dan API publik lain yang mungkin perlu diperluas, terutama jika Anda sudah memiliki tiga atau lebih argumen pada fungsi-fungsi tersebut.
| Buruk | Bagus |
|---|---|
|
|
|
Parameter cache dan logger harus selalu diberikan, meskipun pengguna ingin menggunakan nilai default.
|
Opsi hanya diberikan jika diperlukan.
|
Cara yang kami sarankan untuk mengimplementasikan pola ini adalah dengan menggunakan interface Option yang memiliki metode tidak diekspor, yang mencatat opsi pada struct options yang juga tidak diekspor.
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open membuat koneksi.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
Perlu dicatat bahwa ada metode untuk mengimplementasikan pola ini menggunakan closure, tetapi kami percaya bahwa pola di atas memberikan fleksibilitas lebih bagi pembuat kode dan lebih mudah untuk debugging serta pengujian oleh pengguna. Secara khusus, pola ini memungkinkan opsi untuk dibandingkan satu sama lain dalam pengujian dan mock, sedangkan dengan closure hal ini tidak mungkin dilakukan. Selain itu, pola ini memungkinkan opsi mengimplementasikan interface lain, termasuk fmt.Stringer yang memungkinkan representasi string yang mudah dibaca pengguna untuk opsi-opsi tersebut.
Lihat juga,
Linting
Lebih penting daripada menggunakan kumpulan linter tertentu yang “dianggap terbaik”, adalah melakukan linting secara konsisten di seluruh basis kode.
Kami merekomendasikan menggunakan linter berikut minimal, karena dianggap membantu menangkap isu umum dan sekaligus menetapkan standar kualitas kode yang tinggi tanpa bersifat terlalu mengatur:
- errcheck untuk memastikan bahwa error ditangani dengan benar
- goimports untuk memformat kode dan mengelola import
- golint untuk menunjukkan kesalahan gaya umum
- govet untuk menganalisis kode terhadap kesalahan umum
- staticcheck untuk melakukan berbagai pemeriksaan analisis statis
Lint Runners
Kami merekomendasikan golangci-lint sebagai lint runner utama untuk kode Go, terutama karena performanya pada basis kode besar dan kemampuannya untuk mengonfigurasi serta menggunakan banyak linter kanonik sekaligus. Repo ini memiliki contoh file konfigurasi .golangci.yml dengan linter dan pengaturan yang direkomendasikan.
golangci-lint menyediakan berbagai linter yang bisa digunakan. Linter yang disebutkan di atas adalah set dasar yang direkomendasikan, dan kami mendorong tim untuk menambahkan linter tambahan yang sesuai dengan kebutuhan proyek mereka.