Golang Kodlama Standart Temel İlkeleri

Kaynakları serbest bırakmak için defer kullanın

Dosya ve kilit gibi kaynakları serbest bırakmak için defer kullanın.

Tavsiye edilmez:

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

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

return newCount

// Birden fazla dönüş dalı olduğunda kilidi açmayı unutmak kolaydır

Tavsiye edilir:

p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// Daha okunaklı

Defer'in maliyeti son derece düşüktür, bu nedenle fonksiyonun yürütme süresinin nanosaniye düzeyinde olduğunu kanıtlayabildiğinizde dışlanmalıdır. Okunabilirliği artırmak için defer kullanmak, bunun maliyetinin ihmal edilebilir olduğu için değerlidir. Bu özellikle yalnızca basit bellek erişiminin ötesindeki daha büyük yöntemler için geçerlidir, diğer hesaplamaların kaynak tüketimi, defer'ıninkinden çok daha fazla olduğunda.

Kanalın boyutu 1 veya puffersiz olmalıdır

Kanallar genellikle 1 boyutunda veya puffersiz olmalıdır. Varsayılan olarak kanallar puffersizdir ve boyutları sıfırdır. Herhangi başka bir boyutta kesinlikle gözden geçirilmelidir. Kanalın boyutunu belirlemenin nasıl olduğu, yüksek yük altında kanalın yazma işlemini engelleyen şeyin ne olduğu ve bloke olduğunda sistem mantığında hangi değişikliklerin meydana geldiği düşünülmelidir.

Tavsiye edilmez:

// Herhangi bir durumu karşılamak için yeterli olmalıdır!
c := make(chan int, 64)

Tavsiye edilir:

// Boyut: 1
c := make(chan int, 1) // veya
// Puffersiz kanal, boyutu 0
c := make(chan int)

Enum'lar 1'den başlar

Go'da enum tanıtmak için standart metot, iota kullanarak custom bir tip ve bir const grup bildirmektir. Değişkenlerin varsayılan değeri 0 olduğundan, enumlar genellikle sıfırdan farklı bir değerle başlamalıdır.

Tavsiye edilmez:

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

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

Tavsiye edilir:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

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

Bazı durumlarda sıfır değeri kullanmak mantıklı olabilir (enumlar sıfırdan başlar), örneğin sıfır değeri ideal varsayılan davranış ise.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

Atomic kütüphanesini kullanın

İlkellik türleri (int32, int64, vb.) üzerinde işlem yapmak için sync/atomic paketinden atomic işlemleri kullanın, çünkü değişkenleri okumak veya değiştirmek için atomic işlemleri kullanmayı unutmak kolaydır.

go.uber.org/atomic bu işlemlere tip güvenliğini gizleyerek ekler. Ayrıca kullanışlı bir atomic.Bool tipini içerir.

Tavsiye edilmeyen yaklaşım:

type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // zaten çalışıyor…
     return
  }
  // Foo'yu başlat
}

func (f *foo) isRunning() bool {
  return f.running == 1  // yarış koşulu!
}

Tavsiye edilen yaklaşım:

type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // zaten çalışıyor…
     return
  }
  // Foo'yu başlat
}

func (f *foo) isRunning() bool {
  return f.running.Load()
}

Değişkenlerin Değiştirilebilir Global Değişkenlerden Kaçının

Global değişkenleri değiştirmekten kaçınmak için bağımlılık enjeksiyon yaklaşımını kullanın. Bu, işlev işaretçileri için geçerli olduğu gibi diğer değer tipleri için de geçerlidir.

Tavsiye edilmeyen yaklaşım 1:

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

Tavsiye edilen yaklaşım 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)
}

Tavsiye edilmeyen yaklaşım 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))
}

Tavsiye edilen yaklaşım 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))
}

Önceden Tanımlanmış Kimlikleri Kullanmaktan Kaçının

