تجنب الأسطر الطويلة للغاية

تجنب استخدام أسطر الكود التي تتطلب من القراء الرجوع إلى اليمين أو يسار الشاشة بشكل مفرط.

نوصي بتقييد طول السطر إلى 99 حرفًا. يجب على الكتاب كسر السطر قبل هذا الحد، ولكنه ليس قاعدة صارمة. يمكن أن يتجاوز الكود هذا الحد.

الاتساق

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

الكود الثابت يسهل صيانته، وهو أكثر تنطيقًا، ويتطلب تكلفة تعلم أقل، وأسهل في الترحيل، التحديث، وإصلاح الأخطاء عند ظهور تقاليد جديدة أو حدوث أخطاء.

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

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

تجميع الإعلانات المماثلة

يدعم لغة Go تجميع الإعلانات المماثلة.

غير موصى به:

import "a"
import "b"

موصى به:

import (
  "a"
  "b"
)

كما ينطبق هذا على الثوابت والمتغيرات وإعلانات النوع:

غير موصى به:

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
)

قم بتجميع الإعلانات ذات الصلة معًا وتجنب تجميع الإعلانات غير ذات الصلة.

غير موصى به:

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"

لا توجد قيود على مكان استخدام التجميع. على سبيل المثال، يمكنك استخدامها داخل دالة:

غير موصى به:

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

  ...
}

استثناء: إذا كانت إعلانات المتغيرات متلاصقة مع متغيرات أخرى، خاصة ضمن إعلانات المحلّية في الوظائف، يجب تجميعها معًا. قم بذلك حتى للمتغيرات غير المتصلة المعلنة معًا.

غير موصى به:

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

تجميع الاستيرادات

يجب تجميع الاستيرادات في فئتين:

  • المكتبة القياسية
  • المكتبات الأخرى

افتراضيًا، هذا هو التجميع الذي يُطبقه goimports. غير موصى به:

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

موصى به:

import (
  "fmt"
  "os"

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

اسم الحزمة

عند تسمية الحزمة، يرجى اتباع هذه القواعد:

  • حروف صغيرة فقط، دون حروف كبيرة أو شرطات سفلية.
  • في معظم الحالات، لا حاجة لإعادة التسمية عند الاستيراد.
  • قصير وموجز. تذكر أن الاسم مؤهل بالكامل في كل مكان يُستخدم فيه.
  • تجنب الجمع. على سبيل المثال، استخدم net/url بدلاً من net/urls.
  • تجنب استخدام "common"، "util"، "shared"، أو "lib". هذه ليست معلومات كافية.

تسمية الوظائف

نحن نلتزم بتقليد مجتمع Go في استخدام MixedCaps لأسماء الوظائف. يتم عمل استثناء في تجميع حالات الاختبار ذات الصلة، حيث يمكن أن يحتوي اسم الوظيفة على شرطات سفلية، مثل: TestMyFunction_WhatIsBeingTested.

اسماء مستوردة

إذا لم يتطابق اسم الحزمة مع العنصر الأخير في مسار الاستيراد، يجب استخدام اسم استيراد بديل.

import (
  "net/http"

  client "example.com/client-go"
  trace "example.com/trace/v2"
)

في جميع الحالات الأخرى، يجب تجنب اسماء مستوردة بديلة ما لم يكن هناك تعارض مباشر بين الاستيرادات. غير مُستحسن:

import (
  "fmt"
  "os"

  nettrace "golang.net/x/trace"
)

مُستحسن:

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

  nettrace "golang.net/x/trace"
)

تجميع وترتيب الوظائف

  • يجب ترتيب الوظائف تقريباً حسب الترتيب الذي يتم فيه استدعاؤها.
  • يجب تجميع الوظائف داخل نفس الملف وفقاً للمستقبل.

لذلك، يجب أن تظهر الوظائف المصدرة أولاً في الملف، موضوعة بعد تعاريف struct، const، و var.

قد تظهر newXYZ()/NewXYZ() بعد تعاريف الأنواع ولكن قبل باقي أساليب المستقبل.

مع تجميع الوظائف وفقاً للمستقبل، يجب أن تظهر الوظائف العامة للأدوات في نهاية الملف. غير مُستحسن:

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

تقليل التداخل

