Pedoman Dasar Standar Koding Golang

Gunakan defer untuk melepaskan sumber daya

Gunakan defer untuk melepaskan sumber daya seperti file dan kunci.

Tidak disarankan:

p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// Mudah untuk lupa membuka kunci ketika terdapat beberapa cabang pengembalian

Disarankan:

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// Lebih mudah dibaca

Biaya dari defer sangat rendah, sehingga sebaiknya dihindari hanya ketika Anda bisa membuktikan bahwa waktu eksekusi fungsi berada pada level nanodetik. Menggunakan defer untuk meningkatkan kemudahan baca sangat bermanfaat karena biaya penggunaannya yang sangat kecil. Hal ini terutama berlaku untuk metode yang lebih besar yang melibatkan lebih dari sekadar akses memori sederhana, di mana konsumsi sumber daya dari perhitungan lain jauh melebihi dari defer.

Ukuran Channel harus 1 atau tanpa buffer

Channel seharusnya memiliki ukuran 1 atau tanpa buffer. Secara default, channel adalah tanpa buffer dengan ukuran nol. Setiap ukuran lainnya harus diperiksa dengan ketat. Kita perlu mempertimbangkan bagaimana menentukan ukuran, mempertimbangkan apa yang mencegah channel dari penulisan dalam beban tinggi dan saat diblokir, serta mempertimbangkan perubahan apa yang terjadi dalam logika sistem saat hal ini terjadi.

Tidak disarankan:

// Harusnya cukup untuk menangani situasi apapun!
c := make(chan int, 64)

Disarankan:

// Ukuran: 1
c := make(chan int, 1) // atau
// Channel tanpa buffer, ukurannya 0
c := make(chan int)

Enum dimulai dari 1

Metode standar untuk memperkenalkan enum di Go adalah dengan mendeklarasikan tipe kustom dan sebuah grup const yang menggunakan iota. Karena nilai default dari variabel adalah 0, enum seharusnya biasanya dimulai dengan nilai yang bukan nol.

Tidak disarankan:

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2

Disarankan:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3

Dalam beberapa kasus, menggunakan nilai nol memiliki makna (enum dimulai dari nol), misalnya, ketika nilai nol adalah perilaku default yang ideal.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

// LogToStdout=0, LogToFile=1, LogToRemote=2

Menggunakan atomic

Gunakan operasi atomic dari paket sync/atomic untuk mengoperasikan tipe-tipe primitif (int32, int64, dll.) karena mudah untuk lupa menggunakan operasi atomic untuk membaca atau memodifikasi variabel.

go.uber.org/atomic menambahkan keamanan tipe pada operasi-operasi tersebut dengan menyembunyikan tipe yang mendasarinya. Selain itu, ini termasuk tipe atomic.Bool yang nyaman.

Sikap yang tidak disarankan:

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

Sikap yang disarankan:

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 variabel global yang dapat berubah

Gunakan pendekatan dependency injection untuk menghindari mengubah variabel global. Ini berlaku untuk pointer fungsi dan tipe nilai lainnya.

Pendekatan yang tidak disarankan 1:

// sign.go
var _timeNow = time.Now
func sign(msg string) string {
  now := _timeNow()
  return signWithTime(msg, now)
}

Pendekatan yang disarankan 1:

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

Pendekatan yang tidak disarankan 2:

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

Pendekatan yang disarankan 2:

// 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 Penggunaan Identifier yang Sudah Dideklarasikan

Spesifikasi Bahasa Go menjabarkan beberapa identifier yang sudah dideklarasikan yang tidak boleh digunakan dalam proyek Go. Identifier yang sudah dideklarasikan ini tidak boleh digunakan kembali sebagai nama dalam konteks yang berbeda, karena hal tersebut akan menyembunyikan identifier asli dalam cakupan saat ini (atau cakupan bertingkat mana pun), yang berpotensi menyebabkan kebingungan kode. Dalam kasus terbaik, compiler akan menimbulkan kesalahan; dalam kasus terburuk, kode semacam ini dapat memperkenalkan kesalahan yang sulit diperbaiki.

Praktik yang tidak disarankan 1:

var error string
// `error` secara implisit menutupi identifier bawaan

// atau

func handleErrorMessage(error string) {
    // `error` secara implisit menutupi identifier bawaan
}

Praktik yang disarankan 1:

