- 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
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
goimports
saat save - Menjalankan
golint
dango vet
untuk memeriksa error
Anda bisa menemukan informasi tentang dukungan editor untuk Go tools di sini:
https://go.dev/wiki/IDEsAndTextEditorPlugins
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.
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 |
---|---|
type Handler struct {
// ...
}
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
...
} |
type Handler struct {
// ...
}
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
} |
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,
) {
// ...
}
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 dari sync.Mutex
dan sync.RWMutex
itu valid, jadi Anda hampir tidak pernah membutuhkan pointer ke mutex.
Buruk | Bagus |
---|---|
mu := new(sync.Mutex)
mu.Lock() |
var mu sync.Mutex
mu.Lock() |
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 |
---|---|
type SMap struct {
sync.Mutex
data map[string]string
}
func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}
func (m *SMap) Get(k string) string {
m.Lock()
defer m.Unlock()
return m.data[k]
} |
type SMap struct {
mu sync.Mutex
data map[string]string
}
func NewSMap() *SMap {
return &SMap{
data: make(map[string]string),
}
}
func (m *SMap) Get(k string) string {
m.mu.Lock()
defer m.mu.Unlock()
return m.data[k]
} |
Field |
Mutex dan metode-metodenya adalah detail implementasi dari |
Slice dan map berisi pointer ke data dasarnya, jadi berhati-hatilah dalam situasi ketika data tersebut perlu disalin.
Perlu diingat bahwa pengguna dapat memodifikasi map atau slice yang Anda terima sebagai argumen jika Anda menyimpan referensinya.
Bad | Good |
---|---|
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// Apakah Anda memang bermaksud untuk memodifikasi d1.trips?
trips[0] = ... |
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// Sekarang kita bisa memodifikasi trips[0] tanpa memengaruhi d1.trips.
trips[0] = ... |
Demikian juga, berhati-hatilah terhadap modifikasi pengguna pada map atau slice yang dapat mengekspos status internal.
Buruk | Bagus |
---|---|
type Stats struct {
mu sync.Mutex
counters map[string]int
}
// Snapshot mengembalikan statistik saat ini.
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters
}
// snapshot tidak lagi dilindungi oleh mutex, jadi
// setiap akses ke snapshot berpotensi terjadi data race.
snapshot := stats.Snapshot() |
type Stats struct {
mu sync.Mutex
counters map[string]int
}
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
}
// Snapshot sekarang adalah sebuah salinan.
snapshot := stats.Snapshot() |
Gunakan defer untuk membersihkan sumber daya seperti file dan kunci (locks).
Buruk | Bagus |
---|---|
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// mudah terlewat unlock karena ada beberapa return |
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// mudah dibaca |
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
.
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 |
---|---|
// Seharusnya cukup untuk siapa saja!
c := make(chan int, 64) |
// Ukuran adalah satu
c := make(chan int, 1) // atau
// Channel tidak berbuffer, ukuran nol
c := make(chan int) |
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 |
---|---|
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2 |
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3 |
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
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
saat menangani titik waktu, serta metode pada time.Time
saat membandingkan, menambah, atau mengurangi waktu.
Buruk | Bagus |
---|---|
func isActive(now, start, stop int) bool {
return start <= now && now < stop
} |
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
} |
Gunakan time.Duration
saat menangani periode waktu.
Buruk | Bagus |
---|---|
func poll(delay int) {
for {
// ...
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
poll(10) // itu detik atau milidetik ya? |
func poll(delay time.Duration) {
for {
// ...
time.Sleep(delay)
}
}
poll(10*time.Second) |
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.Duration
dan time.Time
saat berinteraksi dengan sistem eksternal bila memungkinkan. Contohnya:
- Flag baris perintah:
flag
mendukungtime.Duration
lewattime.ParseDuration
- JSON:
encoding/json
mendukung encodingtime.Time
sebagai string RFC 3339 lewat metodeUnmarshalJSON
- SQL:
database/sql
mendukung konversi kolomDATETIME
atauTIMESTAMP
menjaditime.Time
dan sebaliknya jika driver yang digunakan mendukung - YAML:
gopkg.in/yaml.v2
mendukungtime.Time
sebagai string RFC 3339, dantime.Duration
lewattime.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 |
---|---|
// {"interval": 2}
type Config struct {
Interval int `json:"interval"`
} |
// {"intervalMillis": 2000}
type Config struct {
IntervalMillis int `json:"intervalMillis"`
} |
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.
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.Is
atauerrors.As
dengan 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.Errorf
atau 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 |
---|---|
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
if err := foo.Open(); err != nil {
// Tidak bisa menangani error.
panic("unknown error")
} |
// package foo
var ErrCouldNotOpen = errors.New("could not open")
func Open() error {
return ErrCouldNotOpen
}
// package bar
if err := foo.Open(); err != nil {
if errors.Is(err, foo.ErrCouldNotOpen) {
// bisa menangani error
} else {
panic("unknown error")
}
} |
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 |
---|---|
// package foo
func Open(file string) error {
return fmt.Errorf("file %q not found", file)
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
// Tidak bisa menangani error
panic("unknown error")
} |
// package foo
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
func Open(file string) error {
return &NotFoundError{File: file}
}
// package bar
if err := foo.Open("testfile.txt"); err != nil {
var notFound *NotFoundError
if errors.As(err, ¬Found) {
// Bisa menangani error
} else {
panic("unknown error")
}
} |
Perlu diperhatikan bahwa jika Anda mengekspor variabel atau tipe error dari sebuah package, maka hal tersebut akan menjadi bagian dari API publik package tersebut.
Ada tiga opsi utama untuk meneruskan error jika suatu pemanggilan gagal:
- mengembalikan error asli apa adanya
- menambahkan konteks menggunakan
fmt.Errorf
dengan verb%w
- menambahkan konteks menggunakan
fmt.Errorf
dengan 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
%w
jika 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 merupakanvar
atau tipe yang dikenal, dokumentasikan dan uji sebagai bagian dari kontrak fungsi. - Gunakan
%v
untuk menyembunyikan error yang dibungkus. Pemanggil tidak dapat mencocokkannya, tetapi Anda bisa menggantinya dengan%w
di 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 |
---|---|
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
} |
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %w", err)
} |
|
|
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.
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)
}
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.Is
atauerrors.As
dan 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. |
u, err := getUser(id)
if err != nil {
// BURUK: Lihat deskripsi
log.Printf("Could not get user %q: %v", id, err)
return err
} |
Baik: Bungkus (wrap) error dan kembalikan Pemanggil (caller) di tingkat yang lebih atas akan menangani error tersebut. |
u, err := getUser(id)
if err != nil {
return fmt.Errorf("get user %q: %w", id, err)
} |
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. |
if err := emitMetrics(); err != nil {
// Gagal menulis metrik tidak boleh
// menyebabkan aplikasi berhenti.
log.Printf("Could not emit metrics: %v", err)
} |
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. |
tz, err := getUserTimeZone(id)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
// User tidak ada. Gunakan UTC.
tz = time.UTC
} else {
return fmt.Errorf("get user %q: %w", id, err)
}
} |
Bentuk pengembalian satu nilai dari type assertion akan menyebabkan panic jika tipe tidak sesuai. Oleh karena itu, selalu gunakan idiom "comma ok".
Buruk | Bagus |
---|---|
t := i.(string) |
t, ok := i.(string)
if !ok {
// tangani error dengan baik (gracefully)
} |
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 |
---|---|
func run(args []string) {
if len(args) == 0 {
panic("an argument is required")
}
// ...
}
func main() {
run(os.Args[1:])
} |
func run(args []string) error {
if len(args) == 0 {
return errors.New("an argument is required")
}
// ...
return nil
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
} |
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 |
---|---|
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
panic("failed to set up test")
} |
// func TestFoo(t *testing.T)
f, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal("failed to set up test")
} |
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 |
---|---|
type foo struct {
running int32 // atomic
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// sudah berjalan…
return
}
// mulai Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // race!
} |
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// sudah berjalan…
return
}
// mulai Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
} |
Hindari memodifikasi variabel global secara langsung, sebaiknya gunakan dependency injection. Ini berlaku untuk pointer fungsi maupun jenis nilai lainnya.
Buruk | Bagus |
---|---|
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
} |
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
} |
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
} |
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
} |
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 |
---|---|
// ConcreteList adalah daftar entitas.
type ConcreteList struct {
*AbstractList
} |
// ConcreteList adalah daftar entitas.
type ConcreteList struct {
list *AbstractList
}
// Add menambahkan sebuah entitas ke dalam list.
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// Remove menghapus sebuah entitas dari list.
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
} |
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 |
---|---|
// AbstractList is a generalized implementation
// for various kinds of lists of entities.
type AbstractList interface {
Add(Entity)
Remove(Entity)
}
// ConcreteList adalah daftar entitas.
type ConcreteList struct {
AbstractList
} |
// ConcreteList adalah daftar entitas.
type ConcreteList struct {
list AbstractList
}
// Add menambahkan sebuah entitas ke dalam list.
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// Remove menghapus sebuah entitas dari list.
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
} |
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.
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 |
---|---|
var error string
// `error` mengaburkan (shadows) identifier bawaan
// atau
func handleErrorMessage(error string) {
// `error` mengaburkan (shadows) identifier bawaan
} |
var errorMessage string
// `error` merujuk pada bawaan (builtin)
// or
func handleErrorMessage(msg string) {
// `error` merujuk pada bawaan (builtin)
} |
type Foo struct {
// Meskipun field-field ini secara teknis tidak
// termasuk shadowing, mencari string `error` atau `string`
// sekarang menjadi ambigu.
error error
string string
}
func (f Foo) Error() error {
// `error` dan `f.error` secara visual
// terlihat mirip
return f.error
}
func (f Foo) String() string {
// `string` and `f.string` secara visual
// terlihat mirip
return f.string
} |
type Foo struct {
// `error` dan `string` sekarang tidak ambigu.
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
} |
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 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 |
---|---|
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
} |
var _defaultFoo = Foo{
// ...
}
// atau, lebih baik lagi, untuk kemudahan test:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
} |
type Config struct {
// ...
}
var _config Config
func init() {
// Buruk: berdasarkan direktori saat ini
cwd, _ := os.Getwd()
// Buruk: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
} |
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// menangani err
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// menangani err
var config Config
yaml.Unmarshal(raw, &config)
return config
} |
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.
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 |
---|---|
func main() {
body := readFile(path)
fmt.Println(body)
}
func readFile(path string) string {
f, err := os.Open(path)
if err != nil {
log.Fatal(err)
}
b, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
}
return string(b)
} |
func main() {
body, err := readFile(path)
if err != nil {
log.Fatal(err)
}
fmt.Println(body)
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
b, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
} |
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 test
terlewat. - Proses cleanup terabaikan: Saat sebuah fungsi keluar dari program, semua pemanggilan fungsi yang dijadwalkan dengan
defer
tidak akan dijalankan. Ini meningkatkan risiko proses cleanup penting tidak dijalankan.
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 |
---|---|
package main
func main() {
args := os.Args[1:]
if len(args) != 1 {
log.Fatal("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// Jika kita memanggil log.Fatal setelah baris ini,
// f.Close tidak akan dipanggil.
b, err := io.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
} |
package main
func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}
func run() error {
args := os.Args[1:]
if len(args) != 1 {
return errors.New("missing file")
}
name := args[0]
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return err
}
// ...
} |
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.
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 |
---|---|
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
}) |
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Safe to rename Name to Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
}) |
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.
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 |
---|---|
go func() {
for {
flush()
time.Sleep(delay)
}
}() |
var (
stop = make(chan struct{}) // memberikan sinyal ke goroutine untuk berhenti
done = make(chan struct{}) // memberikan sinyal ke kode lain untuk menunggu goroutine selesai
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// Elsewhere...
close(stop) // memberikan sinyal ke goroutine untuk berhenti
<-done // menunggu goroutine selesai |
Tidak ada cara untuk menghentikan goroutine ini. Goroutine ini akan berjalan sampai aplikasi selesai. |
Goroutine ini dapat dihentikan dengan |
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
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 |
---|---|
func init() {
go doWork()
}
func doWork() {
for {
// ...
}
} |
type Worker struct{ /* ... */ }
func NewWorker(...) *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
// ...
}
go w.doWork()
}
func (w *Worker) doWork() {
defer close(w.done)
for {
// ...
case <-w.stop:
return
}
}
// Shutdown memberikan sinyal ke worker untuk berhenti
// dan menunggu hingga worker selesai.
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
} |
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 |
Pedoman performa berlaku hanya pada hot path.
Ketika mengkonversi primitif ke/dari string, strconv
lebih cepat dari fmt
.
Buruk | Bagus |
---|---|
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
} |
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
} |
|
|
Jangan membuat byte slices dari string yang tetap berulang. Sebaliknya, lakukan konversi sekali dan simpan hasilnya.
Buruk | Bagus |
---|---|
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
} |
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
} |
|
|
Tentukan kapasitas container di awal untuk mengalokasikan memori untuk container. Ini meminimalkan alokasi berikutnya (melalui salinan dan penyesuaian ukuran container) saat elemen ditambahkan.
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 |
---|---|
m := make(map[string]os.FileInfo)
files, _ := os.ReadDir("./files")
for _, f := range files {
m[f.Name()] = f
} |
files, _ := os.ReadDir("./files")
m := make(map[string]os.DirEntry, len(files))
for _, f := range files {
m[f.Name()] = f
} |
|
|
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 |
---|---|
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
} |
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
} |
|
|
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.
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.
Go mendukung pengelompokan deklarasi yang mirip.
Buruk | Bagus |
---|---|
import "a"
import "b" |
import (
"a"
"b"
) |
Pengelompokan ini juga berlaku untuk konstanta, variabel, dan deklarasi tipe.
Buruk | Bagus |
---|---|
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64 |
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
) |
Hanya kelompokkan deklarasi yang berkaitan. Jangan kelompokkan deklarasi yang tidak berkaitan.
Buruk | Bagus |
---|---|
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
EnvVar = "MY_ENV"
) |
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const EnvVar = "MY_ENV" |
Kelompok tidak terbatas di mana mereka dapat digunakan. Misalnya, Anda bisa menggunakan mereka didalam fungsi.
Buruk | Bagus |
---|---|
func f() string {
red := color.New(0xff0000)
green := color.New(0x00ff00)
blue := color.New(0x0000ff)
// ...
} |
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
// ...
} |
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 |
---|---|
func (c *client) request() {
caller := c.name
format := "json"
timeout := 5*time.Second
var err error
// ...
} |
func (c *client) request() {
var (
caller = c.name
format = "json"
timeout = 5*time.Second
err error
)
// ...
} |
Ada dua kelompok import:
- Standard library
- Semuanya
Ini adalah kelompok yang diterapkan oleh goimports secara default.
Buruk | Bagus |
---|---|
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
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.
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
.
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 |
---|---|
import (
"fmt"
"os"
runtimetrace "runtime/trace"
nettrace "golang.net/x/trace"
) |
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
) |
- 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 |
---|---|
func (s *something) Cost() {
return calcCost(s.weights)
}
type something struct{ ... }
func calcCost(n []int) int {...}
func (s *something) Stop() {...}
func newSomething() *something {
return &something{}
} |
type something struct{ ... }
func newSomething() *something {
return &something{}
}
func (s *something) Cost() {
return calcCost(s.weights)
}
func (s *something) Stop() {...}
func calcCost(n []int) int {...} |
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 |
---|---|
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
} |
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
} |
Jika sebuah variabel diset di kedua cabang dari sebuah if, maka hal tersebut dapat digantikan dengan satu if saja.
Buruk | Bagus |
---|---|
var a int
if b {
a = 100
} else {
a = 10
} |
a := 10
if b {
a = 100
} |
Di tingkat atas, gunakan kata kunci standar var
. Jangan tentukan tipe, kecuali jika tipe tersebut berbeda dari tipe ekspresinya.
Buruk | Bagus |
---|---|
var _s string = F()
func F() string { return "A" } |
var _s = F()
// Since F already states that it returns a string, we don't need to specify
// the type again.
func F() string { return "A" } |
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 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 |
---|---|
// foo.go
const (
defaultPort = 8080
defaultUser = "user"
)
// bar.go
func Bar() {
defaultPort := 9090
...
fmt.Println("Default port", defaultPort)
// Kita tidak akan melihat error kompilasi jika baris pertama dari
// Bar() dihapus.
} |
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
) |
Pengecualian: Nilai error yang tidak diekspor boleh menggunakan awalan err
tanpa garis bawah.
Lihat Penamaan Error.
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 |
---|---|
type Client struct {
version int
http.Client
} |
type Client struct {
http.Client
version int
} |
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 |
---|---|
type A struct {
// Buruk: A.Lock() dan A.Unlock() sekarang tersedia,
// tidak memberikan manfaat fungsional,
// dan memungkinkan pengguna mengontrol detail
// internal dari A.
sync.Mutex
} |
type countingWriteCloser struct {
// Bagus: Write() disediakan pada
// lapisan luar untuk tujuan
// spesifik, dan mendelegasikan
// pekerjaan ke Write() dari
// inner type.
io.WriteCloser
count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
w.count += len(bs)
return w.WriteCloser.Write(bs)
} |
type Book struct {
// Buruk: pointer mengubah kegunaan nilai nol (zero value).
io.ReadWriter
// field lain
}
// kemudian
var b Book
b.Read(...) // panic: nil pointer
b.String() // panic: nil pointer
b.Write(...) // panic: nil pointer |
type Book struct {
// Bagus: memiliki nilai nol (zero value) yang berguna.
bytes.Buffer
// field lain
}
// kemudian
var b Book
b.Read(...) // ok
b.String() // ok
b.Write(...) // ok |
type Client struct {
sync.Mutex
sync.WaitGroup
bytes.Buffer
url.URL
} |
type Client struct {
mtx sync.Mutex
wg sync.WaitGroup
buf bytes.Buffer
url url.URL
} |
Short variable declarations (:=
) harus digunakan jika variabel tersebut diberi nilai secara eksplisit.
Buruk | Bagus |
---|---|
var s = "foo" |
s := "foo" |
Namun, ada kasus di mana nilai default yang lebih jelas ketika menggunakan var
.
Declaring Empty Slices, misalnya.
Buruk | Bagus |
---|---|
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
} |
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
} |
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).
Jika memungkinkan, kurangi scope dari variabel dan konstanta. Jangan kurangi scope jika hal tersebut konflik dengan Kurangi Level Nesting.
Buruk | Bagus |
---|---|
err := os.WriteFile(name, data, 0644)
if err != nil {
return err
} |
if err := os.WriteFile(name, data, 0644); err != nil {
return err
} |
Jika Anda memerlukan hasil dari pemanggilan fungsi di luar if, maka Anda tidak harus mencoba untuk mengurangi scope.
Buruk | Bagus |
---|---|
if data, err := os.ReadFile(name); err == nil {
err = cfg.Decode(data)
if err != nil {
return err
}
fmt.Println(cfg)
return nil
} else {
return err
} |
data, err := os.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil |
Konstanta tidak perlu menjadi global kecuali jika mereka digunakan dalam beberapa fungsi atau file atau merupakan bagian dari kontrak eksternal dari package.
Buruk | Bagus |
---|---|
const (
_defaultPort = 8080
_defaultUser = "user"
)
func Bar() {
fmt.Println("Default port", _defaultPort)
} |
func Bar() {
const (
defaultPort = 8080
defaultUser = "user"
)
fmt.Println("Default port", defaultPort)
} |
Naked parameters dalam pemanggilan fungsi dapat mengganggu keterbacaan. Tambahkan komentar C-style
(/* ... */
) untuk nama parameter ketika arti mereka tidak jelas.
Buruk | Bagus |
---|---|
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true) |
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */) |
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)
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 |
---|---|
wantError := "unknown name:\"test\"" |
wantError := `unknown error:"test"` |
Anda sebaiknya selalu menentukan nama field ketika menginisialisasi struct. Ini sekarang di-enforce oleh go vet
.
Buruk | Bagus |
---|---|
k := User{"John", "Doe", true} |
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
} |
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"},
}
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 |
---|---|
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
} |
user := User{
FirstName: "John",
LastName: "Doe",
} |
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},
// ...
}
Jika semua field dari struct diabaikan dalam deklarasi, gunakan var
untuk deklarasi struct.
Buruk | Bagus |
---|---|
user := User{} |
var user User |
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.
Gunakan &T{}
daripada new(T)
saat menginisialisasi referensi struct agar konsisten dengan inisialisasi struct.
Buruk | Bagus |
---|---|
sval := T{Name: "foo"}
// tidak konsisten
sptr := new(T)
sptr.Name = "bar" |
sval := T{Name: "foo"}
sptr := &T{Name: "bar"} |
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 |
---|---|
var (
// m1 aman untuk dibaca dan ditulis;
// m2 akan panic ketika ditulis.
m1 = map[T1]T2{}
m2 map[T1]T2
) |
var (
// m1 aman untuk dibaca dan ditulis;
// m2 akan panic ketika ditulis.
m1 = make(map[T1]T2)
m2 map[T1]T2
) |
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 |
---|---|
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3 |
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
} |
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).
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 |
---|---|
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2) |
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2) |
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.
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 |
---|---|
// func TestSplitHostPort(t *testing.T)
host, port, err := net.SplitHostPort("192.0.2.0:8000")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "8000", port)
host, port, err = net.SplitHostPort("192.0.2.0:http")
require.NoError(t, err)
assert.Equal(t, "192.0.2.0", host)
assert.Equal(t, "http", port)
host, port, err = net.SplitHostPort(":8000")
require.NoError(t, err)
assert.Equal(t, "", host)
assert.Equal(t, "8000", port)
host, port, err = net.SplitHostPort("1:8")
require.NoError(t, err)
assert.Equal(t, "1", host)
assert.Equal(t, "8", port) |
// func TestSplitHostPort(t *testing.T)
tests := []struct{
give string
wantHost string
wantPort string
}{
{
give: "192.0.2.0:8000",
wantHost: "192.0.2.0",
wantPort: "8000",
},
{
give: "192.0.2.0:http",
wantHost: "192.0.2.0",
wantPort: "http",
},
{
give: ":8000",
wantHost: "",
wantPort: "8000",
},
{
give: "1:8",
wantHost: "1",
wantPort: "8",
},
}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
host, port, err := net.SplitHostPort(tt.give)
require.NoError(t, err)
assert.Equal(t, tt.wantHost, host)
assert.Equal(t, tt.wantPort, port)
})
} |
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 {
// ...
}
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 |
---|---|
func TestComplicatedTable(t *testing.T) {
tests := []struct {
give string
want string
wantErr error
shouldCallX bool
shouldCallY bool
giveXResponse string
giveXErr error
giveYResponse string
giveYErr error
}{
// ...
}
for _, tt := range tests {
t.Run(tt.give, func(t *testing.T) {
// setup mocks
ctrl := gomock.NewController(t)
xMock := xmock.NewMockX(ctrl)
if tt.shouldCallX {
xMock.EXPECT().Call().Return(
tt.giveXResponse, tt.giveXErr,
)
}
yMock := ymock.NewMockY(ctrl)
if tt.shouldCallY {
yMock.EXPECT().Call().Return(
tt.giveYResponse, tt.giveYErr,
)
}
got, err := DoComplexThing(tt.give, xMock, yMock)
// memverifikasi hasil
if tt.wantErr != nil {
require.EqualError(t, err, tt.wantErr)
return
}
require.NoError(t, err)
assert.Equal(t, want, got)
})
}
} |
func TestShouldCallX(t *testing.T) {
// setup mocks
ctrl := gomock.NewController(t)
xMock := xmock.NewMockX(ctrl)
xMock.EXPECT().Call().Return("XResponse", nil)
yMock := ymock.NewMockY(ctrl)
got, err := DoComplexThing("inputX", xMock, yMock)
require.NoError(t, err)
assert.Equal(t, "want", got)
}
func TestShouldCallYAndFail(t *testing.T) {
// setup mocks
ctrl := gomock.NewController(t)
xMock := xmock.NewMockX(ctrl)
yMock := ymock.NewMockY(ctrl)
yMock.EXPECT().Call().Return("YResponse", nil)
_, err := DoComplexThing("inputY", xMock, yMock)
assert.EqualError(t, err, "Y failed")
} |
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 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 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 |
---|---|
// package db
func Open(
addr string,
cache bool,
logger *zap.Logger
) (*Connection, error) {
// ...
} |
// package db
type Option interface {
// ...
}
func WithCache(c bool) Option {
// ...
}
func WithLogger(log *zap.Logger) Option {
// ...
}
// Open membuat koneksi.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
// ...
} |
Parameter cache dan logger harus selalu diberikan, meskipun pengguna ingin menggunakan nilai default. db.Open(addr, db.DefaultCache, zap.NewNop())
db.Open(addr, db.DefaultCache, log)
db.Open(addr, false /* cache */, zap.NewNop())
db.Open(addr, false /* cache */, log) |
Opsi hanya diberikan jika diperlukan. db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
addr,
db.WithCache(false),
db.WithLogger(log),
) |
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,
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
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.