دليل أسلوب Uber لـ Go
March 1, 2025 · View on GitHub
دليل أسلوب Uber لـ Go
- المقدمة
- الإرشادات
- مؤشرات للواجهات (Interfaces)
- التحقق من امتثال الواجهة
- المستقبلات والواجهات
- تعتبر قيمة Mutex الصفرية صالحة
- نسخ Slices و Maps عند الحدود
- استخدام Defer للتنظيف
- حجم القناة (Channel) واحد أو لا شيء
- بدء التعدادات (Enums) عند الرقم واحد
- استخدام "time" للتعامل مع الوقت
- الأخطاء
- التعامل مع فشل تأكيد النوع
- لا تستخدم Panic
- استخدم go.uber.org/atomic
- تجنب المتغيرات العامة القابلة للتغيير
- تجنب تضمين الأنواع في الهياكل العامة
- تجنب استخدام الأسماء المدمجة
- تجنب
init() - الخروج في الدالة Main
- استخدام علامات الحقل في الهياكل المحولة
- لا تطلق goroutines دون متابعة
- الأداء
- الأسلوب
- تجنب الأسطر الطويلة جدًا
- كن متسقًا
- تجميع التصريحات المتشابهة
- ترتيب مجموعات الاستيراد
- أسماء الحزم
- أسماء الدوال
- استخدام الأسماء المستعارة في الاستيراد
- تجميع وترتيب الدوال
- تقليل التداخل
- تجنب استخدام Else غير الضروري
- تصريحات المتغيرات على المستوى العالي
- اضافة بادئة _ للمتغيرات العامة غير المصدرة
- التضمين في الهياكل
- تصريحات المتغيرات المحلية
- nil هي قيمة صالحة لـ slice
- تقليل نطاق المتغيرات
- تجنب المعاملات المجردة
- استخدام محارف النصوص الخام لتجنب الهروب
- تهيئة الهياكل
- تهيئة Maps
- سلاسل التنسيق خارج Printf
- تسمية دوال Printf
- الأنماط
- التدقيق اللغوي
المقدمة
الأساليب هي القواعد التي تحكم كودنا. مصطلح "الأسلوب" قد يكون مضللاً بعض الشيء، لأن هذه القواعد تغطي أكثر بكثير من مجرد تنسيق ملف المصدر - فـ gofmt يتعامل مع ذلك بالنسبة لنا.
الهدف من هذا الدليل هو إدارة هذا التعقيد من خلال وصف بالتفصيل ما يجب وما لا يجب فعله عند كتابة كود Go في Uber. توجد هذه القواعد للحفاظ على قاعدة الشفرة قابلة للإدارة مع السماح للمهندسين باستخدام ميزات لغة Go بشكل إنتاجي.
تم إنشاء هذا الدليل في الأصل بواسطة Prashant Varanasi و Simon Newton كطريقة لمساعدة بعض الزملاء على اللحاق باستخدام Go. على مر السنين، تم تعديله بناءً على ملاحظات الآخرين.
تصف هذه الوثيقة القواعد الأسلوبية في كود Go التي نتبعها في Uber. الكثير من هذه هي إرشادات عامة لـ Go، في حين أن البعض الآخر يستند إلى موارد خارجية:
نهدف إلى أن تكون عينات الكود دقيقة للإصدارين الفرعيين الأخيرين من إصدارات Go.
يجب أن يكون كل الكود خاليًا من الأخطاء عند تشغيله من خلال golint و go vet. نوصي بإعداد محرر النصوص الخاص بك لـ:
- تشغيل
goimportsعند الحفظ - تشغيل
golintوgo vetللتحقق من الأخطاء
يمكنك العثور على معلومات حول دعم محرر النصوص لأدوات Go هنا: https://go.dev/wiki/IDEsAndTextEditorPlugins
الإرشادات
مؤشرات للواجهات (Interfaces)
نادرًا ما تحتاج إلى مؤشر إلى واجهة. يجب عليك تمرير الواجهات كقيم - البيانات الأساسية يمكن أن تظل مؤشرًا.
الواجهة هي حقلين:
- مؤشر إلى بعض المعلومات الخاصة بالنوع. يمكنك التفكير في هذا على أنه "النوع".
- مؤشر البيانات. إذا كانت البيانات المخزنة مؤشرًا، فيتم تخزينها مباشرة. إذا كانت البيانات المخزنة قيمة، فسيتم تخزين مؤشر للقيمة.
إذا كنت تريد أن تقوم طرق الواجهة بتعديل البيانات الأساسية، فيجب عليك استخدام مؤشر.
التحقق من امتثال الواجهة
تحقق من امتثال الواجهة في وقت التجميع حيثما كان ذلك مناسبًا. وهذا يشمل:
- الأنواع المصدرة التي يجب أن تنفذ واجهات محددة كجزء من عقد API الخاص بها
- الأنواع المصدرة أو غير المصدرة التي تعد جزءًا من مجموعة من الأنواع التي تنفذ نفس الواجهة
- حالات أخرى حيث سيؤدي انتهاك واجهة إلى تعطيل المستخدمين
| سيء | جيد |
|---|---|
|
|
البيان var _ http.Handler = (*Handler)(nil) سيفشل في التجميع إذا توقف *Handler عن مطابقة واجهة http.Handler.
يجب أن يكون الجانب الأيمن من التعيين هو القيمة الصفرية للنوع المؤكد. وهذا هو nil لأنواع المؤشرات (مثل *Handler)، والشرائح، والخرائط، وهيكل فارغ لأنواع الهياكل.
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
المستقبلات والواجهات
يمكن استدعاء الدوال ذات مستقبلات القيمة على المؤشرات وكذلك القيم. الدوال ذات مستقبلات المؤشر يمكن استدعاؤها فقط على المؤشرات أو القيم التي يمكن عنونتها.
على سبيل المثال،
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
// لا يمكننا الحصول على مؤشرات للقيم المخزنة في maps، لأنها ليست
// قيم قابلة للعنونة.
sVals := map[int]S{1: {"A"}}
// يمكننا استدعاء Read على القيم المخزنة في الخريطة لأن Read
// لديها مستقبل قيمة، والذي لا يتطلب أن تكون القيمة
// قابلة للعنونة.
sVals[1].Read()
// لا يمكننا استدعاء Write على القيم المخزنة في الخريطة لأن Write
// لديها مستقبل مؤشر، ومن غير الممكن الحصول على مؤشر
// لقيمة مخزنة في خريطة.
//
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// يمكنك استدعاء كل من Read و Write إذا كانت الخريطة تخزن مؤشرات،
// لأن المؤشرات قابلة للعنونة بشكل جوهري.
sPtrs[1].Read()
sPtrs[1].Write("test")
وبالمثل، يمكن إرضاء واجهة بواسطة مؤشر، حتى إذا كانت الطريقة لها مستقبل قيمة.
type F interface {
f()
}
type S1 struct{}
func (s S1) f() {}
type S2 struct{}
func (s *S2) f() {}
s1Val := S1{}
s1Ptr := &S1{}
s2Val := S2{}
s2Ptr := &S2{}
var i F
i = s1Val
i = s1Ptr
i = s2Ptr
// ما يلي لا يُجمَّع، لأن s2Val قيمة، ولا يوجد مستقبل قيمة لـ f.
// i = s2Val
يحتوي Effective Go على مقال جيد حول المؤشرات مقابل القيم.
تعتبر قيمة Mutex الصفرية صالحة
القيمة الصفرية لـ sync.Mutex و sync.RWMutex صالحة، لذا نادرًا ما تحتاج إلى مؤشر إلى mutex.
| سيء | جيد |
|---|---|
|
|
إذا كنت تستخدم هيكلًا بواسطة مؤشر، فيجب أن يكون mutex حقلًا غير مؤشر عليه. لا تقم بتضمين mutex في الهيكل، حتى إذا كان الهيكل غير مصدر.
| سيء | جيد |
|---|---|
|
|
|
حقل |
mutex وطرقه هي تفاصيل تنفيذ لـ |
نسخ Slices و Maps عند الحدود
تحتوي Slices و Maps على مؤشرات للبيانات الأساسية، لذا كن حذرًا من السيناريوهات التي تحتاج فيها إلى نسخها.
استلام Slices و Maps
ضع في اعتبارك أن المستخدمين يمكنهم تعديل خريطة أو شريحة استلمتها كوسيطة إذا قمت بتخزين مرجع لها.
| سيء | جيد |
|---|---|
|
|
إرجاع Slices و Maps
وبالمثل، كن حذرًا من تعديلات المستخدم للخرائط أو الشرائح التي تكشف عن الحالة الداخلية.
| سيء | جيد |
|---|---|
|
|
استخدام Defer للتنظيف
استخدم defer لتنظيف الموارد مثل الملفات والأقفال.
| سيء | جيد |
|---|---|
|
|
لدى Defer نفقات عامة صغيرة جدًا ويجب تجنبها فقط إذا كنت تستطيع إثبات أن وقت تنفيذ الدالة الخاصة بك هو بترتيب النانوثانية. الفوز في القراءة من خلال استخدام defers يستحق التكلفة الضئيلة لاستخدامها. هذا صحيح بشكل خاص للطرق الأكبر التي لديها أكثر من مجرد وصول للذاكرة البسيطة، حيث تكون الحسابات الأخرى أكثر أهمية من defer.
حجم القناة (Channel) واحد أو لا شيء
يجب أن يكون للقنوات عادةً حجم واحد أو غير مخزنة. بشكل افتراضي، تكون القنوات غير مخزنة ولها حجم صفر. يجب أن يخضع أي حجم آخر لمستوى عالٍ من التدقيق. ضع في اعتبارك كيفية تحديد الحجم، وما الذي يمنع القناة من الامتلاء تحت الحمل وحظر الكتاب، وماذا يحدث عندما يحدث هذا.
| سيء | جيد |
|---|---|
|
|
بدء التعدادات (Enums) عند الرقم واحد
الطريقة القياسية لإدخال التعدادات في Go هي إعلان نوع مخصص ومجموعة const مع iota. نظرًا لأن المتغيرات لها قيمة افتراضية 0، يجب عليك عادةً بدء التعدادات الخاصة بك بقيمة غير صفرية.
| سيء | جيد |
|---|---|
|
|
هناك حالات حيث يكون استخدام القيمة الصفرية منطقيًا، على سبيل المثال عندما تكون حالة القيمة الصفرية هي السلوك الافتراضي المرغوب.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
استخدام "time" للتعامل مع الوقت
الوقت معقد. الافتراضات الخاطئة التي غالبًا ما يتم إجراؤها حول الوقت تشمل ما يلي.
- يوم واحد يحتوي على 24 ساعة
- ساعة واحدة تحتوي على 60 دقيقة
- أسبوع واحد يحتوي على 7 أيام
- سنة واحدة تحتوي على 365 يومًا
- والكثير غير ذلك
على سبيل المثال، 1 يعني أن إضافة 24 ساعة إلى لحظة زمنية لن تنتج دائمًا يومًا تقويميًا جديدًا.
لذلك، استخدم دائمًا حزمة "time" عند التعامل مع الوقت لأنها تساعد في التعامل مع هذه الافتراضات الخاطئة بطريقة أكثر أمانًا ودقة.
استخدم time.Time للحظات الزمنية
استخدم time.Time عند التعامل مع لحظات الوقت، واستخدم الطرق الموجودة في time.Time عند مقارنة الوقت أو إضافته أو طرحه.
| سيء | جيد |
|---|---|
|
|
استخدم time.Duration لفترات الوقت
استخدم time.Duration عند التعامل مع فترات الوقت.
| سيء | جيد |
|---|---|
|
|
بالعودة إلى مثال إضافة 24 ساعة إلى لحظة زمنية، تعتمد الطريقة التي نستخدمها لإضافة الوقت على القصد. إذا أردنا نفس الوقت من اليوم، ولكن في اليوم التقويمي التالي، يجب أن نستخدم Time.AddDate. ومع ذلك، إذا أردنا لحظة زمنية مضمونة تكون بعد 24 ساعة من الوقت السابق، يجب أن نستخدم Time.Add.
newDay := t.AddDate(0 /* سنوات */, 0 /* شهور */, 1 /* أيام */)
maybeNewDay := t.Add(24 * time.Hour)
استخدم time.Time و time.Duration مع الأنظمة الخارجية
استخدم time.Duration و time.Time في التفاعلات مع الأنظمة الخارجية عندما يكون ذلك ممكنًا. على سبيل المثال:
- خيارات سطر الأوامر: يدعم
flagtime.Durationعبرtime.ParseDuration - JSON: يدعم
encoding/jsonترميزtime.Timeكسلسلة RFC 3339 عبر طريقةUnmarshalJSON - SQL: يدعم
database/sqlتحويل أعمدةDATETIMEأوTIMESTAMPإلىtime.Timeوالعكس إذا كان برنامج التشغيل الأساسي يدعم ذلك - YAML: يدعم
gopkg.in/yaml.v2time.Timeكسلسلة RFC 3339، وtime.Durationعبرtime.ParseDuration.
عندما لا يكون من الممكن استخدام time.Duration في هذه التفاعلات، استخدم int أو float64 وقم بتضمين الوحدة في اسم الحقل.
على سبيل المثال، نظرًا لأن encoding/json لا يدعم time.Duration، يتم تضمين الوحدة في اسم الحقل.
| سيء | جيد |
|---|---|
|
|
عندما لا يكون من الممكن استخدام time.Time في هذه التفاعلات، ما لم يتم الاتفاق على بديل، استخدم string وقم بتنسيق الطوابع الزمنية كما هو محدد في RFC 3339. يتم استخدام هذا التنسيق بشكل افتراضي بواسطة Time.UnmarshalText وهو متاح للاستخدام في Time.Format و time.Parse عبر time.RFC3339.
على الرغم من أن هذا لا يميل إلى أن يكون مشكلة في الممارسة العملية، ضع في اعتبارك أن حزمة "time" لا تدعم تحليل الطوابع الزمنية مع ثواني كبيسة (8728)، ولا تأخذ في الاعتبار الثواني الكبيسة في الحسابات (15190). إذا قارنت بين لحظتين من الوقت، فإن الفرق لن يشمل الثواني الكبيسة التي قد تكون حدثت بين هاتين اللحظتين.
الأخطاء
أنواع الأخطاء
هناك عدة خيارات لإعلان الأخطاء. فكر في ما يلي قبل اختيار الخيار الأنسب لحالة الاستخدام الخاصة بك.
- هل يحتاج المستدعي إلى مطابقة الخطأ حتى يتمكن من معالجته؟
إذا كان الجواب نعم، فيجب علينا دعم الدوال
errors.Isأوerrors.Asمن خلال إعلان متغير خطأ على المستوى الأعلى أو نوع مخصص. - هل رسالة الخطأ عبارة عن سلسلة ثابتة،
أم أنها سلسلة ديناميكية تتطلب معلومات سياقية؟
بالنسبة للأول، يمكننا استخدام
errors.New، ولكن بالنسبة للأخير يجب استخدامfmt.Errorfأو نوع خطأ مخصص. - هل نقوم بنشر خطأ جديد تم إرجاعه بواسطة دالة تابعة؟ إذا كان الأمر كذلك، راجع قسم تغليف الأخطاء.
| مطابقة الخطأ؟ | رسالة الخطأ | التوجيه |
|---|---|---|
| لا | ثابتة | errors.New |
| لا | ديناميكية | fmt.Errorf |
| نعم | ثابتة | متغير var على المستوى الأعلى مع errors.New |
| نعم | ديناميكية | نوع error مخصص |
على سبيل المثال،
استخدم errors.New لخطأ بسلسلة ثابتة.
قم بتصدير هذا الخطأ كمتغير لدعم مطابقته مع errors.Is
إذا كان المستدعي بحاجة إلى مطابقة هذا الخطأ ومعالجته.
| بدون مطابقة خطأ | مع مطابقة خطأ |
|---|---|
|
|
بالنسبة للخطأ ذي السلسلة الديناميكية،
استخدم fmt.Errorf إذا لم يحتج المستدعي إلى مطابقته،
ونوع error مخصص إذا احتاج المستدعي إلى مطابقته.
| بدون مطابقة خطأ | مع مطابقة خطأ |
|---|---|
|
|
لاحظ أنه إذا قمت بتصدير متغيرات أو أنواع الخطأ من حزمة، فستصبح جزءًا من واجهة API العامة للحزمة.
تغليف الأخطاء
هناك ثلاثة خيارات رئيسية لنشر الأخطاء إذا فشل استدعاء:
- إرجاع الخطأ الأصلي كما هو
- إضافة سياق باستخدام
fmt.Errorfو الفعل%w - إضافة سياق باستخدام
fmt.Errorfو الفعل%v
أعد الخطأ الأصلي كما هو إذا لم يكن هناك سياق إضافي لإضافته. هذا يحافظ على نوع الخطأ الأصلي والرسالة. هذا مناسب جيدًا للحالات التي تحتوي فيها رسالة الخطأ الأساسية على معلومات كافية لتتبع مكان ظهورها.
وإلا، أضف سياقًا إلى رسالة الخطأ حيثما أمكن حتى تتمكن بدلاً من خطأ غامض مثل "الاتصال مرفوض"، من الحصول على أخطاء أكثر فائدة مثل "استدعاء الخدمة foo: الاتصال مرفوض".
استخدم fmt.Errorf لإضافة سياق إلى أخطائك،
واختر بين الأفعال %w أو %v
بناءً على ما إذا كان المستدعي يجب أن يكون قادرًا على
مطابقة واستخراج السبب الأساسي.
- استخدم
%wإذا كان من المفترض أن يكون للمستدعي حق الوصول إلى الخطأ الأساسي. هذا هو الإعداد الافتراضي الجيد لمعظم الأخطاء المغلفة، ولكن كن على دراية بأن المستدعين قد يبدأون في الاعتماد على هذا السلوك. لذلك بالنسبة للحالات التي يكون فيها الخطأ المغلف هو متغيرvarمعروف أو نوع، قم بتوثيقه واختباره كجزء من عقد الدالة الخاصة بك. - استخدم
%vلحجب الخطأ الأساسي. لن يتمكن المستدعون من مطابقته، ولكن يمكنك التبديل إلى%wفي المستقبل إذا لزم الأمر.
عند إضافة سياق للأخطاء المرتجعة، اجعل السياق موجزًا من خلال تجنب عبارات مثل "فشل في"، والتي تذكر الواضح وتتراكم مع تصاعد الخطأ من خلال المكدس:
| سيء | جيد |
|---|---|
|
|
|
|
ومع ذلك، بمجرد إرسال الخطأ إلى نظام آخر، يجب أن تكون الرسالة واضحة أنها خطأ (مثل علامة err أو بادئة "Failed" في السجلات).
انظر أيضًا لا تتحقق فقط من الأخطاء، تعامل معها بأناقة.
تسمية الأخطاء
بالنسبة لقيم الخطأ المخزنة كمتغيرات عالمية،
استخدم البادئة Err أو err اعتمادًا على ما إذا كانت مُصدَّرة.
هذا التوجيه يحل محل اضافة بادئة _ للمتغيرات العامة غير المصدرة.
var (
// الخطأين التاليين مُصدَّران
// حتى يتمكن مستخدمو هذه الحزمة من مطابقتهما
// مع errors.Is.
ErrBrokenLink = errors.New("link is broken")
ErrCouldNotOpen = errors.New("could not open")
// هذا الخطأ غير مُصدَّر لأننا
// لا نريد جعله جزءًا من واجهة API العامة.
// قد نستخدمه داخل الحزمة
// مع errors.Is.
errNotFound = errors.New("not found")
)
بالنسبة لأنواع الخطأ المخصصة، استخدم اللاحقة Error بدلاً من ذلك.
// وبالمثل، يتم تصدير هذا الخطأ
// حتى يتمكن مستخدمو هذه الحزمة من مطابقته
// مع errors.As.
type NotFoundError struct {
File string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("file %q not found", e.File)
}
// وهذا الخطأ غير مصدر لأننا
// لا نريد جعله جزءًا من واجهة API العامة.
// يمكننا استخدامه داخل الحزمة
// مع errors.As.
type resolveError struct {
Path string
}
func (e *resolveError) Error() string {
return fmt.Sprintf("resolve %q", e.Path)
}
التعامل مع الأخطاء مرة واحدة
عندما يتلقى مستدعٍ خطأً من مستدعى، يمكنه التعامل معه بمجموعة متنوعة من الطرق المختلفة اعتمادًا على ما يعرفه عن الخطأ.
تشمل هذه، على سبيل المثال لا الحصر:
- إذا كان عقد المستدعى يحدد أخطاء محددة،
مطابقة الخطأ مع
errors.Isأوerrors.Asوالتعامل مع الفروع بشكل مختلف - إذا كان الخطأ قابلاً للاسترداد، تسجيل الخطأ والتدهور بأناقة
- إذا كان الخطأ يمثل حالة فشل خاصة بالمجال، إرجاع خطأ محدد جيدًا
- إعادة الخطأ، سواء مغلفًا أو حرفيًا
بغض النظر عن كيفية تعامل المستدعي مع الخطأ، يجب عليه عادةً التعامل مع كل خطأ مرة واحدة فقط. لا ينبغي للمستدعي، على سبيل المثال، تسجيل الخطأ ثم إعادته، لأن مستدعييه قد يتعاملون مع الخطأ أيضًا.
على سبيل المثال، ضع في اعتبار الحالات التالية:
| الوصف | الكود |
|---|---|
|
سيء: سجّل الخطأ وأعده من المحتمل أن يتخذ المستدعون في أعلى المكدس إجراءً مماثلاً مع الخطأ. القيام بذلك يتسبب في الكثير من الضجيج في سجلات التطبيق مقابل قيمة قليلة. |
|
|
جيد: غلّف الخطأ وأعده سيتعامل المستدعون في أعلى المكدس مع الخطأ.
استخدام |
|
|
جيد: سجّل الخطأ وتدهور بأناقة إذا لم تكن العملية ضرورية بشكل صارم، يمكننا توفير تجربة متدهورة ولكن غير منقطعة من خلال التعافي منها. |
|
|
جيد: طابق الخطأ وتدهور بأناقة إذا كان المستدعى يحدد خطأ معين في عقده، وكان الفشل قابلاً للاسترداد، طابق على حالة الخطأ هذه وتدهور بأناقة. بالنسبة لجميع الحالات الأخرى، غلّف الخطأ وأعده. سيتعامل المستدعون في أعلى المكدس مع الأخطاء الأخرى. |
|
التعامل مع فشل تأكيد النوع
نموذج العودة بقيمة واحدة من تأكيد النوع سوف يتسبب في حدوث panic عند نوع غير صحيح. لذلك، استخدم دائمًا عبارة "comma ok".
| سيء | جيد |
|---|---|
|
|
لا تستخدم Panic
يجب على الكود الذي يعمل في بيئة الإنتاج تجنب استخدام panic. تعتبر panic مصدرًا رئيسيًا لـ الإخفاقات المتتالية. إذا حدث خطأ، يجب على الدالة أن تعيد الخطأ وتسمح لمن استدعاها أن يقرر كيفية التعامل معه.
| سيء | جيد |
|---|---|
|
|
Panic/recover ليست استراتيجية للتعامل مع الأخطاء. يجب أن يحدث panic في البرنامج فقط عندما يحدث شيء لا يمكن إصلاحه مثل إلغاء مرجع nil. الاستثناء لهذه القاعدة هو تهيئة البرنامج: الأشياء السيئة عند بدء تشغيل البرنامج التي يجب أن توقف البرنامج قد تسبب panic.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
حتى في الاختبارات، يفضل استخدام t.Fatal أو t.FailNow بدلاً من panic لضمان تحديد الاختبار كفاشل.
| سيء | جيد |
|---|---|
|
|
استخدم go.uber.org/atomic
العمليات الذرية باستخدام حزمة sync/atomic تعمل على الأنواع الأساسية (int32، int64، إلخ) مما يجعل من السهل نسيان استخدام العملية الذرية لقراءة أو تعديل المتغيرات.
go.uber.org/atomic يضيف أمان الأنواع لهذه العمليات عن طريق إخفاء النوع الأساسي. بالإضافة إلى ذلك، يتضمن نوع atomic.Bool المريح.
| سيء | جيد |
|---|---|
|
|
تجنب المتغيرات العامة القابلة للتغيير
تجنب تغيير المتغيرات العامة، واستخدم حقن التبعيات بدلاً من ذلك. هذا ينطبق على مؤشرات الدوال وكذلك على أنواع القيم الأخرى.
| سيء | جيد |
|---|---|
|
|
|
|
تجنب تضمين الأنواع في الهياكل العامة
هذه الأنواع المضمنة تسرب تفاصيل التنفيذ، وتعيق تطور النوع، وتخفي التوثيق.
بافتراض أنك قمت بتنفيذ مجموعة متنوعة من أنواع القوائم باستخدام AbstractList مشترك، تجنب تضمين AbstractList في تنفيذاتك الملموسة للقائمة.
بدلاً من ذلك، اكتب يدويًا فقط الطرق لقائمتك الملموسة التي ستفوض إلى القائمة المجردة.
type AbstractList struct {}
// Add adds an entity to the list.
func (l *AbstractList) Add(e Entity) {
// ...
}
// Remove removes an entity from the list.
func (l *AbstractList) Remove(e Entity) {
// ...
}
| سيء | جيد |
|---|---|
|
|
تسمح Go بـ تضمين النوع كحل وسط بين الوراثة والتركيب. النوع الخارجي يحصل على نسخ ضمنية من طرق النوع المضمن. هذه الطرق، بشكل افتراضي، تفوض إلى نفس الطريقة للمثيل المضمن.
يكتسب الهيكل أيضًا حقلاً بنفس اسم النوع. لذا، إذا كان النوع المضمن عامًا، فإن الحقل يكون عامًا. للحفاظ على التوافق الخلفي، يجب أن تحتفظ كل نسخة مستقبلية من النوع الخارجي بالنوع المضمن.
نادرًا ما يكون النوع المضمن ضروريًا. إنه وسيلة مريحة تساعدك على تجنب كتابة طرق التفويض المملة.
حتى تضمين واجهة AbstractList متوافقة، بدلاً من الهيكل، سيوفر للمطور مرونة أكبر للتغيير في المستقبل، ولكنه سيظل يُسرب تفاصيل أن القوائم الملموسة تستخدم تنفيذًا مجردًا.
| سيء | جيد |
|---|---|
|
|
سواء مع هيكل مضمن أو واجهة مضمنة، يضع النوع المضمن قيودًا على تطور النوع.
- إضافة طرق إلى واجهة مضمنة هو تغيير كاسر.
- إزالة طرق من هيكل مضمن هو تغيير كاسر.
- إزالة النوع المضمن هو تغيير كاسر.
- استبدال النوع المضمن، حتى ببديل يلبي نفس الواجهة، هو تغيير كاسر.
على الرغم من أن كتابة طرق التفويض هذه أمر مُمل، إلا أن الجهد الإضافي يخفي تفاصيل التنفيذ، ويترك المزيد من الفرص للتغيير، ويزيل أيضًا الاتجاه غير المباشر لاكتشاف واجهة List الكاملة في التوثيق.
تجنب استخدام الأسماء المدمجة
مواصفات لغة Go تحدد العديد من المعرفات المعلنة مسبقًا المدمجة التي لا ينبغي استخدامها كأسماء داخل برامج Go.
اعتمادًا على السياق، فإن إعادة استخدام هذه المعرفات كأسماء إما ستظل الأصل داخل النطاق المعجمي الحالي (وأي نطاقات متداخلة) أو تجعل الكود المتأثر مربكًا. في أفضل الحالات، سيشتكي المُجمِّع؛ وفي أسوأ الحالات، قد يقدم مثل هذا الكود أخطاء كامنة يصعب اكتشافها.
| سيء | جيد |
|---|---|
|
|
|
|
لاحظ أن المُجمِّع لن ينتج أخطاء عند استخدام المعرفات المعلنة مسبقًا، ولكن الأدوات مثل go vet يجب أن تشير بشكل صحيح إلى هذه الحالات وغيرها من حالات التظليل.
تجنب init()
تجنب init() حيثما أمكن. عندما يكون init() لا مفر منه أو مرغوبًا فيه، يجب أن يحاول الكود:
- أن يكون حتميًا تمامًا، بغض النظر عن بيئة البرنامج أو الاستدعاء.
- تجنب الاعتماد على ترتيب أو آثار جانبية لوظائف
init()أخرى. بينما ترتيبinit()معروف جيدًا، يمكن أن يتغير الكود، وبالتالي فإن العلاقات بين وظائفinit()يمكن أن تجعل الكود هشًا وعرضة للأخطاء. - تجنب الوصول إلى أو التلاعب بالحالة العالمية أو البيئية، مثل معلومات الجهاز، متغيرات البيئة، دليل العمل، وسيطات/مدخلات البرنامج، إلخ.
- تجنب I/O، بما في ذلك نظام الملفات والشبكة واستدعاءات النظام.
الكود الذي لا يستطيع تلبية هذه المتطلبات من المحتمل أن ينتمي كمساعد ليتم استدعاؤه كجزء من main() (أو في مكان آخر في دورة حياة البرنامج)، أو يتم كتابته كجزء من main() نفسه. على وجه الخصوص، يجب أن تولي المكتبات التي يُقصد استخدامها من قبل برامج أخرى عناية خاصة لتكون حتمية تمامًا وعدم تنفيذ "سحر init".
| سيء | جيد |
|---|---|
|
|
|
|
مع مراعاة ما سبق، بعض الحالات التي قد يكون فيها init() مفضلاً أو ضروريًا قد تشمل:
- التعبيرات المعقدة التي لا يمكن تمثيلها كتعيينات مفردة.
- الخطافات القابلة للتوصيل، مثل لهجات
database/sql، سجلات نوع الترميز، وما إلى ذلك. - التحسينات لـ Google Cloud Functions وأشكال أخرى من الحساب المسبق الحتمي.
الخروج في الدالة Main
تستخدم برامج Go os.Exit أو log.Fatal* للخروج فورًا. (Panicking ليست طريقة جيدة للخروج من البرامج، لا تستخدم panic.)
استدع os.Exit أو log.Fatal* فقط في main(). يجب أن تعيد جميع الدوال الأخرى أخطاء للإشارة إلى الفشل.
| سيء | جيد |
|---|---|
|
|
المنطق: البرامج التي تحتوي على وظائف متعددة تخرج تقدم بعض المشكلات:
- تدفق التحكم غير الواضح: أي وظيفة يمكنها إنهاء البرنامج لذا يصبح من الصعب التفكير في تدفق التحكم.
- صعب الاختبار: الوظيفة التي تنهي البرنامج ستنهي أيضًا الاختبار الذي يستدعيها. هذا يجعل الوظيفة صعبة الاختبار ويقدم مخاطر تخطي اختبارات أخرى لم يتم تشغيلها بعد بواسطة
go test. - تخطي التنظيف: عندما تخرج وظيفة من البرنامج، فإنها تتخطى استدعاءات الوظيفة المدرجة في قائمة الانتظار مع بيانات
defer. هذا يضيف مخاطر تخطي مهام التنظيف المهمة.
الخروج مرة واحدة
إذا أمكن، فضل استدعاء os.Exit أو log.Fatal مرة واحدة على الأكثر في main(). إذا كانت هناك سيناريوهات خطأ متعددة توقف تنفيذ البرنامج، ضع هذا المنطق تحت وظيفة منفصلة وأعد الأخطاء منها.
هذا له تأثير تقصير وظيفة main() ووضع جميع منطق الأعمال الرئيسي في وظيفة منفصلة قابلة للاختبار.
| سيء | جيد |
|---|---|
|
|
المثال أعلاه يستخدم log.Fatal، لكن التوجيه ينطبق أيضًا على os.Exit أو أي كود مكتبة يستدعي os.Exit.
func main() {
if err := run(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
يمكنك تغيير توقيع run() ليناسب احتياجاتك. على سبيل المثال، إذا كان برنامجك يجب أن يخرج برموز خروج محددة للأخطاء، فقد يعيد run() رمز الخروج بدلاً من الخطأ. هذا يسمح أيضًا لاختبارات الوحدة بالتحقق من هذا السلوك مباشرة.
func main() {
os.Exit(run(args))
}
func run() (exitCode int) {
// ...
}
بشكل أعم، لاحظ أن الوظيفة run() المستخدمة في هذه الأمثلة ليست مقصودة لتكون وصفية. هناك مرونة في الاسم والتوقيع وإعداد وظيفة run(). من بين أمور أخرى، يمكنك:
- قبول وسيطات سطر الأوامر غير المحللة (مثل
run(os.Args[1:])) - تحليل وسيطات سطر الأوامر في
main()وتمريرها إلىrun - استخدام نوع خطأ مخصص لنقل رمز الخروج مرة أخرى إلى
main() - وضع منطق الأعمال في طبقة مختلفة من التجريد من
package main
يتطلب هذا التوجيه فقط أن يكون هناك مكان واحد في main() مسؤول عن الخروج الفعلي من العملية.
استخدام علامات الحقل في الهياكل المحولة
أي حقل هيكل يتم تحويله إلى JSON أو YAML أو تنسيقات أخرى تدعم تسمية الحقول المستندة إلى العلامات يجب أن يكون مزودًا بالعلامة ذات الصلة.
| سيء | جيد |
|---|---|
|
|
المنطق: الشكل المتسلسل للهيكل هو عقد بين أنظمة مختلفة. التغييرات في بنية الشكل المتسلسل - بما في ذلك أسماء الحقول - تكسر هذا العقد. تحديد أسماء الحقول داخل العلامات يجعل العقد صريحًا، ويحمي من كسر العقد عن طريق الخطأ من خلال إعادة التصميم أو إعادة تسمية الحقول.
لا تطلق goroutines دون متابعة
الـ Goroutines خفيفة الوزن، لكنها ليست مجانية: على الأقل، تستهلك ذاكرة للـ stack الخاص بها و CPU للجدولة. بينما هذه التكاليف صغيرة للاستخدامات النموذجية للـ goroutines، يمكن أن تسبب مشاكل أداء كبيرة عندما يتم إطلاقها بأعداد كبيرة دون التحكم في دورة حياتها. يمكن للـ goroutines ذات دورة الحياة غير المُدارة أن تسبب أيضًا مشاكل أخرى مثل منع الكائنات غير المستخدمة من جمع القمامة والاحتفاظ بالموارد التي لم تعد مستخدمة.
لذلك، لا تسرب goroutines في كود الإنتاج. استخدم go.uber.org/goleak لاختبار تسريبات goroutine داخل الحزم التي قد تطلق goroutines.
بشكل عام، كل goroutine:
- يجب أن يكون لها وقت متوقع ستتوقف فيه عن التشغيل؛ أو
- يجب أن يكون هناك طريقة للإشارة إلى الـ goroutine بأنه يجب أن تتوقف
في كلتا الحالتين، يجب أن تكون هناك طريقة للكود لحظر وانتظار انتهاء الـ goroutine.
على سبيل المثال:
| سيء | جيد |
|---|---|
|
|
|
لا توجد طريقة لإيقاف هذه الـ goroutine. ستعمل حتى يخرج التطبيق. |
يمكن إيقاف هذه الـ goroutine بـ |
انتظر خروج goroutines
بالنظر إلى goroutine تم إطلاقها بواسطة النظام، يجب أن تكون هناك طريقة لانتظار خروج الـ goroutine. هناك طريقتان شائعتان للقيام بذلك:
-
استخدام
sync.WaitGroup. افعل هذا إذا كانت هناك عدة goroutines تريد الانتظار لهاvar wg sync.WaitGroup for i := 0; i < N; i++ { wg.Add(1) go func() { defer wg.Done() // ... }() } // للانتظار حتى تنتهي جميعها: wg.Wait() -
أضف
chan struct{}آخر تغلقه الـ goroutine عندما تنتهي. افعل هذا إذا كانت هناك goroutine واحدة فقط.done := make(chan struct{}) go func() { defer close(done) // ... }() // للانتظار حتى تنتهي الـ goroutine: <-done
لا تضع goroutines في init()
يجب ألا تطلق وظائف init() goroutines.
انظر أيضًا تجنب init().
إذا احتاجت حزمة إلى goroutine خلفية،
يجب أن تعرض كائنًا مسؤولًا عن إدارة دورة حياة goroutine.
يجب أن يوفر الكائن طريقة (Close، Stop، Shutdown، إلخ)
تشير إلى الـ goroutine الخلفية بالتوقف، وتنتظر خروجها.
| سيء | جيد |
|---|---|
|
|
|
تطلق goroutine خلفية بشكل غير مشروط عندما يستورد المستخدم هذه الحزمة. ليس لدى المستخدم أي تحكم في الـ goroutine أو وسيلة لإيقافها. |
تطلق العامل فقط إذا طلب المستخدم ذلك. توفر وسيلة لإيقاف العامل بحيث يمكن للمستخدم تحرير الموارد المستخدمة بواسطة العامل. لاحظ أنه يجب عليك استخدام |
الأداء
تنطبق إرشادات الأداء المحددة فقط على المسار الساخن (hot path).
تفضيل strconv على fmt
عند تحويل الأنواع البدائية من/إلى سلاسل نصية، strconv أسرع من
fmt.
| سيء | جيد |
|---|---|
|
|
|
|
تجنب التحويلات المتكررة من النص إلى البايت
لا تقم بإنشاء شرائح بايت من سلسلة ثابتة بشكل متكرر. بدلاً من ذلك، قم بالتحويل مرة واحدة والتقط النتيجة.
| سيء | جيد |
|---|---|
|
|
|
|
تفضيل تحديد سعة الحاويات
حدد سعة الحاوية حيثما أمكن من أجل تخصيص ذاكرة للحاوية مقدمًا. هذا يقلل من التخصيصات اللاحقة (عن طريق نسخ وتغيير حجم الحاوية) مع إضافة العناصر.
تحديد تلميحات سعة Map
حيثما أمكن، قدم تلميحات السعة عند تهيئة Maps باستخدام make().
make(map[T1]T2, hint)
توفير تلميح سعة لـ make() يحاول تحجيم Map بشكل صحيح في وقت التهيئة، مما يقلل الحاجة إلى تنمية Map والتخصيصات مع إضافة عناصر إلى Map.
لاحظ أنه، على عكس الشرائح، لا تضمن تلميحات سعة Map التخصيص الكامل والاستباقي، ولكنها تستخدم لتقريب عدد دلاء خريطة التجزئة المطلوبة. ونتيجة لذلك، قد تظل التخصيصات تحدث عند إضافة عناصر إلى Map، حتى تصل إلى السعة المحددة.
| سيء | جيد |
|---|---|
|
|
|
يتم إنشاء |
يتم إنشاء |
تحديد سعة Slice
حيثما أمكن، قدم تلميحات السعة عند تهيئة الشرائح باستخدام make()، خاصة عند الإلحاق.
make([]T, length, capacity)
على عكس Maps، سعة الشريحة ليست تلميحًا: سيخصص المُجمِّع ذاكرة كافية لسعة الشريحة كما هو مقدم إلى make()، مما يعني أن عمليات append() اللاحقة لن تتكبد أي تخصيصات (حتى يتطابق طول الشريحة مع السعة، وبعد ذلك ستتطلب أي عمليات إلحاق تغيير حجم لاحتواء عناصر إضافية).
| سيء | جيد |
|---|---|
|
|
|
|
الأسلوب
تجنب الأسطر الطويلة جدًا
تجنب أسطر الكود التي تتطلب من القراء التمرير أفقيًا أو إدارة رؤوسهم كثيرًا.
نوصي بحد طول سطر ليّن يبلغ 99 حرفًا. يجب على المؤلفين أن يهدفوا إلى لف الأسطر قبل الوصول إلى هذا الحد، ولكنه ليس حدًا صارمًا. يُسمح للكود بتجاوز هذا الحد.
كن متسقًا
يمكن تقييم بعض الإرشادات المحددة في هذا المستند بشكل موضوعي؛ البعض الآخر ظرفي أو سياقي أو ذاتي.
فوق كل شيء آخر، كن متسقًا.
الكود المتسق أسهل في الصيانة، وأسهل في التبرير، ويتطلب تكلفة معرفية أقل، وأسهل في الترحيل أو التحديث مع ظهور اتفاقيات جديدة أو إصلاح فئات من الأخطاء.
وعلى العكس من ذلك، فإن وجود أساليب متباينة أو متعارضة متعددة ضمن قاعدة شفرة واحدة يتسبب في تكاليف صيانة، وعدم يقين، وتنافر معرفي، وكل ذلك يمكن أن يساهم بشكل مباشر في سرعة أقل، ومراجعات كود مؤلمة، وأخطاء.
عند تطبيق هذه الإرشادات على قاعدة شفرة، يُوصى بإجراء التغييرات على مستوى الحزمة (أو أكبر): التطبيق على مستوى أقل من الحزمة ينتهك المخاوف المذكورة أعلاه من خلال إدخال أنماط متعددة في نفس الكود.
تجميع التصريحات المتشابهة
تدعم Go تجميع التصريحات المتشابهة.
| سيء | جيد |
|---|---|
|
|
هذا ينطبق أيضًا على الثوابت والمتغيرات وتعريفات الأنواع.
| سيء | جيد |
|---|---|
|
|
جمّع فقط التصريحات المتعلقة ببعضها. لا تجمّع التصريحات التي لا علاقة لها ببعضها.
| سيء | جيد |
|---|---|
|
|
المجموعات غير محدودة في الأماكن التي يمكن استخدامها فيها. على سبيل المثال، يمكنك استخدامها داخل الدوال.
| سيء | جيد |
|---|---|
|
|
استثناء: يجب تجميع تعريفات المتغيرات، خاصة داخل الدوال، معًا إذا تم تعريفها بجوار متغيرات أخرى. افعل هذا للمتغيرات المعلنة معًا حتى لو كانت غير مرتبطة.
| سيء | جيد |
|---|---|
|
|
ترتيب مجموعات الاستيراد
يجب أن تكون هناك مجموعتان للاستيراد:
- المكتبة القياسية
- كل شيء آخر
هذا هو التجميع المطبق بواسطة goimports بشكل افتراضي.
| سيء | جيد |
|---|---|
|
|
أسماء الحزم
عند تسمية الحزم، اختر اسمًا يكون:
- كل الحروف صغيرة. لا حروف كبيرة أو شرطات سفلية.
- لا يحتاج إلى إعادة تسمية باستخدام الاستيرادات المسماة في معظم مواقع الاستدعاء.
- قصير وموجز. تذكر أن الاسم يتم تحديده بالكامل في كل موقع استدعاء.
- ليس بصيغة الجمع. على سبيل المثال،
net/url، وليسnet/urls. - ليس "common" أو "util" أو "shared" أو "lib". هذه أسماء سيئة وغير مفيدة.
انظر أيضًا Package Names و Style guideline for Go packages.
أسماء الدوال
نتبع اتفاقية مجتمع Go في استخدام MixedCaps لأسماء الدوال. يتم استثناء وظائف الاختبار، التي قد تحتوي على شرطات سفلية لغرض تجميع حالات الاختبار ذات الصلة، مثل
TestMyFunction_WhatIsBeingTested.
استخدام الأسماء المستعارة في الاستيراد
يجب استخدام التسمية المستعارة للاستيراد إذا كان اسم الحزمة لا يتطابق مع العنصر الأخير من مسار الاستيراد.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
في جميع السيناريوهات الأخرى، يجب تجنب الأسماء المستعارة للاستيراد ما لم يكن هناك تعارض مباشر بين الاستيرادات.
| سيء | جيد |
|---|---|
|
|
تجميع وترتيب الدوال
- يجب ترتيب الدوال بترتيب الاستدعاء التقريبي.
- يجب تجميع الدوال في ملف حسب المستقبل.
لذلك، يجب أن تظهر الدوال المصدّرة أولاً في الملف، بعد تعريفات struct و const و var.
قد يظهر newXYZ()/NewXYZ() بعد تعريف النوع، ولكن قبل باقي الطرق على المستقبل.
بما أن الدوال مجمعة حسب المستقبل، يجب أن تظهر دوال المساعدة البسيطة نحو نهاية الملف.
| سيء | جيد |
|---|---|
|
|
تقليل التداخل
يجب أن يقلل الكود من التداخل حيثما أمكن من خلال التعامل مع حالات الخطأ/الشروط الخاصة أولاً والعودة مبكرًا أو مواصلة الحلقة. قلل من كمية الكود المتداخل على مستويات متعددة.
| سيء | جيد |
|---|---|
|
|
تجنب استخدام Else غير الضروري
إذا تم تعيين متغير في كلا فرعي if، يمكن استبداله بـ if واحد.
| سيء | جيد |
|---|---|
|
|
تصريحات المتغيرات على المستوى العالي
على المستوى العالي، استخدم الكلمة الأساسية القياسية var. لا تحدد النوع، إلا إذا كان ليس نفس نوع التعبير.
| سيء | جيد |
|---|---|
|
|
حدد النوع إذا كان نوع التعبير لا يتطابق مع النوع المطلوب بالضبط.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F يعيد كائن من نوع myError ولكننا نريد error.
اضافة بادئة _ للمتغيرات العامة غير المصدرة
أضف بادئة _ إلى vars و consts العالمية غير المصدرة لتوضيح متى يتم استخدامها أنها رموز عالمية.
المنطق: المتغيرات والثوابت على المستوى العالي لها نطاق الحزمة. استخدام اسم عام يجعل من السهل استخدام القيمة الخاطئة عن طريق الخطأ في ملف مختلف.
| سيء | جيد |
|---|---|
|
|
استثناء: قيم الأخطاء غير المصدرة يمكن أن تستخدم البادئة err بدون الشرطة السفلية.
انظر تسمية الأخطاء.
التضمين في الهياكل
يجب أن تكون الأنواع المضمنة في أعلى قائمة حقول الهيكل، ويجب أن يكون هناك سطر فارغ يفصل الحقول المضمنة عن الحقول العادية.
| سيء | جيد |
|---|---|
|
|
يجب أن يوفر التضمين فائدة ملموسة، مثل إضافة أو زيادة الوظائف بطريقة مناسبة دلالياً. يجب أن يقوم بذلك مع عدم وجود أي تأثيرات سلبية تواجه المستخدم (انظر أيضًا: تجنب تضمين الأنواع في الهياكل العامة).
استثناء: لا ينبغي تضمين Mutexes، حتى في الأنواع غير المصدرة. انظر أيضًا: تعتبر قيمة Mutex الصفرية صالحة.
لا ينبغي أن يكون التضمين:
- تجميليًا بحتًا أو موجهًا للراحة.
- يجعل الأنواع الخارجية أكثر صعوبة في الإنشاء أو الاستخدام.
- يؤثر على قيم الصفر للأنواع الخارجية. إذا كان للنوع الخارجي قيمة صفرية مفيدة، فيجب أن يظل له قيمة صفرية مفيدة بعد تضمين النوع الداخلي.
- يكشف عن دوال أو حقول غير ذات صلة من النوع الخارجي كتأثير جانبي لتضمين النوع الداخلي.
- يكشف عن أنواع غير مصدرة.
- يؤثر على دلالات نسخ الأنواع الخارجية.
- يغير API النوع الخارجي أو دلالات النوع.
- يضمن شكلًا غير أساسي من النوع الداخلي.
- يكشف عن تفاصيل تنفيذ النوع الخارجي.
- يسمح للمستخدمين بمراقبة أو التحكم في الأجزاء الداخلية للنوع.
- يغير السلوك العام للدوال الداخلية من خلال التفاف بطريقة قد تفاجئ المستخدمين بشكل معقول.
ببساطة، قم بالتضمين بوعي وتعمد. اختبار جيد هو، "هل سيتم إضافة كل هذه الطرق / الحقول الداخلية المصدرة مباشرة إلى النوع الخارجي"؛ إذا كانت الإجابة "بعض" أو "لا"، فلا تقم بتضمين النوع الداخلي - استخدم حقلًا بدلاً من ذلك.
| سيء | جيد |
|---|---|
|
|
|
|
|
|
تصريحات المتغيرات المحلية
يجب استخدام تصريحات المتغيرات القصيرة (:=) إذا تم تعيين متغير إلى قيمة ما بشكل صريح.
| سيء | جيد |
|---|---|
|
|
ومع ذلك، هناك حالات تكون فيها القيمة الافتراضية أوضح عند استخدام كلمة var الرئيسية. إعلان Slices فارغة، على سبيل المثال.
| سيء | جيد |
|---|---|
|
|
nil هي قيمة صالحة لـ slice
nil هي قيمة صالحة لـ slice بطول 0. هذا يعني أنه،
-
لا يجب عليك إرجاع slice بطول صفر بشكل صريح. قم بإرجاع
nilبدلاً من ذلك.سيء جيد if x == "" { returnint{} }if x == "" { return nil } -
للتحقق مما إذا كان slice فارغًا، استخدم دائمًا
len(s) == 0. لا تتحقق منnil.سيء جيد func isEmpty(sstring) bool { return s == nil }func isEmpty(sstring) bool { return len(s) == 0 } -
يمكن استخدام القيمة الصفرية (slice معلن باستخدام
var) على الفور دونmake().سيء جيد nums :=int{} // أو، nums := make(int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }var numsint if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
تذكر أنه على الرغم من أنها slice صالحة، إلا أن slice nil لا تعادل slice مخصصة بطول 0 - أحدهما nil والآخر ليس كذلك - وقد يتم التعامل مع الاثنين بشكل مختلف في مواقف مختلفة (مثل التسلسل).
تقليل نطاق المتغيرات
حيثما أمكن، قلل من نطاق المتغيرات والثوابت. لا تقلل النطاق إذا كان يتعارض مع تقليل التداخل.
| سيء | جيد |
|---|---|
|
|
إذا كنت بحاجة إلى نتيجة استدعاء دالة خارج if، فلا يجب عليك محاولة تقليل النطاق.
| سيء | جيد |
|---|---|
|
|
لا تحتاج الثوابت إلى أن تكون عامة إلا إذا تم استخدامها في دوال أو ملفات متعددة أو كانت جزءًا من عقد خارجي للحزمة.
| سيء | جيد |
|---|---|
|
|
تجنب المعاملات المجردة
يمكن أن تضر المعاملات المجردة في استدعاءات الدوال بقابلية القراءة. أضف تعليقات على غرار C (/* ... */) لأسماء المعاملات عندما لا يكون معناها واضحًا.
| سيء | جيد |
|---|---|
|
|
الأفضل من ذلك، استبدل أنواع bool المجردة بأنواع مخصصة للحصول على رمز أكثر قابلية للقراءة وآمن من النوع. يسمح هذا بأكثر من حالتين فقط (صحيح / خطأ) لتلك المعلمة في المستقبل.
type Region int
const (
UnknownRegion Region = iota
Local
)
type Status int
const (
StatusReady Status = iota + 1
StatusDone
// ربما سيكون لدينا StatusInProgress في المستقبل.
)
func printInfo(name string, region Region, status Status)
استخدام محارف النصوص الخام لتجنب الهروب
يدعم Go محارف النصوص الخام، والتي يمكن أن تمتد على أسطر متعددة وتتضمن علامات اقتباس. استخدم هذه لتجنب السلاسل التي تم الهروب منها يدويًا والتي يصعب قراءتها.
| سيء | جيد |
|---|---|
|
|
تهيئة الهياكل
استخدام أسماء الحقول لتهيئة الهياكل
يجب عليك دائمًا تحديد أسماء الحقول عند تهيئة الهياكل. يتم فرض ذلك الآن بواسطة go vet.
| سيء | جيد |
|---|---|
|
|
استثناء: يجوز حذف أسماء الحقول في جداول الاختبار عندما يكون هناك 3 حقول أو أقل.
tests :=struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
حذف حقول القيمة الصفرية في الهياكل
عند تهيئة الهياكل بأسماء الحقول، احذف الحقول التي لها قيم صفرية إلا إذا كانت توفر سياقًا ذا مغزى. بخلاف ذلك، دع Go يعين هذه إلى قيم صفرية تلقائيًا.
| سيء | جيد |
|---|---|
|
|
يساعد هذا في تقليل الضوضاء للقراء عن طريق حذف القيم الافتراضية في هذا السياق. يتم تحديد القيم ذات المغزى فقط.
قم بتضمين قيم صفرية حيث توفر أسماء الحقول سياقًا ذا مغزى. على سبيل المثال، يمكن أن تستفيد حالات الاختبار في جداول الاختبار من أسماء الحقول حتى عندما تكون قيمتها صفرية.
tests :=struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
استخدام var للهياكل ذات القيمة الصفرية
عند حذف جميع حقول الهيكل في إعلان، استخدم صيغة var لإعلان الهيكل.
| سيء | جيد |
|---|---|
|
|
يميز هذا الهياكل ذات القيمة الصفرية عن تلك التي تحتوي على حقول غير صفرية على غرار التمييز الذي تم إنشاؤه لـ تهيئة الخريطة، ويتطابق مع كيفية تفضيلنا لـ إعلان Slices فارغة.
تهيئة مراجع الهياكل
استخدم &T{} بدلاً من new(T) عند تهيئة مراجع الهياكل بحيث تكون متسقة مع تهيئة الهيكل.
| سيء | جيد |
|---|---|
|
|
تهيئة Maps
يفضل استخدام make(..) للخرائط الفارغة والخرائط المأهولة برمجيًا. هذا يجعل تهيئة الخريطة مميزة بصريًا عن الإعلان، ويجعل من السهل إضافة تلميحات الحجم لاحقًا إذا كانت متوفرة.
| سيء | جيد |
|---|---|
|
|
|
الإعلان والتهيئة متشابهان بصريًا. |
الإعلان والتهيئة متميزان بصريًا. |
حيثما أمكن، قم بتوفير تلميحات السعة عند تهيئة الخرائط باستخدام make(). انظر تحديد تلميحات سعة الخريطة لمزيد من المعلومات.
من ناحية أخرى، إذا كانت الخريطة تحتوي على قائمة ثابتة من العناصر، فاستخدم محارف الخريطة لتهيئة الخريطة.
| سيء | جيد |
|---|---|
|
|
القاعدة الأساسية هي استخدام محارف الخريطة عند إضافة مجموعة ثابتة من العناصر في وقت التهيئة، وإلا استخدم make (وحدد تلميحًا للحجم إذا كان متوفرًا).
سلاسل التنسيق خارج Printf
إذا قمت بإعلان سلاسل تنسيق لدوال نمط Printf خارج حرف سلسلة، فاجعلها قيمًا const.
يساعد هذا go vet على إجراء تحليل ثابت لسلسلة التنسيق.
| سيء | جيد |
|---|---|
|
|
تسمية دوال Printf
عند إعلان دالة بنمط Printf، تأكد من أن go vet يمكنه اكتشافها والتحقق من سلسلة التنسيق.
هذا يعني أنه يجب عليك استخدام أسماء دوال Printf المعرفة مسبقًا إن أمكن. سيتحقق go vet من هذه بشكل افتراضي. راجع عائلة Printf لمزيد من المعلومات.
إذا لم يكن استخدام الأسماء المعرفة مسبقًا خيارًا، فقم بإنهاء الاسم الذي تختاره بـ f: Wrapf، وليس Wrap. يمكن مطالبة go vet بالتحقق من أسماء Printf محددة ولكن يجب أن تنتهي بـ f.
go vet -printfuncs=wrapf,statusf
راجع أيضًا go vet: التحقق من عائلة Printf.
الأنماط
جداول الاختبار
يمكن أن تكون الاختبارات التي تعتمد على الجداول مع الاختبارات الفرعية نمطًا مفيدًا لكتابة الاختبارات لتجنب تكرار التعليمات البرمجية عندما تكون منطق الاختبار الأساسي متكررًا.
إذا كان نظام ما قيد الاختبار يحتاج إلى اختباره مقابل ظروف متعددة حيث تتغير أجزاء معينة من المدخلات والمخرجات، فيجب استخدام اختبار يعتمد على الجدول لتقليل التكرار وتحسين قابلية القراءة.
| سيء | جيد |
|---|---|
|
|
تسهل جداول الاختبار إضافة سياق إلى رسائل الخطأ وتقليل منطق التكرار وإضافة حالات اختبار جديدة.
نتبع الاصطلاح الذي يشير إلى شريحة الهياكل باسم tests وكل حالة اختبار tt. علاوة على ذلك، نشجع على توضيح قيم الإدخال والإخراج لكل حالة اختبار باستخدام بادئات give و want.
tests :=struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
تجنب التعقيد غير الضروري في اختبارات الجدول
يمكن أن تكون اختبارات الجدول صعبة القراءة والصيانة إذا كانت الاختبارات الفرعية تحتوي على تأكيدات شرطية أو منطق تفرع آخر. لا ينبغي استخدام اختبارات الجدول كلما كانت هناك حاجة إلى منطق معقد أو شرطي داخل الاختبارات الفرعية (أي منطق معقد داخل حلقة for).
تضر اختبارات الجدول الكبيرة والمعقدة بقابلية القراءة والصيانة لأن قارئي الاختبار قد يواجهون صعوبة في تصحيح أخطاء الاختبار التي تحدث.
يجب تقسيم اختبارات الجدول مثل هذه إلى إما جداول اختبار متعددة أو دوال Test ... فردية متعددة.
بعض المُثل التي يجب أن تهدف إليها هي:
- التركيز على أضيق وحدة سلوك
- تقليل "عمق الاختبار" إلى أدنى حد، وتجنب التأكيدات الشرطية (انظر أدناه)
- تأكد من استخدام جميع حقول الجدول في جميع الاختبارات
- تأكد من تشغيل جميع منطق الاختبار لجميع حالات الجدول
في هذا السياق، يعني "عمق الاختبار" "ضمن اختبار معين، عدد التأكيدات المتتالية التي تتطلب تأكيدات سابقة للتمسك بها" (على غرار تعقيد الدورة). يعني وجود اختبارات "أقل عمقًا" وجود علاقات أقل بين التأكيدات، والأهم من ذلك، أن هذه التأكيدات أقل عرضة للشرط افتراضيًا.
بشكل ملموس، يمكن أن تصبح اختبارات الجدول مربكة وصعبة القراءة إذا كانت تستخدم مسارات تفرع متعددة (مثل shouldError و expectCall وما إلى ذلك)، أو تستخدم العديد من عبارات if لتوقعات وهمية محددة (مثل shouldCallFoo)، أو ضع الدوال داخل الجدول (مثل setupMocks func (* FooMock)).
ومع ذلك، عند اختبار السلوك الذي يتغير فقط بناءً على تغيير الإدخال، قد يكون من الأفضل تجميع الحالات المتشابهة معًا في اختبار جدول لتوضيح كيفية تغير السلوك بشكل أفضل عبر جميع المدخلات، بدلاً من تقسيم الوحدات المماثلة إلى اختبارات منفصلة وجعلها أكثر صعوبة للمقارنة والتباين.
إذا كان نص الاختبار قصيرًا ومباشرًا، فمن المقبول وجود مسار تفرع واحد لحالات النجاح مقابل الفشل مع حقل جدول مثل shouldErr لتحديد توقعات الخطأ.
| سيء | جيد |
|---|---|
|
|
هذا التعقيد يجعل من الصعب تغيير وفهم وإثبات صحة الاختبار.
على الرغم من عدم وجود إرشادات صارمة، إلا أنه يجب أن تكون قابلية القراءة والصيانة دائمًا في الاعتبار عند اتخاذ القرار بين اختبارات الجدول مقابل الاختبارات المنفصلة لمدخلات / مخرجات متعددة لنظام ما.
اختبارات متوازية
يجب أن تحرص الاختبارات المتوازية، مثل بعض الحلقات المتخصصة (على سبيل المثال، تلك التي تفرخ goroutines أو تلتقط مراجع كجزء من نص الحلقة)، على تخصيص متغيرات الحلقة بشكل صريح ضمن نطاق الحلقة لضمان احتفاظها بالقيم المتوقعة.
tests :=struct{
give string
// ...
}{
// ...
}
for _, tt := range tests {
tt := tt // لـ t.Parallel
t.Run(tt.give, func(t *testing.T) {
t.Parallel()
// ...
})
}
في المثال أعلاه، يجب علينا إعلان متغير tt ضمن نطاق تكرار الحلقة بسبب استخدام t.Parallel () أدناه.
إذا لم نفعل ذلك، فستتلقى معظم أو كل الاختبارات قيمة غير متوقعة لـ tt، أو قيمة تتغير أثناء تشغيلها.
الخيارات الوظيفية
الخيارات الوظيفية هي نمط تعلن فيه عن نوع Option مُبهم يسجل المعلومات في بعض الهياكل الداخلية. أنت تقبل عددًا متغيرًا من هذه الخيارات وتتصرف بناءً على المعلومات الكاملة المسجلة بواسطة الخيارات في الهيكل الداخلي.
استخدم هذا النمط للوسائط الاختيارية في المُنشئات وواجهات برمجة التطبيقات العامة الأخرى التي تتوقع أنك ستحتاج إلى توسيعها، خاصةً إذا كان لديك بالفعل ثلاث وسيطات أو أكثر في تلك الدوال.
| سيء | جيد |
|---|---|
|
|
|
يجب دائمًا توفير معلمات ذاكرة التخزين المؤقت والمسجل، حتى لو أراد المستخدم استخدام الإعداد الافتراضي.
|
يتم توفير الخيارات فقط إذا لزم الأمر.
|
طريقتنا المقترحة لتنفيذ هذا النمط هي مع واجهة Option التي تحتوي على طريقة غير مُصدرة، وتسجيل الخيارات في بنية options غير مُصدرة.
type options struct {
cache bool
logger *zap.Logger
}
type Option interface {
apply(*options)
}
type cacheOption bool
func (c cacheOption) apply(opts *options) {
opts.cache = bool(c)
}
func WithCache(c bool) Option {
return cacheOption(c)
}
type loggerOption struct {
Log *zap.Logger
}
func (l loggerOption) apply(opts *options) {
opts.logger = l.Log
}
func WithLogger(log *zap.Logger) Option {
return loggerOption{Log: log}
}
// Open creates a connection.
func Open(
addr string,
opts ...Option,
) (*Connection, error) {
options := options{
cache: defaultCache,
logger: zap.NewNop(),
}
for _, o := range opts {
o.apply(&options)
}
// ...
}
لاحظ أن هناك طريقة لتنفيذ هذا النمط باستخدام عمليات الإغلاق ولكننا نعتقد أن النمط أعلاه يوفر مرونة أكبر للمؤلفين وأسهل في التصحيح والاختبار للمستخدمين. على وجه الخصوص، يسمح بمقارنة الخيارات مع بعضها البعض في الاختبارات والمحاكاة، مقابل عمليات الإغلاق حيث يكون ذلك مستحيلًا. علاوة على ذلك، فإنه يتيح للخيارات تنفيذ واجهات أخرى، بما في ذلك fmt.Stringer الذي يسمح بتمثيلات سلسلة قابلة للقراءة من قبل المستخدم للخيارات.
راجع أيضًا،
التدقيق اللغوي
الأهم من أي مجموعة "مباركة" من المدققات اللغوية، هو التدقيق اللغوي بشكل متسق عبر قاعدة التعليمات البرمجية.
نوصي باستخدام المدققات اللغوية التالية كحد أدنى، لأننا نشعر أنها تساعد في اكتشاف المشكلات الأكثر شيوعًا وأيضًا وضع معيار عالٍ لجودة التعليمات البرمجية دون أن تكون وصفية بشكل غير ضروري:
- errcheck للتأكد من معالجة الأخطاء
- goimports لتنسيق التعليمات البرمجية وإدارة عمليات الاستيراد
- golint للإشارة إلى أخطاء الأسلوب الشائعة
- govet لتحليل التعليمات البرمجية بحثًا عن الأخطاء الشائعة
- staticcheck لإجراء فحوصات تحليل ثابتة مختلفة
Lint Runners
نوصي بـ golangci-lint كأداة تشغيل lint للتعليمات البرمجية Go، ويرجع ذلك إلى حد كبير إلى أدائها في قواعد التعليمات البرمجية الأكبر وقدرتها على تكوين واستخدام العديد من أدوات lint الأساسية في وقت واحد. يحتوي هذا المستودع على مثال لملف التكوين .golangci.yml مع أدوات lint وإعدادات موصى بها.
يحتوي golangci-lint على أدوات lint متنوعة متاحة