var errorMessage string
// `error` sekarang menunjuk ke identifier bawaan yang tidak terselubung

// atau

func handleErrorMessage(msg string) {
    // `error` sekarang menunjuk ke identifier bawaan yang tidak terselubung
}

Praktik yang tidak disarankan 2:

type Foo struct {
    // Meskipun bidang-bidang ini secara teknis tidak menutupi, mendefinisikan ulang string `error` atau `string` sekarang menjadi ambigu.
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` dan `f.error` secara visual terlihat serupa
    return f.error
}

func (f Foo) String() string {
    // `string` dan `f.string` secara visual terlihat serupa
    return f.string
}

Praktik yang disarankan 2:

type Foo struct {
    // `error` dan `string` sekarang eksplisit.
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

Perlu dicatat bahwa compiler tidak akan menimbulkan kesalahan saat menggunakan identifier yang sudah dideklarasikan, namun alat seperti go vet akan dengan benar menyoroti masalah terkait secara implisit.

Hindari penggunaan init()

Cobalah untuk menghindari penggunaan init() sebisa mungkin. Ketika init() tidak dapat dihindari atau diutamakan, kode harus mencoba untuk:

  1. Memastikan kelengkapan tanpa memperhatikan lingkungan program atau pemanggilan.
  2. Hindari mengandalkan urutan atau efek samping dari fungsi init() lainnya. Meskipun urutan init() adalah eksplisit, kode dapat berubah, sehingga hubungan antara fungsi init() dapat membuat kode menjadi rapuh dan rentan terhadap kesalahan.
  3. Hindari mengakses atau memanipulasi status global atau lingkungan, seperti informasi mesin, variabel lingkungan, direktori kerja, parameter/ masukan program, dll.
  4. Hindari I/O, termasuk sistem file, jaringan, dan panggilan sistem.

Kode yang tidak memenuhi persyaratan ini mungkin sebaiknya dimasukkan sebagai bagian dari panggilan main() (atau di tempat lain dalam siklus program) atau ditulis sebagai bagian dari main() itu sendiri. Terutama, perpustakaan yang dimaksudkan untuk digunakan oleh program lain harus memperhatikan kelengkapan daripada melakukan "inisialisasi ajaib".

Pendekatan yang tidak disarankan 1:

type Foo struct {
    // ...
}
var _defaultFoo Foo
func init() {
    _defaultFoo = Foo{
        // ...
    }
}

Pendekatan yang disarankan 1:

var _defaultFoo = Foo{
    // ...
}
// atau, untuk uji coba yang lebih baik:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

Pendekatan yang tidak disarankan 2:

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

Pendekatan yang disarankan 2:

type Config struct {
    // ...
}
func loadConfig() Config {
    cwd, err := os.Getwd()
    // tangani kesalahan
    raw, err := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // tangani kesalahan
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

Dengan pertimbangan di atas, dalam beberapa kasus, init() mungkin lebih diutamakan atau diperlukan, termasuk:

  • Tidak dapat diwakili sebagai satu tugas ungkapan kompleks.
  • Hook yang dapat dimasukkan, seperti database/sql, registri tipe, dll.

Lebih memilih menentukan kapasitas slice saat melakukan append

Selalu prioritaskan menentukan nilai kapasitas untuk make() saat menginisialisasi slice yang akan dilampirkan.

Pendekatan yang tidak disarankan:

for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

Pendekatan yang disarankan:

for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}

Menggunakan Tag Lapangan dalam Serialisasi Struktur

Saat melakukan serialisasi ke JSON, YAML, atau format lain yang mendukung penamaan lapangan berdasarkan tag, tag relevan harus digunakan untuk anotasi.

Tidak Disarankan:

type Stock struct {
  Price int
  Name  string
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

Disarankan:

type Stock struct {
  Price int    `json:"price"`
  Name  string `json:"name"`
  // Aman untuk mengubah Nama menjadi Simbol.
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

Secara teoritis, format serialisasi struktur adalah kontrak antara sistem-sistem berbeda. Mengubah format serialisasi struktur (termasuk nama lapangan) akan melanggar kontrak ini. Menentukan nama lapangan dalam tag membuat kontrak menjadi eksplisit dan juga membantu mencegah pelanggaran kontrak secara tidak sengaja melalui refaktorisasi atau pengubahan nama lapangan.