يجب على الكود تقليل التداخل عن طريق التعامل مع الأخطاء/الحالات الخاصة في أقرب وقت ممكن وإما بإرجاعها أو مواصلة الحلقة. تقليل التداخل يقلل من كمية الكود على مستويات متعددة.

غير مُستحسن:

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

الفروع غير الضرورية

إذا كانت المتغيرات تُعين في كلا الفروعين للجملة الشرطية، يمكن استبدالها بجملة شرطية واحدة.

غير مُستحسن:

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

مُستحسن:

a := 10
if b {
  a = 100
}

إعلان المتغيرات على المستوى الرئيسي

على المستوى الرئيسي، استخدم الكلمة الرئيسية var القياسية. لا تحدد النوع ما لم يختلف عن نوع التعبير.

غير مُستحسن:

var _s string = F()

func F() string { return "A" }

مُستحسن:

var _s = F()
// بما أن F تُرجع نوع string بشكل صريح، فلا حاجة لتحديد النوع بشكل صريح لـ _s

func F() string { return "A" }

حدد النوع إذا لم يتطابق تماما مع النوع المطلوب للتعبير.

type myError struct{}

func (myError) Error() string { return "error" }

func F() myError { return myError{} }

var _e error = F()
// تعيد F مثيلًا من نوع myError، لكن نحن بحاجة إلى نوع error

استخدام '_' كبادئة للثوابت والمتغيرات العلوية غير المصديرة

بالنسبة للثوابت والمتغيرات العلوية غير المصديرة، يجب أن يتم بادرتها بشرطة سفلية _ للإشارة بشكل صريح إلى طبيعتها العامة عند الاستخدام.

المنطق الأساسي: المتغيرات والثوابت العلوية لها نطاق على مستوى الحزمة. يمكن أن يؤدي استخدام أسماء عامة بسهولة إلى استخدام القيمة الخطأ بطريق الخطأ في ملفات أخرى.

غير مستحب:

// foo.go

const (
  defaultPort = 8080
  defaultUser = "user"
)

// bar.go

func Bar() {
  defaultPort := 9090
  ...
  fmt.Println("Default port", defaultPort)

  // لن نرى خطأ ترجمة إذا تم حذف السطر الأول من
  // Bar()
}

مستحب:

// foo.go

const (
  _defaultPort = 8080
  _defaultUser = "user"
)

استثناء: يمكن أن تستخدم القيم غير المصديرة للأخطاء بادئة "err" بدون شرطة سفلية. انظر إلى تسمية الأخطاء.

تضمين في الهياكل

يجب وضع الأنواع المضمنة (مثل mutex) في أعلى قائمة الحقول داخل الهيكل ويجب أن يكون هناك سطر فارغ يفصل الحقول المضمنة عن الحقول العادية.

غير مستحب:

type Client struct {
  version int
  http.Client
}

مستحب:

type Client struct {
  http.Client

  version int
}

يجب أن يوفر التضمين فوائد ملموسة، مثل إضافة أو تعزيز الوظائف بطريقة دلالية مناسبة. يجب استخدامه دون أي تأثير سلبي على المستخدم. (انظر أيضا: تجنب تضمين الأنواع في الهياكل العمومية)

استثناءات: حتى في الأنواع غير المصدرة، يجب عدم استخدام Mutex كحقل مضمن. انظر أيضا: صحيح معترف به صفري للMutex.

عدم الاستحباط:

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

بإختصار، التضمين ينبغي أن يتم بوعي وبغرض. اختبار جيد هو، "هل سيتم إضافة جميع هذه الوظائف/الحقول المصدرة مباشرة إلى النوع الخارجي؟" إذا كان الجواب "نعم" أو "بعضها"، فلا تضمّ النوع الداخلي - استخدم الحقول بدلاً من ذلك.

غير مستحب:

type A struct {
    // سيئة: A.Lock() وA.Unlock() متاحة الآن
    // لا توفر أي فائدة وتسمح للمستخدم بالتحكم في التفاصيل الداخلية لـ A.
    sync.Mutex
}

مستحب:

type countingWriteCloser struct {
    // جيد: يتم توفير Write() على المستوى الخارجي لغرض محدد،
    // ويحول العمل إلى Write() من النوع الداخلي.
    io.WriteCloser
    count int
}
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
    w.count += len(bs)
    return w.WriteCloser.Write(bs)
}

