الإرشادات الأساسية لمعايير كتابة الشفرات في Golang

استخدام 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.

يجب أن يكون حجم القناة 1 أو غير مخزن

يجب أن تحتوي القنوات عادةً على حجم يساوي 1 أو تكون غير مخزنة. بشكل افتراضي، تكون القنوات غير مخزنة بحجم صفر. يجب أن يتم مراجعة أي حجم آخر بشكل صارم. يجب أن نفكر في كيفية تحديد الحجم، وننظر فيما يمنع القناة من الكتابة تحت الأحمال العالية وعند الحظر، وننظر في التغيرات التي تحدث في منطق النظام عند حدوث هذا.

غير مُوصى به:

// ينبغي أن يكون كافيًا للتعامل مع أي موقف!
c := make(chan int, 64)

مُوصى به:

// الحجم: 1
c := make(chan int, 1) // أو
// قناة غير مخزنة، الحجم هو 0
c := make(chan int)

تبدأ الثوابت من الرقم 1

الطريقة القياسية لإدخال الثوابت في Go هي إعلان نوع مخصص ومجموعة const تستخدم iota. نظرًا لأن القيمة الافتراضية للمتغيرات هي 0، يجب أن تبدأ الثوابت عادةً بقيمة غير صفر.

غير مُوصى به:

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

في بعض الحالات، يُعقل استخدام القيمة صفر (الثوابت تبدأ من الصفر)، على سبيل المثال، عندما تكون القيمة الصفرية هي السلوك الافتراضي المثالي.

type LogOutput int

const (
  LogToStdout LogOutput = iota
  LogToFile
  LogToRemote
)

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

استخدام العمليات الذرية

استخدم العمليات الذرية من حزمة sync/atomic للعمل على الأنواع الأساسية (int32, int64, وما إلى ذلك) لأنه من السهل نسيان استخدام العمليات الذرية لقراءة أو تعديل المتغيرات.

go.uber.org/atomic يضيف سلامة النوع لهذه العمليات عن طريق إخفاء النوع الأساسي. بالإضافة إلى ذلك، يتضمن نوع atomic.Bool المريح.

طريقة غير مُوصى بها:

type foo struct {
  running int32  // ذري
}

func (f* foo) start() {
  if atomic.SwapInt32(&f.running, 1) == 1 {
     // بالفعل قيد التشغيل…
     return
  }
  // ابدأ Foo
}

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
  }
  // ابدأ Foo
}

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

تجنب استخدام المتغيرات العالمية القابلة للتغيير

استخدم الحقن التالًي لتجنب تغيير المتغيرات العالمية. يمكن تطبيق هذا الأمر على مؤشرات الدالة وأنواع القيم الأخرى.

النهج غير الموصى به 1:

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

النهج الموصى به 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)
}

النهج غير الموصى به 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))
}

النهج الموصى به 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))
}

تجنب استخدام المعرفات المحددة مسبقًا

يستعرض مواصفات لغة Go العديد من المعرفات المحددة مسبقًا التي لا ينبغي استخدامها في مشاريع Go. يجب عدم إعادة استخدام هذه المعرفات المحددة مسبقًا كأسماء في سياقات مختلفة، حيث أن ذلك قد يخفي المعرّفات الأصلية في النطاق الحالي (أو أي نطاق متداخل)، مما قد يؤدي إلى الارتباك في الكود. في أفضل الحالات، سيقوم المترجم بإثارة خطأ. وفي أسوأ الحالات، قد يقدم مثل هذا الكود أخطاء صعبة الاسترداد.

الممارسة غير الموصى بها 1:

var error string
// يخفي `error` معرف البنية الأساسية ضمناً

// أو

func handleErrorMessage(error string) {
    // يخفي `error` معرف البنية الأساسية ضمناً
}

الممارسة الموصى بها 1:

var errorMessage string
// الآن، يشير `error` إلى المعرف الأصلي غير المخفي

// أو

func handleErrorMessage(msg string) {
    // الآن، يشير `error` إلى المعرف الأصلي غير المخفي
}

الممارسة غير الموصى بها 2:

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
}

الممارسة الموصى بها 2:

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. تجنب الإدخال/الإخراج، بما في ذلك نظم الملفات والشبكات واستدعاءات النظام.

الكود الذي لا يلبي هذه المتطلبات قد ينتمي كجزء من استدعاء main() (أو في أي مكان آخر في دورة حياة البرنامج) أو يكتب كجزء من main() نفسه. على وجه الخصوص، يجب أن تولي المكتبات المقصود استخدامها من قبل برامج أخرى اهتمامًا خاصًا للاكتمال بدلاً من أداء "سحر البدء".

الطريقة غير المفضلة 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()
    // سيئ: إدخال/إخراج
    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، سجلات الأنواع، إلخ.

تفضيل تحديد سعة الشريطة عند الإضافة

يجب أن يكون تحديد قيمة السعة أولوية عند استخدام make() عند تهيئة شريطة للإضافة.

الطريقة غير المفضلة:

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

من النظرية، تنسيق التسلسل لهيكل هو عقد بين أنظمة مختلفة. جعل تغييرات في شكل التسلسل للهيكل (بما في ذلك أسماء الحقول) سيكسر هذا العقد. تحديد أسماء الحقول في الوسوم يجعل العقد واضحًا ويساعد أيضًا في منع انتهاكات عرضية للعقد من خلال عمليات الإعادة الهيكلية أو إعادة تسمية الحقول عن طريق الخطأ.