العودة إلى المدونة

var مقابل make في Go: متى تستخدم كلًا منهما (مع أمثلة)

تعطيك var القيمة الصفرية، وتهيئ make الـ slices والـ maps والـ channels. متى تستخدم كلًا منهما في Go، ولماذا تسبب الكتابة في map قيمتها nil حالة panic بينما تنجح الإضافة إلى slice قيمته nil.

var مقابل make في Go: متى تستخدم كلًا منهما (مع أمثلة)

في Go طريقتان لإنشاء متغير، هما var و make، واختيار الطريقة الخاطئة سبب شائع لكثير من أخطاء البدايات. أشهرها map تسبب panic عند أول كتابة فيها. ويليها مباشرة channel تبقى معلقة إلى الأبد. يوضح هذا المقال متى تستخدم كلًا منهما، ولماذا تُعد الـ slices والـ maps والـ channels حالات خاصة. وكل مثال فيه قابل للتشغيل.

جدول المحتويات

ما الفرق بين var و make في Go؟

تصرّح var عن متغير وتضبطه على القيمة الصفرية لنوعه. وهي تعمل مع كل الأنواع في اللغة. أما make فلا تقبل إلا slice أو map أو channel، أي الأنواع المدمجة الثلاثة التي تحتاج إلى تجهيز في وقت التشغيل، وتعيد قيمة يمكنها استيعاب البيانات فورًا. القيمة الصفرية لهذه الأنواع الثلاثة هي nil، ولذلك تتخطى var التجهيز الذي تقوم به make.

بشكل ملموس، تخصص make جدول التجزئة (hash table) الذي تقوم عليه الـ map، وتجهّز طوابير الإرسال والاستقبال في الـ channel، وتمنح الـ slice مصفوفة أساسية (backing array). وإذا صرّحت عن أي من هذه الأنواع الثلاثة باستخدام var فستحصل على nil بدلًا من ذلك.

الـ map نفسها بالطريقتين:

example.gogo
package 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 Tmake(T, ...)
تعمل معأي نوعالـ slices والـ maps والـ channels فقط
تعطيكالقيمة الصفريةقيمة مهيأة جاهزة للاستخدام
نتيجة sliceslice قيمته nil (الطول 0، السعة 0)slice ليس nil وله مصفوفة أساسية
نتيجة mapmap قيمتها nil (الكتابة تسبب panic)map قابلة للكتابة
نتيجة channelchannel قيمتها nil (تبقى معلقة إلى الأبد)channel جاهزة للاستخدام
تعيدالقيمةالقيمة، ولا تعيد مؤشرًا أبدًا

متى تستخدم var؟

استخدم var مع أي نوع تكون قيمته الصفرية مفيدة بالفعل: الأعداد والقيم المنطقية والسلاسل النصية والـ structs والمؤشرات والـ interfaces. صُممت Go بحيث تكون القيمة الصفرية حالة بداية صالحة، ولهذا نادرًا ما تحتاج إلى تهيئة أي شيء أولًا.

bytes.Buffer مثال جيد على ذلك. فليست له دالة إنشاء (constructor). تصرّح عنه باستخدام var وتبدأ الكتابة فيه:

example.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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.gogo
package 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 عندما تحتاج تحديدًا إلى مؤشر، وغالبًا ما يكون مؤشرًا إلى قيمة حرفية لحقل اختياري.

قاعدة سريعة للاختيار

إذا لم تكن متأكدًا أيهما تستخدم، فمرّ على هذه القائمة من الأعلى وتوقف عند أول سطر ينطبق:

  1. map ستكتب فيها مفاتيح؟ استخدم make(map[K]V). الـ map التي قيمتها nil تسبب panic عند أول كتابة.
  2. channel سترسل عليها goroutines؟ استخدم make(chan T) أو make(chan T, n). الـ channel التي قيمتها nil تبقى معلقة إلى الأبد.
  3. slice تريد تحديد حجمه مسبقًا، من أجل السرعة أو للإسناد بالفهرس؟ استخدم make([]T, n) أو make([]T, 0, n).
  4. slice لن تكبّره إلا باستخدام append؟ استخدم var s []T. الـ slice الذي قيمته nil يعمل مع append.
  5. مؤشر إلى قيمة جديدة؟ استخدم new(T)، أو new(expr) في Go 1.26 للإشارة إلى قيمة حرفية.
  6. أي شيء آخر (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):

اكتب Go كما يكتبها مهندس أول

دروس تفاعلية في متصفحك. الدروس الأولى مجانية.

جرّب درسًا مجانيًاأو أنشئ حسابًا مجانيًا