Go Dil Belirtimi Go projelerinde kullanılmaması gereken birkaç önceden tanımlanmış kimliği açıklar. Bu önceden tanımlanmış kimlikler farklı bağlamlarda adların tekrar kullanılmaması gerektiğini belirtir, aksi takdirde mevcut kapsamda (veya herhangi bir iç içe kapsamda) orijinal kimlikleri gizleyebilir ve potansiyel olarak kod karışıklığına neden olabilir. En iyi durumda derleyici bir hata verir; en kötü durumda ise bu tür kodlar potansiyel, geri kazanılması zor hatalara neden olabilir.

Tavsiye edilmeyen Uygulama 1:

var hata string
// `error` önceden tanımlı kimliği zımnen gizler

// veya

func hataMesajıİşle(error string) {
    // `error` önceden tanımlı kimliği zımnen gizler
}

Tavsiye edilen Uygulama 1:

var hataMesajı string
// `error` artık gölgelememiş önceden tanımlı kimliğe işaret eder

// veya

func hataMesajıİşle(mesaj string) {
    // `error` artık gölgelememiş önceden tanımlı kimliğe işaret eder
}

Tavsiye edilmeyen Uygulama 2:

type Foo struct {
    // Bu alanların teknik olarak gölgelemesi olmasa da, `error` veya `string` şimdi tekrar tanımlanabilir hale gelir.
    hata  error
    string string
}

func (f Foo) Hata() error {
    // `error` ve `f.error` görsel olarak benzer görünür
    return f.hata
}

func (f Foo) String() string {
    // `string` ve `f.string` görsel olarak benzer görünür
    return f.string
}

Tavsiye edilen Uygulama 2:

type Foo struct {
    // `error` ve `string` artık açıktır.
    hata error
    str string
}

func (f Foo) Hata() error {
    return f.hata
}

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

Derleyici, önceden tanımlı kimlikleri kullanırken hata oluşturmaz, ancak go vet gibi araçlar bu ve diğer zımni ilişkili sorunları doğru bir şekilde belirtecektir.

init() Kullanmaktan Kaçının

Mümkün olduğunca init() kullanmaktan kaçının. init() kullanılmaması mümkün olmadığı veya tercih edildiği durumlarda, kod aşağıdaki hususlara dikkat etmelidir:

  1. Program ortamından veya çağrıdan bağımsız olarak eksiksiz olmasını sağlayın.
  2. init() işlevlerinin sırasına veya yan etkilerine bağlı kalmaktan kaçının. init()'in sırası açık olmasına rağmen, kod değişebileceğinden init() işlevleri arasındaki ilişki kodu kırılgan ve hata eğilimli hale getirebilir.
  3. Makine bilgileri, ortam değişkenleri, çalışma dizinleri, program parametreleri/girdileri vb. gibi global veya ortam durumlarına erişimi veya manipülasyonunu engelleyin.
  4. Dosya sistemleri, ağ ve sistem çağrıları dahil olmak üzere I/O'ya erişimi engelleyin.

Bu gereksinimleri karşılamayan kodlar, main() çağrısının bir parçası (veya programın yaşam döngüsünün başka bir yerinde) olarak veya main()'in kendisi olarak yazılmalıdır. Özellikle diğer programlar tarafından kullanılması amaçlanan kütüphaneler, "init sihirbazlığı" yerine eksiksizliğe özel bir dikkat göstermelidir.

Önerilmeyen yaklaşım 1:

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

Tavsiye edilen yaklaşım 1:

var _defaultFoo = Foo{
    // ...
}
// veya, daha iyi test edilebilirlik için:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

Önerilmeyen yaklaşım 2:

type Config struct {
    // ...
}
var _config Config
func init() {
    // Kötü: mevcut dizine bağlı
    cwd, _ := os.Getwd()
    // Kötü: I/O
    raw, _ := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}

Tavsiye edilen yaklaşım 2:

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