إعلانات المتغيرات المحلية

إذا تم تعيين قيمة لمتغير بشكل صريح، يجب استخدام الشكل المختصر للإعلان (:=).

غير مستحب:

var s = "foo"

مستحب:

s := "foo"

على الرغم من ذلك، في بعض الحالات، يمكن أن يكون استخدام كلمة "var" للقيم الافتراضية أكثر وضوحًا.

غير مستحب:

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 هي قوائم فارغة صحيحة

nil هي قائمة صحيحة بطول 0، وهذا يعني:

  • يجب عليك عدم إرجاع قائمة بطول صفر بشكل صريح. بدلاً من ذلك، عدِّل nil.

غير مُنصوح به:

if x == "" {
  return []int{}
}

مُنصوح به:

if x == "" {
  return nil
}
  • للتحقق مما إذا كانت القائمة فارغة، استخدم دائمًا len(s) == 0 بدلاً من nil.

غير مُنصوح به:

func isEmpty(s []string) bool {
  return s == nil
}

مُنصوح به:

func isEmpty(s []string) bool {
  return len(s) == 0
}
  • يمكن استخدام القوائم ذات القيمة صفرية (القوائم التي تم إعلانها باستخدام var) مباشرة دون استدعاء make().

غير مُنصوح به:

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

تذكر، على الرغم من أن القائمة الفارغة nil هي قائمة صحيحة، إلا أنها ليست متساوية لقائمة بطول 0 (إحداهما nil والأخرى ليست)، وقد يتم التعامل معهما بشكل مختلف في حالات مختلفة (على سبيل المثال، التسلسل).

تقليص نطاق المتغيرات

إذا كان ممكنًا، حاول تقليص نطاق المتغيرات، ما لم يتعارض ذلك مع قاعدة تقليل التداخل.

غير مُنصوح به:

err := os.WriteFile(name, data, 0644)
if err != nil {
 return err
}

مُنصوح به:

if err := os.WriteFile(name, data, 0644); err != nil {
 return err
}

إذا كان يتعين استخدام نتيجة استدعاء دالة خارج التعبير if، فلا تحاول تقليص نطاق المتغيرات.

غير مُنصوح به:

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

تجنب تضمين المعاملات بشكل معزول

قد تؤثر المعاملات المفهومة بشكل غير واضح في استدعاءات الدوال سلبًا على قراءة الكود. عندما لا يكون معنى أسماء المعاملات واضحًا، يمكنك إضافة تعليقات بنمط C (/* ... */) إلى المعاملات.

غير مُنصوح به:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true, true)

مُنصوح به:

// func printInfo(name string, isLocal, done bool)

printInfo("foo", true /* isLocal */, true /* done */)

في المثال أعلاه، يمكن أن يكون النهج المثلى هو استبدال أنواع bool بأنواع مخصصة. بذلك، يمكن أن يدعم المعامل مستقبلاً ما هو أكثر من حالتين (صحيح/غير صحيح).

type Region int

const (
  UnknownRegion Region = iota
  Local
)

type Status int

const (
  StatusReady Status= iota + 1
  StatusDone
  // قد نقوم في المستقبل بإضافة حالة "قيد التقدم".
)

func printInfo(name string, region Region, status Status)

استخدام السلاسل النصية الخام لتجنب الحاجة لتقديم المصدر

تدعم Go استخدام السلاسل النصية الخامة، حيث تُعرَّف باستخدام " ` " لتمثيل السلاسل النصية الخام. في المواقف التي تتطلب التقديم، يجب استخدام هذا النهج لتنفيذ السلاسل النصية الخامة بدلاً من السلاسل النصية المعقدة للقراءة.

ويمكن أن تشمل سلاسل النصوص الخامة عدة أسطر وتكون قابلة للتضمين فيها الاقتباسات. يمكن استخدام هذه السلاسل لتجنب السلاسل النصية المعقدة والصعبة للقراءة.

غير مُنصوح به:

wantError := "unknown name:\"test\""

مُنصوح به:

wantError := `unknown error:"test"`

تهيئة الهياكل باستخدام أسماء الحقول

عند تهيئة هيكل، يجب تقريبا دائما تحديد أسماء الحقول. يتم فرض ذلك حالياً من خلال go vet.

غير مُفضل:

k := User{"John", "Doe", true}

مُفضل:

