مقدمة
تشتهر Go (Golang) ببساطتها وإيجازها. ومع ذلك، خلف هذه السهولة الظاهرية تكمن مخاطر خفية تكون خادعة بشكل خاص للمطورين الذين أتقنوا بالفعل بناء الجملة والتراكيب الأساسية. الانتقال إلى المستوى "المتقدم" لا يتعلق بمعرفة الأنماط المعقدة، بل بفهم عميق لعمل بيئة التشغيل (runtime) والأنواع والتزامن.
في هذه المقالة، سنحلل أهم أخطاء مطوري Go المبتدئين التي تتجاوز "نسيت إغلاق ملف" أو "خلطت بين nil والخطأ". ستتعلم كيفية تجنب تسرب الذاكرة، والتعامل الخاطئ مع الغوروتينات والقنوات، وستفهم لماذا nil ليس دائمًا nil.
الخطأ 1: تسرب الغوروتينات (Goroutine Leaks)
أغلى خطأ في Go هو تشغيل غوروتينة ونسيانها. غالبًا ما يعتقد المطورون المبتدئون أن الغوروتينة ستنتهي من تلقاء نفسها بمجرد تنفيذ الدالة. ولكن إذا كانت الغوروتينة محظورة على قناة لن تستقبل بيانات أبدًا، فستظل معلقة إلى الأبد.
المشكلة: قناة منسية بدون مستقبل
func process(data []int) chan int { out := make(chan int) go func() { for _, v := range data { out <- v * 2 } close(out) }() return out}
func main() { ch := process([]int{1, 2, 3}) // نقرأ قيمة واحدة فقط، القناة لن تُغلق fmt.Println(<-ch) // الغوروتينة ستعلق محاولة إرسال 4 و 6 إلى القناة // time.Sleep(time.Second) — تسرب للذاكرة}الحل: تحكم دائمًا في عمر الغوروتينة. استخدم context.Context للإلغاء أو القنوات المخزنة مؤقتًا إذا لم تكن متأكدًا من عدد القراء.
func process(ctx context.Context, data []int) <-chan int { out := make(chan int) go func() { defer close(out) for _, v := range data { select { case out <- v * 2: case <-ctx.Done(): return } } }() return out}الخطأ 2: الاستخدام الخاطئ لـ select مع default
تركيبة select مع default هي أداة قوية للعمليات غير المحظورة. لكن المبتدئين غالبًا ما يستخدمونها كـ "عكاز"، مما يؤدي إلى حلقة مزدحمة (busy-loop) مع استهلاك عالي لوحدة المعالجة المركزية.
المشكلة: حلقة لا نهائية بدون حظر
for { select { case msg := <-ch: fmt.Println(msg) default: // لا نفعل شيئًا، لكن الحلقة لا تُحظر }}هذا الكود سيهدر المعالج، مستهلكًا 100% من نواة واحدة. default يجعل select غير محظور، وتتحول الحلقة إلى استقصاء لا نهائي.
الحل: إما إزالة default (عندها سينتظر select)، أو إضافة time.Sleep أو time.Tick لتقليل الحمل.
// الخيار الصحيح: بدون defaultfor msg := range ch { fmt.Println(msg)}
// أو مع مهلة زمنيةfor { select { case msg := <-ch: fmt.Println(msg) case <-time.After(100 * time.Millisecond): // فحص دوري }}الخطأ 3: واجهة nil ليست nil
هذا فخ كلاسيكي في Go. المتغير من نوع interface{} يساوي nil فقط إذا كان كل من نوعه وقيمته يساويان nil. إذا قمت بتعيين مؤشر إلى nil لواجهة، فإن الواجهة تتوقف عن كونها nil.
المشكلة: التحقق من nil عبر واجهة
type MyError struct{}
func (e *MyError) Error() string { return "خطأ"}
func getError() error { var e *MyError = nil return e // نعيد مؤشر nil، لكن النوع ليس nil}
func main() { err := getError() if err != nil { fmt.Println("يوجد خطأ!")