راهنمایی‌های پایه استاندارد کدنویسی گولانگ

استفاده از defer برای آزادسازی منابع

از defer برای آزادسازی منابع مانند فایل‌ها و قفل‌ها استفاده کنید.

توصیه نمی‌شود:

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

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

return newCount

// آسان است که وقتی شاخه‌های بازگشت چندین‌تایی وجود داشته باشد، فراموش شود که آن‌ها را آزاد کنیم

توصیه می‌شود:

p.Lock()
defer p.Unlock()

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

p.count++
return p.count

// خوانایی بیشتر

فرسودگی defer بسیار پایین است، بنابراین تنها باید از آن اجتناب کرد وقتی که می‌توانید اثبات کنید زمان اجرای تابع در سطح نانوثانیه است. استفاده از defer برای بهبود خوانایی ارزش‌مند است زیرا هزینه استفاده از آن‌ها ناچیز است. این به‌ویژه برای متدهای بزرگ که شامل چیزهای بیشتری از دسترسی ساده به حافظه هستند که مصرف منابع محاسبات دیگر از defer فراتر می‌رود، قابل اجرا است.

اندازه کانال باید ۱ یا غیربافر شده باشد

کانال‌ها به‌طور معمول باید دارای اندازه ۱ یا غیربافر شوند. به‌طور پیش‌فرض، کانال‌ها غیربافر با اندازه صفر هستند. هر اندازه دیگری باید به‌صورت دقیق بررسی شود. ما باید در نظر داشته باشیم که چگونه اندازه را تعیین کنیم، مانعی که کانال را از نوشتن در شرایط بار بالا می‌کند و در هنگام مسدود شدن، تغییراتی که در منطق سیستم رخ می‌دهد را مد نظر قرار دهیم.

توصیه نمی‌شود:

// باید کافی باشد برای مدیریت هر شرایط!
c := make(chan int, 64)

توصیه می‌شود:

// اندازه: 1
c := make(chan int, 1) // یا
// کانال غیربافر، اندازه ۰
c := make(chan int)

Enums از عدد 1 شروع شود

روش استاندارد معرفی Enums در گولانگ این است که یک نوع سفارشی و یک گروه const که از iota استفاده می‌کند اعلام شود. از آنجایی که مقدار پیش‌فرض متغیرها ۰ است، Enums به‌طور معمول باید با یک مقدار غیرصفر شروع شوند.

توصیه نمی‌شود:

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

در برخی موارد، استفاده از مقدار صفر معنی دار است (Enums از صفر شروع می‌شوند)، به‌عنوان مثال، زمانی که مقدار صفر، رفتار پیش‌فرض ایده‌آل است.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

استفاده از atomic

از عملیات‌های atomic از پکیج sync/atomic برای عمل بر روی انواع ابتدایی (int32, int64 و غیره) استفاده کنید زیرا آسان است که عملیات‌های atomic برای خواندن یا تغییر متغیرها فراموش شود.

go.uber.org/atomic ایمنی نوع را به این عملیات‌ها اضافه می‌کند به خواص پنهان کردن نوع اساسی می‌پردازد. بیشتر این پکیج شامل نوع atomic.Bool راحتی نیز می‌شود.

روش توصیه نمی‌شود:

type foo struct {
  running int32  // atomic
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // از قبل درحال اجراست...
     return
  }
  // شروع فو
}

func (f *foo) isRunning() bool {
  return f.running == 1  // ریس!
}

روش توصیه می‌شود:

type foo struct {
  running atomic.Bool
}

func (f *foo) start() {
  if f.running.Swap(true) {
     // از قبل درحال اجراست...
     return
  }
  // شروع فوی
}

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

اجتناب استفاده از متغیرهای سراسری قابل تغییر

استفاده از روش تزریق وابستگی برای جلوگیری از تغییر متغیرهای سراسری را مورد استفاده قرار دهید. این مورد قابل اجرا برای اشاره‌گرهای تابع و نوع‌های مقادیر دیگر نیز می‌باشد.

رویکرد توصیه نشده ۱:

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

اجتناب از استفاده از اسامی قبلاً اعلام‌شده

مشخصات زبان گولان چندین شناسه قبلاً اعلام‌شده را مشخص کرده که در پروژه‌های گولان باید استفاده نشود. این شناسه‌های قبلاً اعلام‌شده نباید به عنوان نام‌ها در زمینه‌های مختلف مجدداً استفاده شوند، زیرا این کار ممکن است منجر به پنهان کردن شناسه‌های اصلی در دامنه جاری (یا هر دامنه تو در تو) شود و احتمالاً منجر به ابهام کدها شود. در بهترین حالت، کامپایلر خطاهایی را اعلام می‌کند؛ در بدترین حالت، چنین کدی ممکن است خطاهای پتانسیلی و دشوار برای بازیابی معرفی نماید.

روش توصیه نشده ۱:

var error string
// `error` به طور ضمنی شناسه تعریف‌شده جلویی را پنهان می‌کند

// یا

func handleErrorMessage(error string) {
    // `error` به طور ضمنی شناسه تعریف‌شده جلویی را پنهان می‌کند
}