k := User{
    FirstName: "John",
    LastName: "Doe",
    Admin: true,
}

استثناء: عند وجود 3 حقول أو أقل، يمكن أحيانًا حذف أسماء الحقول في الجداول الاختبارية.

tests := []struct{
  op Operation
  want string
}{
  {Add, "add"},
  {Subtract, "subtract"},
}

حذف الحقول ذات قيمة صفر في الهياكل

عند تهيئة هيكل باسماء الحقول، ما لم تكن هناك سياق ذو معنى، يجب تجاهل الحقول التي تحتوي على قيمة صفر. بمعنى، دعنا نضبط هذه تلقائياً لتكون قيم صفر.

غير مُفضل:

user := User{
  FirstName: "John",
  LastName: "Doe",
  MiddleName: "",
  Admin: false,
}

مُفضل:

user := User{
  FirstName: "John",
  LastName: "Doe",
}

يساعد هذا على تقليل العقبات في القراءة من خلال حذف القيم الافتراضية في السياق. حدد القيم ذات المعنى فقط.

تضمن القيمة الصفرية عندما توفر أسماء الحقول سياقًا معنويًا. على سبيل المثال، يمكن لحالات الاختبار في جداول الاختبار المدفوعة بالأسماء الفائدة من تسمية الحقول، حتى لو كانت قيمها صفر.

tests := []struct{
  give string
  want int
}{
  {give: "0", want: 0},
  // ...
}

استخدام var لهياكل القيمة الصفرية

إذا تم حذف كافة الحقول في هيكل، استخدم var لتعريف الهيكل.

غير مُفضل:

user := User{}

مُفضل:

var user User

يميز هذا الهيكل ذو القيمة الصفرية عن الهياكل التي تحتوي على حقول ذات قيم غير صفرية، على غرار ما نفضله عند تعريف قائمة فارغة.

تهيئة مراجع الهيكل

عند تهيئة مراجع الهيكل، استخدم &T{} بدلًا من new(T) لجعله متسقًا مع تهيئة الهيكل.

غير مُفضل:

sval := T{Name: "foo"}

// غير متسق
sptr := new(T)
sptr.Name = "bar"

مُفضل:

sval := T{Name: "foo"}

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

تهيئة الخرائط

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

غير مُفضل:

var (
  // m1 مكتوبة بطريقة آمنة للقراءة والكتابة؛
  // m2 تحدث استثناء عند الكتابة
  m1 = map[T1]T2{}
  m2 map[T1]T2
)

مُفضل:

var (
  // m1 مكتوبة بطريقة آمنة للقراءة والكتابة؛
  // m2 تحدث استثناء عند الكتابة
  m1 = make(map[T1]T2)
  m2 map[T1]T2
)

| التعريف والتهيئة تبدو متشابهة جداً. | التعريف والتهيئة تبدو مختلفة جداً. |

عند الإمكانية، قدم حجم الخريطة أثناء التهيئة، انظر تحديد حجم الخريطة للتفاصيل.

بالإضافة إلى ذلك، إذا احتوت الخريطة على قائمة ثابتة من العناصر، استخدم القيم الثابتة لتهيئة الخريطة.

غير مُفضل:

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

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

تنسيق السلسلة لدوال الطباعة بنمط Printf

إذا قمت بتعريف سلسلة تنسيق لدالة بنمط Printf خارج دالة، قم بتعيينها كثابت const.

يساعد هذا go vet في أداء تحليل ثابت على سلسلة التنسيق.

غير مُفضل:

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

مُفضل:

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

تسمية الدوال بنمط Printf

عند إعلان دوال بنمط Printf، تأكد من أن go vet يمكنه اكتشاف سلسلة التنسيق وفحصها.

وهذا يعني أنه يجب عليك استخدام أسماء الدوال بنمط Printf المحددة مسبقًا قدر الإمكان. سيقوم go vet بالتحقق من ذلك تلقائيًا. لمزيد من المعلومات، انظر Printf Family.

إذا لم يمكن استخدام الأسماء المحددة مسبقًا، يجب أن ينتهي الاسم المحدد بـ f: Wrapf بدلاً من Wrap. يمكن لـ go vet طلب فحص أسماء معينة بنمط Printf ولكن يجب أن ينتهي الاسم بـ f.

go vet -printfuncs=wrapf,statusf