Skip to content

stanleydv12/uber-go-guide-id

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 

Repository files navigation

Panduan Penulisan Kode Go oleh Uber

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:

  1. Effective Go
  2. Go Common Mistakes
  3. Go Code Review Comments

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 dan go vet untuk 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:

  1. Sebuah pointer ke informasi yang spesifik ke tipe. Anda bisa menganggap ini sebagai "type."
  2. 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
BurukBagus
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,
) {
  // ...
}

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.

BurukBagus
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.

BurukBagus
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, serta metode Lock dan Unlock, secara tidak sengaja menjadi bagian dari API yang diekspor oleh SMap.

Mutex dan metode-metodenya adalah detail implementasi dari SMap yang disembunyikan dari pemanggilnya.

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
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] = ...

Mengembalikan Slice dan Map

Demikian juga, berhati-hatilah terhadap modifikasi pengguna pada map atau slice yang dapat mengekspos status internal.

BurukBagus
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

Gunakan defer untuk membersihkan sumber daya seperti file dan kunci (locks).

BurukBagus
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.

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.

BurukBagus
// 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)

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.

BurukBagus
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

Gunakan "time" untuk Mengelola Waktu

Waktu itu rumit. Kesalahan asumsi yang sering dibuat tentang waktu meliputi:

  1. Sehari itu 24 jam
  2. Satu jam itu 60 menit
  3. Seminggu itu 7 hari
  4. Setahun itu 365 hari
  5. 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.

BurukBagus
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 untuk Periode Waktu

Gunakan time.Duration saat menangani periode waktu.

BurukBagus
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.Time dan time.Duration dengan Sistem Eksternal

Gunakan time.Duration dan time.Time saat berinteraksi dengan sistem eksternal bila memungkinkan. Contohnya:

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.

BurukBagus
// {"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.

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 fungsi errors.Is atau errors.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 menggunakan errors.New, tapi untuk yang kedua harus menggunakan fmt.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 cocokError 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 cocokError 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, &notFound) {
    // 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.

Pembungkusan Error

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 merupakan var 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:

BurukBagus
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)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error

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.Is atau errors.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:

DeskripsiKode

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.
Penggunaan %w memastikan mereka dapat mencocokkan error menggunakan errors.Is atau errors.As jika relevan.

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)
  }
}

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".

BurukBagus
t := i.(string)
t, ok := i.(string)
if !ok {
  // tangani error dengan baik (gracefully)
}

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.

BurukBagus
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.

BurukBagus
// 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")
}

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.

BurukBagus
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 Global yang Dapat Dimutasi

Hindari memodifikasi variabel global secara langsung, sebaiknya gunakan dependency injection. Ini berlaku untuk pointer fungsi maupun jenis nilai lainnya.

BurukBagus
// 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))
}

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) {
  // ...
}
BurukBagus
// 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.

BurukBagus
// 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.

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.

BurukBagus
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 init()

Hindari penggunaan init() jika memungkinkan. Jika init() tidak bisa dihindari atau memang diperlukan, kode harus berusaha untuk:

  1. Bersifat sepenuhnya deterministik, tidak tergantung pada environment program atau cara pemanggilan.
  2. Menghindari ketergantungan pada urutan atau efek samping dari fungsi init() lain.
    Meskipun urutan init() sudah diketahui, kode bisa berubah, sehingga hubungan antar fungsi init() bisa membuat kode rentan dan mudah error.
  3. Menghindari akses atau manipulasi global state atau environment satet, seperti informasi mesin, variabel environment, direktori kerja, argumen/masukan program, dll.
  4. 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".

BurukBagus
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.

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.

BurukBagus
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.

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.

BurukBagus
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 ke run
  • 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.

BurukBagus
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.

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:

BurukBagus
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 close(stop), kode lain dapat menunggu goroutine selesai dengan <-done.

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.

BurukBagus
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 WaitGroup. Lihat juga Menunggu goroutine untuk selesai.

Performa

Pedoman performa berlaku hanya pada hot path.

Gunakan strconv dibandingkan fmt

Ketika mengkonversi primitif ke/dari string, strconv lebih cepat dari fmt.

BurukBagus
for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}
for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

Hindari konversi string ke byte yang berulang

Jangan membuat byte slices dari string yang tetap berulang. Sebaliknya, lakukan konversi sekali dan simpan hasilnya.

BurukBagus
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)
}
BenchmarkBad-4   50000000   22.2 ns/op
BenchmarkGood-4  500000000   3.25 ns/op

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.

BurukBagus
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
}