روش توصیه شده ۱:

var errorMessage string
// `error` اکنون به شناسه تعریف‌شده جلویی وابسته است

// یا

func handleErrorMessage(msg string) {
    // `error` اکنون به شناسه تعریف‌شده جلویی وابسته است
}

روش توصیه نشده ۲:

type Foo struct {
    // با وجود اینکه این فیلدها به طور فنی سایه نمی‌افکنند، تعریف مجدد رشته‌های `error` و `string` اکنون دچار ابهام می‌شود.
    error  error
    string string
}

func (f Foo) Error() error {
    // `error` و `f.error` به صورت بصری به یکدیگر شبیه می‌آیند
    return f.error
}

func (f Foo) String() string {
    // `string` و `f.string` به صورت بصری به یکدیگر شبیه می‌آیند
    return f.string
}

روش توصیه شده ۲:

type Foo struct {
    // `error` و `string` اکنون صریح می‌باشند.
    err error
    str string
}

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

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

توجه داشته باشید که کامپایلر هنگام استفاده از شناسه‌های قبلاً اعلام‌شده خطا نمی‌دهد، اما ابزارهایی مانند go vet مشکلات مربوط به این و دیگر مسایل مرتبط را به درستی اشاره می‌کنند.

اجتناب استفاده از init()

سعی کنید تا جای ممکن از استفاده از init() خودداری کنید. زمانی که استفاده از init() غیرقابل اجتناب یا ترجیحی است، کد باید سعی کند که:

  1. کامل بودن را بدون توجه به محیط برنامه یا فراخوانی، تضمین کند.
  2. از وابستگی به ترتیب یا اثرات جانبی سایر توابع init() پرهیز کند. با اینکه ترتیب init() صریح است، اما کد ممکن است تغییر کند، بنابراین رابطه بین توابع init() ممکن است کد را آسیب پذیر و خطایی کند.
  3. از دسترسی یا تغییر وضعیت‌های سراسری یا محیطی، مانند اطلاعات دستگاه، متغیرهای محیطی، دایرکتوری‌های کاری، پارامترها/ورودی‌های برنامه و غیره جلوگیری کند.
  4. از I/O، شامل فایل سیستم، شبکه و فراخوانی‌های سیستمی، خودداری کند.

کدی که این الزامات را برآورده نمی‌کند، ممکن است به عنوان قسمتی از فراخوانی main() (یا جای دیگری در چرخه عمر برنامه) تعلق داشته باشد یا به عنوان بخشی از خود main() نوشته شود. به خصوص، کتابخانه‌های قرار است توسط برنامه‌های دیگر استفاده شوند، باید ویژه توجه به کامل بودن نسبت به انجام "جادوی init" داشته باشند.

رویکرد غیرتوصیه شده 1:

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

رویکرد توصیه شده 1:

var _defaultFoo = Foo{
    // ...
}
// یا، برای آزمون بهتر:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}

رویکرد غیرتوصیه شده 2:

type Config struct {
    // ...
}
var _config Config
func init() {
    // بد: بر اساس دایرکتوری فعلی
    cwd, _ := os.Getwd()
    // بد: I/O
    raw, _ := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}

رویکرد توصیه شده 2:

type Config struct {
    // ...
}
func loadConfig() Config {
    cwd, err := os.Getwd()
    // بررسی خطا
    raw, err := os.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    // بررسی خطا
    var config Config
    yaml.Unmarshal(raw, &config)
    return config
}

استفاده از نظرات فوق، در برخی موارد، init() ممکن است مورد ترجیح یا ضروری‌تر باشد، از جمله:

  • نمی‌توان به عنوان یک اختصاص واحد یک عبارت پیچیده نمایش داد.
  • قابل قراردادن هوک‌ها، مانند database/sql، ثبت انواع و غیره.

ترجیح دادن مشخص کردن ظرفیت برای برداشتن در دسته (slice)

همیشه در اولویت باید مقدار ظرفیت را برای make() هنگام مقداردهی اولیه یک دسته (slice) برای برداشتن مشخص کرد.

رویکرد غیرتوصیه شده:

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

استفاده از برچسب‌های فیلد در سریالسازی ساختارها

هنگام سریالسازی به JSON، YAML یا هر فرمت دیگری که پشتیبانی از تمییز بنام فیلد‌ها برمبنای برچسب‌ها دارد، باید برچسب‌های مربوطه برای حاشیه زدن استفاده شوند.

غیرتوصیه شده:

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"`
  // امن به منظور تغییر نام Name به Symbol هستند.
}
bytes, err := json.Marshal(Stock{
  Price: 137,
  Name:  "UBER",
})

در تئوری، فرمت سریال‌ساختار یک قرارداد بین سیستم‌های مختلف است. انجام تغییرات روی فرمت سریالی‌ساختار (شامل نام‌های فیلد) این قرارداد را شکسته خواهد کرد. مشخص کردن نام‌های فیلد در برچسب‌ها قرارداد را صریح می‌کند و همچنین به جلوگیری از نقضهای اتفاقی قرارداد از طریق بازخوانی یا تغییر نام فیلدها از طریق بازسازی کمک می‌کند.