الدوال المجهولة
الدالة الحرفية (function literal) دالة بلا اسم. وهي تعبير، لذا تُكتب في أي مكان تُكتب فيه قيمة: متغيّر، أو وسيط، أو قيمة معادة، أو حقل في بنية.
لا تملك Go صيغة مختصرة بالسهم أو lambda. الشكل الكامل func(params) results { body } هو الطريقة الوحيدة لكتابتها.
التقاط المتغيّرات
تستطيع الدالة الحرفية استخدام متغيّرات الدالة المحيطة بها. وهي لا تنسخها: الإغلاق والشيفرة المحيطة يتشاركان متغيّرًا واحدًا.
يعيش المتغيّر الملتقط ما دام إغلاق ما يشير إليه، حتى بعد عودة الدالة التي عرّفته. ينقله المترجم إلى الكومة (heap) عند الحاجة؛ ولا تدير ذلك بنفسك أبدًا.
المولّدات والعدّادات
إعادة إغلاق من دالة تمنح كل دالة معادة حالتها الخاصة.
كل استدعاء لـ counter() ينشئ n جديدًا، فيعدّ c1 وc2 كل على حدة. لا شيء في الخارج يستطيع قراءة n أو إعادة ضبطه، وهذا شكل صغير من التغليف.
الإغلاقات في الحلقات (Go 1.22 وما بعده)
قبل Go 1.22 كان لحلقة for متغيّر حلقة واحد للحلقة كلها. الإغلاقات المنشأة داخلها كانت كلها تلتقط هذا المتغيّر الوحيد وترى قيمته الأخيرة. كان هذا أشيع خطأ في الإغلاقات في Go.
منذ Go 1.22 يحصل كل تكرار على نسخته الخاصة، فتطبع الشيفرة التالية 0 1 2:
في Go 1.21 وما قبله، كانت الحلقة نفسها مكتوبة بالشكل for i := 0; i < 3; i++ (المرور بـ range على عدد صحيح جديد أيضًا في 1.22) تطبع 3 3 3. تنطبق القاعدة الجديدة لكل وحدة حسب سطر go في go.mod، فالوحدة القديمة تبقى على السلوك القديم حتى يُرفع سطر go فيها إلى 1.22 أو أحدث. ما زلت سترى i := i أو v := v داخل الحلقات في الشيفرة الأقدم: كان ذلك الإصلاح اليدوي وأصبح الآن زائدًا.
قاعدة النسخة لكل تكرار تشمل فقط المتغيّرات التي تعرّفها عبارة for نفسها. المتغيّر المعرّف قبل الحلقة والمحدَّث داخلها يبقى متغيّرًا مشتركًا واحدًا:
var last string
for _, s := range items {
last = s
handlers = append(handlers, func() { use(last) }) // every closure sees the final value
}
الإغلاقات والـ goroutines
الـ goroutine التي تبدأ بدالة حرفية هي إغلاق أيضًا. تنطبق قواعد المشاركة نفسها، إضافة إلى قاعدة التزامن المعتادة: إذا كتبت عدة goroutines في متغيّر ملتقط، فأنت تحتاج إلى mutex أو قناة.
يطبع هذا دائمًا 5050. كل goroutine تقرأ i الخاص بها (دلالات حلقة Go 1.22) وتكتب في total المشترك تحت قفل. احذف الـ mutex فتصبح النتيجة غير متوقّعة؛ ويبلغ عنها go run -race على أنها سباق بيانات. راجع الـ goroutines لمعرفة WaitGroup وكاشف السباقات.
أين تظهر الإغلاقات في الشيفرة الحقيقية
- الترتيب والبحث: كل من
slices.SortFuncوslices.IndexFuncوsort.Sliceتأخذ دالة حرفية. - التنظيف المؤجّل:
defer func() { ... }()تنفّذ كتلة عند خروج الدالة ويمكنها قراءة متغيّرات الدالة. راجع defer. - الطبقات الوسيطة في HTTP: دالة تأخذ معالجًا وتعيد دالة حرفية جديدة من النوع
http.HandlerFuncتغلّفه. - الإعداد: الخيارات الوظيفية (
func WithTimeout(d time.Duration) Option { return func(c *Config) { c.Timeout = d } }) إغلاقات على الوسيط.
مثال طبقة وسيطة تلتقط معاملًا وعدّادًا معًا:
أخطاء شائعة
- توقّع لقطة ثابتة. يقرأ الإغلاق القيمة الحالية للمتغيّر عند تنفيذه، لا القيمة وقت إنشائه. مرّر القيمة معاملًا إذا احتجت لقطة:
go func(v int) { ... }(x). - الدوال الحرفية العودية. لا تستطيع الدالة الحرفية الإشارة إلى نفسها عبر المتغيّر الذي تُسند إليه في عبارة
:=نفسها. عرّف المتغيّر أولًا:var walk func(n int); walk = func(n int) { ... walk(n-1) }. - كتابات غير متزامنة من goroutines. الالتقاط ليس تزامنًا. احمِ الكتابات المشتركة.
الأسئلة الشائعة
ما هي الدالة المجهولة في Go؟
دالة حرفية بلا اسم: func(x int) int { return x * 2 }. يمكنك إسنادها إلى متغيّر، أو تمريرها وسيطًا، أو إعادتها، أو استدعاؤها فورًا بإضافة () بعد القوس الختامي. لا تملك Go صيغة lambda منفصلة؛ الدالة الحرفية هي الـ lambda.
ما هو الإغلاق (closure) في Go؟
دالة حرفية تشير إلى متغيّرات معرّفة خارجها. تُبقي الدالة هذه المتغيّرات حيّة وتشاركها مع النطاق المحيط، فتظهر التغييرات التي تحدث داخل الإغلاق في الخارج والعكس. تُلتقط المتغيّرات بالمرجع ولا تُنسخ.
هل تلتقط الإغلاقات في Go المتغيّرات بالقيمة أم بالمرجع؟
بالمرجع. الإغلاق والشيفرة المحيطة يستخدمان المتغيّر نفسه. إذا احتجت لقطة ثابتة، انسخ القيمة إلى متغيّر جديد قبل إنشاء الإغلاق، أو مرّرها وسيطًا.
هل أُصلح خطأ متغيّر الحلقة مع الإغلاقات في Go؟
نعم، منذ Go 1.22. كل تكرار لحلقة for يعرّف الآن متغيّر حلقة جديدًا، فترى الإغلاقات والـ goroutines المنشأة داخل الحلقة قيمة ذلك التكرار. ينطبق ذلك على الوحدات التي يذكر ملف go.mod فيها go 1.22 أو أحدث. الشيفرة الأقدم تحتوي غالبًا على i := i داخل الحلقة كحل مؤقت؛ لا ضرر منه لكنه لم يعد ضروريًا.