m dibuat tanpa hint kapasitas; mungkin ada lebih alokasi pada saat penugasan.

m dibuat dengan hint kapasitas; mungkin ada lebih sedikit alokasi pada saat penugasan.

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).

BurukBagus
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)
  }
}
BenchmarkBad-4    100000000    2.48s
BenchmarkGood-4   100000000    0.21s

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.

BurukBagus
import "a"
import "b"
import (
  "a"
  "b"
)

Pengelompokan ini juga berlaku untuk konstanta, variabel, dan deklarasi tipe.

BurukBagus
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.

BurukBagus
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.

BurukBagus
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.

BurukBagus
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
  )

  // ...
}

Urutan Mengelompokkan Import

Ada dua kelompok import:

  • Standard library
  • Semuanya

Ini adalah kelompok yang diterapkan oleh goimports secara default.

BurukBagus
import (
  "fmt"
  "os"
  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)
import (
  "fmt"
  "os"

  "go.uber.org/atomic"
  "golang.org/x/sync/errgroup"
)

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, bukan net/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.

BurukBagus
import (
  "fmt"
  "os"
  runtimetrace "runtime/trace"

  nettrace "golang.net/x/trace"
)
import (
  "fmt"
  "os"
  "runtime/trace"

  nettrace "golang.net/x/trace"
)

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.

BurukBagus
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 {...}

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.

BurukBagus
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()
}

Else yang Tidak Perlu

Jika sebuah variabel diset di kedua cabang dari sebuah if, maka hal tersebut dapat digantikan dengan satu if saja.

BurukBagus
var a int
if b {
  a = 100
} else {
  a = 10
}
a := 10
if b {
  a = 100
}

Deklarasi Variabel Tingkat Atas

Di tingkat atas, gunakan kata kunci standar var. Jangan tentukan tipe, kecuali jika tipe tersebut berbeda dari tipe ekspresinya.

BurukBagus
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 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.

BurukBagus
// 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.

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.

BurukBagus
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.

BurukBagus
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
}

Deklarasi Variabel Lokal

Short variable declarations (:=) harus digunakan jika variabel tersebut diberi nilai secara eksplisit.

BurukBagus
var s = "foo"
s := "foo"

Namun, ada kasus di mana nilai default yang lebih jelas ketika menggunakan var. Declaring Empty Slices, misalnya.

BurukBagus
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

nil adalah slice yang valid dengan panjang 0. Ini berarti bahwa,

  • Anda tidak harus mengembalikan slice dengan panjang nol secara eksplisit. Sebaiknya kembalikan nil.

    BurukBagus
    if x == "" {
      return []int{}
    }
    if x == "" {
      return nil
    }
  • Untuk memeriksa apakah slice kosong, selalu gunakan len(s) == 0. Jangan periksa nil.

    BurukBagus
    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 tanpa make().

    BurukBagus
    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.

BurukBagus
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.

BurukBagus
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.

BurukBagus
const (
  _defaultPort = 8080
  _defaultUser = "user"
)

func Bar() {
  fmt.Println("Default port", _defaultPort)
}
func Bar() {
  const (
    defaultPort = 8080
    defaultUser = "user"
  )
  fmt.Println("Default port", defaultPort)
}

Hindari Naked Parameters

Naked parameters dalam pemanggilan fungsi dapat mengganggu keterbacaan. Tambahkan komentar C-style (/* ... */) untuk nama parameter ketika arti mereka tidak jelas.

BurukBagus
// 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)

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.

BurukBagus
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`

Menginisialisasi Struct

Gunakan Nama Field untuk Menginisialisasi Struct

Anda sebaiknya selalu menentukan nama field ketika menginisialisasi struct. Ini sekarang di-enforce oleh go vet.

BurukBagus
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"},
}

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.

BurukBagus
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},
  // ...
}

Gunakan var untuk Struct dengan Nilai Nol

Jika semua field dari struct diabaikan dalam deklarasi, gunakan var untuk deklarasi struct.

BurukBagus
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.

Menginisialisasi Struct References

Gunakan &T{} daripada new(T) saat menginisialisasi referensi struct agar konsisten dengan inisialisasi struct.

BurukBagus
sval := T{Name: "foo"}

// tidak konsisten
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}

sptr := &T{Name: "bar"}

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.

BurukBagus
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.

BurukBagus
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).

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.

BurukBagus
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2)

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.

BurukBagus
// 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 {
  // ...
}

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.

BurukBagus
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 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.

BurukBagus
// 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,

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.

About

The Uber Go Guide - Terjemahan Bahasa Indonesia. Sumber : https://github.com/uber-go/guide

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published