في Go طريقتان لإنشاء متغير، هما var و make، واختيار الطريقة الخاطئة سبب شائع لكثير من أخطاء البدايات. أشهرها map تسبب panic عند أول كتابة فيها. ويليها مباشرة channel تبقى معلقة إلى الأبد. يوضح هذا المقال متى تستخدم كلًا منهما، ولماذا تُعد الـ slices والـ maps والـ channels حالات خاصة. وكل مثال فيه قابل للتشغيل.
جدول المحتويات
- ما الفرق بين var و make في Go؟
- متى تستخدم var؟
- متى تستخدم make؟
- لماذا تسبب الكتابة في map قيمتها nil حالة panic بينما تنجح الإضافة إلى slice قيمته nil؟
- ماذا عن new؟ (var مقابل make مقابل new)
- قاعدة سريعة للاختيار
- الأسئلة الشائعة
- المصادر
ما الفرق بين var و make في Go؟
تصرّح var عن متغير وتضبطه على القيمة الصفرية لنوعه. وهي تعمل مع كل الأنواع في اللغة. أما make فلا تقبل إلا slice أو map أو channel، أي الأنواع المدمجة الثلاثة التي تحتاج إلى تجهيز في وقت التشغيل، وتعيد قيمة يمكنها استيعاب البيانات فورًا. القيمة الصفرية لهذه الأنواع الثلاثة هي nil، ولذلك تتخطى var التجهيز الذي تقوم به make.
بشكل ملموس، تخصص make جدول التجزئة (hash table) الذي تقوم عليه الـ map، وتجهّز طوابير الإرسال والاستقبال في الـ channel، وتمنح الـ slice مصفوفة أساسية (backing array). وإذا صرّحت عن أي من هذه الأنواع الثلاثة باستخدام var فستحصل على nil بدلًا من ذلك.
الـ map نفسها بالطريقتين:
example.gogopackage main import "fmt" func main() { var counts map[string]int // nil map, no hash table yet made := make(map[string]int) // initialized, ready to write fmt.Println(counts == nil) // true fmt.Println(made == nil) // false made["/health"] = 1 // fine, made is initialized fmt.Println(made["/health"]) // 1 // counts["/health"] = 1 // panic: assignment to entry in nil map }
كثيرًا ما يتوقع الناس أن يعيد تخصيص الذاكرة مؤشرًا (pointer). لكن make لا تفعل ذلك. فهي تخصص الذاكرة ثم تعطيك القيمة مباشرة.
لذلك تعيد make([]int, 3) قيمة []int عادية، وليس *[]int، وتستخدمها دون أي إلغاء للإشارة (dereference). وتعيد make(map[string]int) قيمة map يمكنك الكتابة فيها فورًا. أما الدالة المدمجة التي تعيد مؤشرًا فعلًا فهي new، وسنتناولها لاحقًا.
var x T | make(T, ...) | |
|---|---|---|
| تعمل مع | أي نوع | الـ slices والـ maps والـ channels فقط |
| تعطيك | القيمة الصفرية | قيمة مهيأة جاهزة للاستخدام |
| نتيجة slice | slice قيمته nil (الطول 0، السعة 0) | slice ليس nil وله مصفوفة أساسية |
| نتيجة map | map قيمتها nil (الكتابة تسبب panic) | map قابلة للكتابة |
| نتيجة channel | channel قيمتها nil (تبقى معلقة إلى الأبد) | channel جاهزة للاستخدام |
| تعيد | القيمة | القيمة، ولا تعيد مؤشرًا أبدًا |
متى تستخدم var؟
استخدم var مع أي نوع تكون قيمته الصفرية مفيدة بالفعل: الأعداد والقيم المنطقية والسلاسل النصية والـ structs والمؤشرات والـ interfaces. صُممت Go بحيث تكون القيمة الصفرية حالة بداية صالحة، ولهذا نادرًا ما تحتاج إلى تهيئة أي شيء أولًا.
bytes.Buffer مثال جيد على ذلك. فليست له دالة إنشاء (constructor). تصرّح عنه باستخدام var وتبدأ الكتابة فيه:
example.gogopackage main import ( "bytes" "fmt" ) func main() { var buf bytes.Buffer // zero value is ready to use buf.WriteString("status=") buf.WriteString("ok") fmt.Println(buf.String()) // status=ok }
ويعمل sync.Mutex بالطريقة نفسها. فقيمته الصفرية mutex غير مقفل، ولا توجد دالة إنشاء باسم NewMutex لأنك لا تحتاج إليها.
تصلح var أيضًا مع الـ slice، بشرط ألا تكبّره إلا باستخدام append. الـ slice الذي قيمته nil هو slice فارغ صالح طوله 0 وسعته 0، وتخصص append مصفوفة أساسية عند إضافة أول عنصر. لكن لا يمكنك الوصول إلى عناصره بالفهرس. فالتعبير ids[0] = 3 يسبب panic من نوع index out of range لأن الطول ما زال 0. إذا كنت تحتاج إلى الإسناد بالفهرس، فاستخدم make([]int, n) ليكون للـ slice طول حقيقي من البداية. أما لتجميع العناصر باستخدام append، فالـ slice الذي قيمته nil مناسب:
example.gogopackage main import "fmt" func main() { var ids []int // nil slice, but append-ready for i := 1; i <= 3; i++ { ids = append(ids, i*10) } fmt.Println(ids) // [10 20 30] fmt.Println(ids == nil) // false, append allocated it }
كتابة ids := make([]int, 0) هنا لا تفيدك بشيء. الـ slice الذي قيمته nil أقصر، ويتصرف بالطريقة نفسها بعد تنفيذ أول append. وإذا أردت التمرن على ذلك، ففي دورة Composite Types مجموعة كاملة من التمارين عن سلوك الـ slices والـ maps.
متى تستخدم make؟
استخدم make عندما يجب أن تستوعب slice أو map أو channel بيانات قبل أن يُضاف إليها أي شيء بـ append. عمليًا يعني ذلك map ستكتب فيها مفاتيح بعد قليل، أو channel سترسل عليها goroutines، أو slice تريد تحديد حجمه مسبقًا لتكون مصفوفته الأساسية موجودة من البداية.
الـ maps هي الأكثر تكرارًا، لأنه لا يوجد ما يقابل append ليخصص لك map تلقائيًا. إذا كنت تنوي إسناد أي مفتاح، فعليك إنشاء الـ map بـ make أولًا. وعدّ الطلبات لكل مسار في معالج HTTP حالة معتادة:
example.gogopackage main import "fmt" func main() { hits := make(map[string]int) // must initialize before writing for _, path := range []string{"/api", "/api", "/health"} { hits[path]++ // safe: map is initialized } fmt.Println(hits["/api"]) // 2 fmt.Println(hits["/health"]) // 1 }
والـ channels مشابهة. الـ channel التي ترسل عليها يجب إنشاؤها بـ make، لأن الـ channel التي قيمتها nil تبقى معلقة إلى الأبد (سنعود إلى ذلك لاحقًا). ويبدأ توزيع العمل على العمّال (worker fan-out) بـ channel منشأة بـ make:
example.gogopackage main import "fmt" func main() { jobs := make(chan int, 3) // buffered channel, capacity 3 jobs <- 1 jobs <- 2 close(jobs) for j := range jobs { fmt.Println("processing job", j) } }
أما مع الـ slices فلـ make صيغتان. تعطيك make([]T, n) slice طوله n مملوءًا بالقيم الصفرية، وتصل إلى عناصره بالفهرس مباشرة. وتعطيك make([]T, 0, n) slice فارغًا يتسع لـ n عنصرًا، تضيف إليه بـ append دون إعادة تخصيص. استخدم الصيغة الأولى عندما ستملأ كل خانة بالفهرس، كما في قراءة عدد ثابت من البايتات:
example.gogopackage main import "fmt" func main() { // length 4, index into it directly buf := make([]byte, 4) copy(buf, []byte("data")) fmt.Println(string(buf)) // data // length 0, capacity 8, append without regrowing sums := make([]int, 0, 8) for i := 0; i < 8; i++ { sums = append(sums, i*i) } fmt.Println(sums) // [0 1 4 9 16 25 36 49] }
الصيغة الثانية تتعلق بالأداء. فكلما كبر الـ slice، تظل append تنسخه إلى مصفوفات أكبر. وإذا كنت تعرف تقريبًا عدد العناصر القادمة، فإن تحديد السعة مسبقًا يوفر عليك هذا النسخ. ولا يتغير سلوك البرنامج، بل سرعته فقط.
لماذا تسبب الكتابة في map قيمتها nil حالة panic بينما تنجح الإضافة إلى slice قيمته nil؟
لأن كلًا منهما صُمم بطريقة مختلفة. تعرف append كيف تتعامل مع الـ slice الذي قيمته nil. ففي أول استدعاء تخصص مصفوفة أساسية وتعيد slice حقيقيًا، لذلك تنجح الكتابة عبر append دائمًا. أما الـ maps فليس لديها شيء من هذا القبيل. فالكتابة تذهب مباشرة إلى جدول تجزئة، والـ map التي قيمتها nil لا تملك جدولًا، فيطلق الـ runtime حالة panic بدلًا من أن يخمّن ما قصدته.
يصطدم معظم الناس بهذا في أسبوعهم الأول مع Go. إليك ما يفعله كل نوع قيمته nil، بدءًا بالـ slice الذي قيمته nil، والذي يمكنك القراءة منه والإضافة إليه دون مشكلات:
example.gogopackage main import "fmt" func main() { var s []int // nil slice fmt.Println(len(s), cap(s)) // 0 0 fmt.Println(s == nil) // true s = append(s, 42) // works: append allocates fmt.Println(s) // [42] }
أما الـ map التي قيمتها nil فمختلفة. القراءة منها تعيد القيمة الصفرية لنوع القيم، والمرور عليها بـ range لا يفعل شيئًا، لكن أول عملية كتابة تسبب panic:
example.gogopackage main import "fmt" func main() { var m map[string]int // nil map fmt.Println(m["missing"]) // 0, reading nil map is safe fmt.Println(len(m)) // 0, also safe m["key"] = 1 // panic: assignment to entry in nil map fmt.Println(m) }
هذا الانقسام بين قراءة آمنة وكتابة تسبب panic هو سبب سهولة تفويت الخطأ. فالـ map التي قيمتها nil تجتاز كل اختبار يكتفي بالقراءة منها، ثم تسبب panic في أول مرة تصل فيها بيانات حقيقية. والحل لا يتغير: هيّئها بـ make قبل أول كتابة، أو استقبل map أنشأها المستدعي مسبقًا.
سلوك الـ channels التي قيمتها nil هو الأغرب بين الثلاثة. فهي لا تسبب panic. الإرسال أو الاستقبال على channel قيمتها nil لا يكتمل أبدًا، وهذا في goroutine واحدة يعني جمودًا (deadlock):
example.gogopackage main func main() { var ch chan int // nil channel ch <- 1 // blocks forever: fatal deadlock }
يبدو هذا فخًا، لكنه مفيد داخل select. فالـ channel التي قيمتها nil لا تكون جاهزة أبدًا، لذلك يؤدي ضبط متغير channel على nil إلى تعطيل الحالة (case) الخاصة به، وبهذه الطريقة يمكنك إيقاف فرع أثناء التشغيل. لكن انتبه إلى close: إغلاق channel قيمتها nil يسبب panic، لذلك لا تغلق إلا الـ channels التي أنشأتها بـ make. تشرح دورة Concurrency Fundamentals نمط select مع channels قيمتها nil بالتفصيل. ويرد ثنائي الـ map والـ slice اللذين قيمتهما nil أيضًا في قائمتنا عن الأخطاء الشائعة في Go التي يجب تجنبها، إذ يقع فيه المطورون أصحاب الخبرة أيضًا.
ماذا عن new؟ (var مقابل make مقابل new)
الدالة new هي الدالة المدمجة الثالثة للتخصيص، ولها وظيفة مختلفة عن make. تخصص new(T) مساحة مصفّرة لقيمة من النوع T وتعيد *T. أما make فلا تتعامل إلا مع الـ slices والـ maps والـ channels وتعيد القيمة نفسها. وتعمل new مع أي نوع وتعيد دائمًا مؤشرًا.
السبب المعتاد لاستدعائها هو الحصول على مؤشر إلى قيمة صفرية جديدة دون التصريح عن متغير مسمّى أولًا:
example.gogopackage main import "fmt" func main() { p := new(int) // *int pointing at a zeroed int fmt.Println(*p) // 0 *p = 7 fmt.Println(*p) // 7 }
منذ Go 1.26 أصبحت new تقبل تعبيرًا (expression) أيضًا. تخصص new(expr) المساحة، وتخزن فيها قيمة التعبير، ثم تعيد مؤشرًا إليها. وبهذا لم تعد هناك حاجة إلى الدوال المساعدة القديمة على غرار intPtr، التي لم يكن لها غرض سوى الحصول على مؤشر إلى قيمة حرفية (literal):
example.gogopackage main import "fmt" func main() { port := new(8080) // *int pointing at 8080 (Go 1.26) enabled := new(true) // *bool pointing at true fmt.Println(*port) // 8080 fmt.Println(*enabled) // true }
هذا مفيد مع حقول struct الاختيارية، حيث يعني المؤشر الذي قيمته nil «غير مضبوط»، ويعني المؤشر غير nil أن أحدًا اختار قيمة. وفي دورة Pointers & Memory تمرين لضبط القيم الافتراضية في الإعدادات مبني على هذا النمط. الدوال المدمجة الثلاث جنبًا إلى جنب:
| الصيغة | تعمل مع | تعيد | هل النتيجة جاهزة للاستخدام؟ |
|---|---|---|---|
var x T | أي نوع | القيمة الصفرية | جاهزة إذا كانت القيمة الصفرية جاهزة (لا تصلح للكتابة في map) |
make(T, ...) | slice و map و channel | القيمة T | نعم، مهيأة |
new(T) | أي نوع | *T، أي مؤشر | تشير إلى قيمة T مصفّرة |
new(expr) (Go 1.26) | أي تعبير | مؤشر إلى تلك القيمة | تشير إلى القيمة المعطاة |
مع slice أو map أو channel ستملؤها بعد قليل، تحتاج إلى make في أغلب الأحوال. واستخدم new عندما تحتاج تحديدًا إلى مؤشر، وغالبًا ما يكون مؤشرًا إلى قيمة حرفية لحقل اختياري.
قاعدة سريعة للاختيار
إذا لم تكن متأكدًا أيهما تستخدم، فمرّ على هذه القائمة من الأعلى وتوقف عند أول سطر ينطبق:
- map ستكتب فيها مفاتيح؟ استخدم
make(map[K]V). الـ map التي قيمتها nil تسبب panic عند أول كتابة. - channel سترسل عليها goroutines؟ استخدم
make(chan T)أوmake(chan T, n). الـ channel التي قيمتها nil تبقى معلقة إلى الأبد. - slice تريد تحديد حجمه مسبقًا، من أجل السرعة أو للإسناد بالفهرس؟ استخدم
make([]T, n)أوmake([]T, 0, n). - slice لن تكبّره إلا باستخدام
append؟ استخدمvar s []T. الـ slice الذي قيمته nil يعمل معappend. - مؤشر إلى قيمة جديدة؟ استخدم
new(T)، أوnew(expr)في Go 1.26 للإشارة إلى قيمة حرفية. - أي شيء آخر (int أو string أو bool أو struct أو معظم أنواع المكتبة القياسية)؟ استخدم
var، لأن القيمة الصفرية جاهزة للاستخدام.
إذا كنت ستتذكر شيئًا واحدًا فليكن هذا: قبل أن تكتب مفتاحًا في map، تحقق من مصدرها. الـ map التي وصلت كمعامل يُفترض أن المستدعي هيّأها. أما الـ map التي صرّحت عنها بنفسك باستخدام var فقيمتها nil، وأول كتابة فيها ستسبب panic.
الأسئلة الشائعة
هل يمكنني استخدام var مع map؟
يمكنك التصريح عن map باستخدام var m map[string]int. والقراءة منها والمرور عليها بـ range واستدعاء len عليها كلها عمليات آمنة. أما كتابة مفتاح فليست آمنة: يسبب m["x"] = 1 حالة panic مع الرسالة assignment to entry in nil map. إذا كنت تقرأ فقط، فلا بأس بـ var. وإذا كنت ستكتب أي مفتاح، فهيّئ الـ map بـ make أولًا أو استقبل map أنشأها المستدعي مسبقًا.
هل make([]int, 0) هو نفسه var s []int؟
طول كل منهما 0، ويتصرف الاثنان بالطريقة نفسها بمجرد استخدام append. يظهر الفرق عند المقارنة مع nil. فقيمة var s []int هي nil، بينما make([]int, 0) هو slice فارغ ليس nil. ويهم ذلك إذا كان الكود أو أحد الاختبارات يتحقق من s == nil، أو إذا كنت تحوّل البيانات إلى JSON، حيث تصبح nil القيمة null ويصبح الـ slice الفارغ []. أما للإضافة العادية فالأفضل استخدام var.
هل تعمل make مع struct؟
لا. لا تقبل make إلا الـ slices والـ maps والـ channels، لذلك فإن make(MyStruct) خطأ في الترجمة (compile error). فالـ struct لا يحتاج إلى التجهيز في وقت التشغيل الذي تقوم به make. أنشئه بقيمة حرفية مركبة (composite literal) مثل MyStruct{}، أو صرّح عنه بـ var s MyStruct للحصول على القيمة الصفرية، أو استخدم new(MyStruct) عندما تريد *MyStruct.
make أم new: أيهما تستخدم للحصول على مؤشر؟
استخدم new عندما تريد مؤشرًا. تعيد new(T) قيمة *T تشير إلى قيمة مصفّرة، وفي Go 1.26 تعيد new(expr) مؤشرًا إلى قيمة ذلك التعبير. أما make فلا تعيد مؤشرًا أبدًا. بل تعيد slice أو map أو channel مهيأة. ولا تتداخل الاثنتان: تعطيك make مجموعة (collection) قابلة للاستخدام، وتعطيك new مؤشرًا.
المصادر
المراجع الأساسية المذكورة في هذا المقال (آخر تحقق في 3 يوليو 2026):
- The Go Programming Language Specification: Making slices, maps and channels، تاريخ الاطلاع 2026-07-03
- The Go Programming Language Specification: Allocation (new)، تاريخ الاطلاع 2026-07-03
- The Go Programming Language Specification: The zero value، تاريخ الاطلاع 2026-07-03
- Effective Go: Allocation with make and new، تاريخ الاطلاع 2026-07-03
- Go 1.26 Release Notes: التغييرات على اللغة، تاريخ الاطلاع 2026-07-03
