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

الكلمة المفتاحية map في Go: الـ maps ذات القيمة nil وترتيب التكرار والتزامن

كيف تعمل الكلمة المفتاحية map في Go: الصيغة الحرفية مقابل make، و panic الـ map ذات القيمة nil، و comma-ok، وترتيب التكرار العشوائي، والمفاتيح القابلة للمقارنة، والوصول المتزامن، والمجموعات، والحزمة maps.

الكلمة المفتاحية map في Go: الـ maps ذات القيمة nil وترتيب التكرار والتزامن

تصرّح الكلمة المفتاحية map عن جدول التجزئة (hash table) المدمج في Go. فالنوع map[K]V يربط مفاتيح من النوع K بقيم من النوع V، مع زمن ثابت في المتوسط للبحث والإدراج والحذف. والـ maps جزء من اللغة نفسها، لذا لا تستورد حزمة ولا تنشئ كائنًا من صنف HashMap. وتحصل بدلًا من ذلك على صيغة حرفية، والدوال المدمجة make و delete و clear و len، ودعم for range (مواصفات Go).

الخلاصة

  • التعبير map[string]int{"/healthz": 1} أو make(map[string]int) ينشئ map. أما var m map[string]int فيعطيك map قيمتها nil.
  • القراءة من map قيمتها nil تعيد القيمة الصفرية. والكتابة فيها تسبب panic بالرسالة assignment to entry in nil map.
  • المفتاح غير الموجود يعيد القيمة الصفرية. استخدم v, ok := m[k] حين تحتاج إلى معرفة هل المفتاح موجود.
  • ترتيب التكرار غير محدد وعشوائي. رتّب المفاتيح باستخدام slices.Sorted(maps.Keys(m)) حين يهمك الترتيب.
  • يجب أن تكون المفاتيح قابلة للمقارنة. السلاسل النصية والأرقام والـ structs والمصفوفات تعمل. أما الـ slices والـ maps والدوال فلا تعمل.
  • قيمة الـ map واصف (descriptor) صغير. وتمريرها إلى دالة يسمح لتلك الدالة بتعديل map المستدعي.
  • التعبير m[k].Field = x لا يُترجم حين تكون القيمة struct. انسخها وعدّلها ثم خزّنها من جديد، أو خزّن مؤشرات.
  • الـ maps ليست آمنة للكتابة المتزامنة. احمِها باستخدام sync.Mutex أو استخدم sync.Map في الحالات التي صُمّم لها.
  • النوع map[string]struct{} هو المجموعة (set) في Go. وتضيف الحزمة maps (منذ Go 1.21) الدوال Clone و Equal و DeleteFunc و Keys و Collect.

كيف تصرّح عن map وتنشئها في Go؟

يُكتب نوع الـ map بالشكل map[KeyType]ValueType. وتنشئ الـ map بصيغة حرفية مركّبة (composite literal) حين تعرف بعض المدخلات مسبقًا، أو باستخدام make حين تبدأ بـ map فارغة:

example.gogo
package main

import "fmt"

func main() {
	statusText := map[int]string{
		200: "OK",
		404: "Not Found",
		503: "Service Unavailable",
	}

	hits := make(map[string]int)
	hits["/api/orders"]++
	hits["/api/orders"]++
	hits["/healthz"]++

	fmt.Println(statusText[404], len(statusText))
	fmt.Println(hits["/api/orders"], hits["/healthz"], len(hits))
}
example.texttext
Not Found 3
2 1 2

يعمل hits["/api/orders"]++ مع مفتاح لم يوجد بعد. فالقراءة تعيد 0، والزيادة تجعلها 1، والكتابة تخزّنها. فالعدّاد لا يحتاج إلى أي تهيئة.

تأخذ make تلميحًا اختياريًا للحجم، كما في make(map[string]int, len(requests)). فيحجز وقت التشغيل مساحة لعدد المدخلات هذا تقريبًا، فلا تضطر الـ map إلى النمو أثناء ملئها. والتلميح ليس حدًا أقصى، فالـ map تنمو بعده. ويعيد len(m) عدد المدخلات، أما cap فلا يعمل مع الـ maps.

يقارن مقال var مقابل make في Go بين طريقتَي الحصول على map فارغة، ويشرح لماذا لا تكفي var وحدها.

لماذا تسبب الكتابة في map قيمتها nil حالة panic؟

القيمة الصفرية لنوع الـ map هي nil. والـ map ذات القيمة nil لا يقف خلفها جدول تجزئة، لذا لا يوجد مكان لوضع مدخل جديد. وعادةً يظهر هذا في حقل map داخل struct لم يهيّئه أحد:

example.gogo
package main

import "fmt"

type Router struct {
	routes map[string]string
}

func main() {
	var r Router
	fmt.Println(r.routes["/health"] == "", len(r.routes))
	r.routes["/health"] = "healthHandler"
}
example.texttext
true 0
panic: assignment to entry in nil map

القراءة تعمل. فالـ map ذات القيمة nil تتصرف مثل map فارغة في البحث و len و range و delete، لذا يُطبع السطر الأول بشكل طبيعي. والكتابة وحدها تسبب panic. والحل هو إنشاء الـ map قبل أول كتابة، وعادةً يكون ذلك في دالة إنشاء:

example.gogo
func NewRouter() *Router {
	return &Router{routes: make(map[string]string)}
}

تحجز أدوات فك الترميز مثل json.Unmarshal الـ map بنفسها حين تجد كائن JSON، لذا يمكن لهدف من النوع map[string]any أن يبدأ بالقيمة nil. أما الكود الذي تكتبه أنت فعليه أن يستدعي make أو يستخدم صيغة حرفية.

كيف تتحقق من وجود مفتاح في map في Go؟

فهرسة map بمفتاح غير موجود تعيد القيمة الصفرية لنوع القيمة. وفي حالة العدّاد هذا ما تريده تمامًا، لأن المسار الذي لم يطلبه أحد عدد زياراته 0. أما مع القيم الأخرى فالقيمة الصفرية تخفي فرقًا حقيقيًا. فـ map لخيارات الميزات (feature flags) تعيد false لخيار مُعطَّل، وتعيدها أيضًا لخيار لم يُعرَّف أصلًا.

تعيد صيغة comma-ok قيمة منطقية ثانية تخبرك هل كان المفتاح موجودًا:

example.gogo
package main

import "fmt"

func main() {
	flags := map[string]bool{
		"new_checkout": true,
		"dark_mode":    false,
	}

	for _, name := range []string{"new_checkout", "dark_mode", "beta_search"} {
		enabled, ok := flags[name]
		if !ok {
			fmt.Printf("%s: unknown flag\n", name)
			continue
		}
		fmt.Printf("%s: %v\n", name, enabled)
	}
}
example.texttext
new_checkout: true
dark_mode: false
beta_search: unknown flag

لا تكون ok مساوية لـ false إلا مع beta_search. ودونها سيؤدي خطأ إملائي في اسم الخيار إلى تعطيل الميزة بصمت. استخدم الصيغة البسيطة حين تكون القيمة الصفرية إجابة صحيحة، مثل الأعداد والمجاميع. واستخدم comma-ok حين يعني «غير موجود» شيئًا مختلفًا عن «صفر».

كيف تحذف المفاتيح وتفرّغ الـ map؟

تحذف delete(m, k) مفتاحًا واحدًا. وحذف مفتاح غير موجود لا يفعل شيئًا، ولا يُعدّ خطأ. أما clear(m)، التي أُضيفت في Go 1.21، فتحذف كل المدخلات وتُبقي الـ map محجوزة (ملاحظات إصدار Go 1.21):

example.gogo
cache := map[int]string{42: "[email protected]", 7: "[email protected]"}

delete(cache, 42)
delete(cache, 999)
fmt.Println(len(cache), cache)

clear(cache)
fmt.Println(len(cache), cache == nil)
example.texttext
1 map[7:[email protected]]
0 false

بعد clear تصبح الـ map فارغة لكنها تبقى صالحة للاستخدام، وكل متغير يشير إليها يرى الـ map الفارغة. أما إسناد cache = map[int]string{} بدلًا من ذلك فيعطي map جديدة لهذا المتغير وحده.

حذف المفاتيح أثناء المرور على الـ map نفسها مسموح. وتنص المواصفات على أن المدخل المحذوف أثناء التكرار لن يظهر لاحقًا في الحلقة (مواصفات Go). وإضافة مفاتيح أثناء التكرار مسموحة أيضًا، لكن المدخل الجديد قد يظهر في الحلقة نفسها وقد لا يظهر.

لماذا يكون ترتيب التكرار على الـ map عشوائيًا في Go؟

تنص المواصفات على أن ترتيب التكرار على map غير محدد، وليس مضمونًا أن يبقى كما هو من تكرار إلى آخر. بل إن وقت التشغيل يختار نقطة بداية عشوائية في كل range. وقد طبعت ثلاث مرات تشغيل للحلقة نفسها على hits ما يلي:

example.texttext
/api/orders 41 /healthz 120 /api/users 17
/api/orders 41 /healthz 120 /api/users 17
/api/users 17 /api/orders 41 /healthz 120

هذه العشوائية مقصودة. ففي إصدارات Go الأولى كان الترتيب يبدو ثابتًا مع الـ maps الصغيرة، وبدأت البرامج تعتمد عليه. وحين تغيّر التنفيذ تعطلت تلك البرامج. وجعل الترتيب عشوائيًا يُظهر الخلل في اختباراتك بدلًا من بيئة الإنتاج (مدونة Go، Go maps in action).

حين تحتاج إلى ترتيب ثابت، في سطر سجل أو تقرير أو اختبار، رتّب المفاتيح. ومنذ Go 1.23 تعيد maps.Keys مُكرِّرًا (iterator)، وتجمعه slices.Sorted وترتّبه في استدعاء واحد:

example.gogo
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	hits := map[string]int{"/api/orders": 41, "/healthz": 120, "/api/users": 17}

	for _, path := range slices.Sorted(maps.Keys(hits)) {
		fmt.Printf("%-12s %d\n", path, hits[path])
	}
}
example.texttext
/api/orders  41
/api/users   17
/healthz     120

تطبع fmt.Println الـ map كاملةً بمفاتيح مرتبة أصلًا، ولهذا كان خرج fmt.Println(cache) أعلاه ثابتًا. وترتّب encoding/json أيضًا مفاتيح الـ map عند التحويل إلى JSON. فالعشوائية لا تخص إلا range.

ما الأنواع التي يمكن أن تكون مفاتيح لـ map في Go؟

يجب أن يكون نوع المفتاح قابلًا للمقارنة، أي أن العاملين == و != معرّفان له. والسلاسل النصية والأعداد الصحيحة والأعداد العشرية والقيم المنطقية والمؤشرات والقنوات والـ interfaces كلها قابلة للمقارنة. وكذلك الـ structs والمصفوفات التي تكون كل حقولها أو عناصرها قابلة للمقارنة. أما الـ slices والـ maps والدوال فليست كذلك، ويرفضها المترجم:

example.gogo
seen := map[[]string]bool{}
example.texttext
./main.go:4:14: invalid map key type []string

مفاتيح الـ struct مفيدة حين يتكون المفتاح من أكثر من جزء. هنا يُخزَّن زمن استجابة كل endpoint بمفتاح يجمع الطريقة (method) والمسار معًا:

example.gogo
type Endpoint struct {
	Method string
	Path   string
}

latencyMs := map[Endpoint]int{
	{"GET", "/api/orders"}:  38,
	{"POST", "/api/orders"}: 112,
}
fmt.Println(latencyMs[Endpoint{"POST", "/api/orders"}])

يطبع هذا 112. فالـ struct تُقارَن حقلًا بحقل، لذا تجد قيمتان من Endpoint لهما الطريقة والمسار نفساهما المدخل نفسه. وهذا يغنيك عن الحيلة القديمة التي تبني سلسلة نصية مفتاحًا مثل "POST /api/orders" ثم تقسمها لاحقًا من جديد. ولاستخدام قائمة معرّفات مفتاحًا، حوّل الـ slice إلى مصفوفة ثابتة الحجم أو اجمعها في سلسلة نصية واحدة.

مفاتيح الـ interface مثل map[any]int تُترجم، لكن إدراج slice وقت التشغيل يسبب panic بالرسالة runtime error: hash of unhashable type []string. ومفاتيح الأعداد العشرية تعمل لكنها فكرة سيئة. فالقيمة NaN لا تساوي نفسها، لذا يضيف كل m[math.NaN()] = 1 مدخلًا جديدًا لا يمكنك الوصول إليه بالبحث بعد ذلك.

هل تُمرَّر الـ maps بالمرجع في Go؟

تمرّر Go كل شيء بالقيمة، والـ maps ضمن ذلك. لكن قيمة متغير الـ map واصف صغير يشير إلى جدول التجزئة، لذا فنسخه ينسخ المؤشر لا المدخلات. والدالة التي تستقبل map تستطيع تغيير مدخلات المستدعي. أما الدالة التي تسند map جديدة إلى معاملها فلا تغيّر إلا نسختها الخاصة:

example.gogo
func recordStatus(counts map[int]int, status int) {
	counts[status]++
}

func resetCounts(counts map[int]int) {
	counts = map[int]int{}
}

func main() {
	counts := map[int]int{}
	recordStatus(counts, 200)
	recordStatus(counts, 500)
	resetCounts(counts)
	fmt.Println(counts)
}
example.texttext
map[200:1 500:1]

كتبت recordStatus عبر الجدول المشترك، لذا يظهر العددان كلاهما. أما resetCounts فاستبدلت متغيرها المحلي ولم يكن لذلك أثر على المستدعي. ولتفريغ map المستدعي، استدعِ clear(counts) داخل الدالة. ولا يبلّغ go vet عن هذه الحالة، لذا تُترجم الدالة الثانية ولا تفعل شيئًا دون أي تنبيه.

وتنطبق المشاركة نفسها على حقول الـ struct والقيم المُعادة. فدالة القراءة (getter) التي تعيد map داخلية تسمح للمستدعين بتعديل حالتك. أعِد maps.Clone(m) حين ينبغي أن يحصل المستدعون على نسختهم الخاصة.

لماذا لا يمكنني الإسناد إلى حقل struct داخل map؟

قيمة الـ map غير قابلة للعنونة (not addressable). فوقت التشغيل قد ينقل المدخلات حين ينمو الجدول، لذا لا تعطيك Go مرجعًا إلى قيمة مخزّنة داخله. ولهذا يفشل هذا الكود في الترجمة:

example.gogo
type Session struct {
	UserID   int
	Requests int
}

sessions := map[string]Session{"s_91": {UserID: 42}}
sessions["s_91"].Requests++
example.texttext
./main.go:10:2: cannot assign to struct field sessions["s_91"].Requests in map

هناك حلّان. انسخ القيمة إلى الخارج وغيّرها ثم خزّنها من جديد. أو خزّن مؤشرات، فتحمل الـ map عناوين وتعيش الـ struct خارج الجدول:

example.gogo
s := sessions["s_91"]
s.Requests++
sessions["s_91"] = s

byToken := map[string]*Session{"s_91": {UserID: 42}}
byToken["s_91"].Requests++

كلا الحلّين يترك Requests مساوية لـ 1. وصيغة النسخ ثم التخزين تُبقي الـ map المالك الوحيد للبيانات. أما صيغة المؤشر فتحديثها أقصر وتتجنب نسخ الـ structs الكبيرة، لكن المفتاح غير الموجود يعطيك مؤشرًا قيمته nil، لذا يسبب byToken["s_404"].Requests++ حالة panic. تحقق أولًا باستخدام comma-ok. وللسبب نفسه لا يمكنك أخذ &sessions["s_91"].

هل الـ maps في Go آمنة للاستخدام المتزامن؟

لا. القراءات المتزامنة لا مشكلة فيها، لكن أي كتابة تتزامن مع قراءة أو كتابة أخرى تُعدّ سباق بيانات (data race). ويكتشف وقت التشغيل بعض هذه السباقات فيوقف البرنامج كله بخطأ قاتل (fatal error) لا تستطيع recover التقاطه. فثمانية goroutines تزيد map عدّاد مشتركة تنهار في الحال:

example.gogo
hits := map[string]int{}
var wg sync.WaitGroup
for range 8 {
	wg.Go(func() {
		for range 10000 {
			hits["/api/orders"]++
		}
	})
}
wg.Wait()
example.texttext
fatal error: concurrent map writes

والقراءة التي تتسابق مع كتابة تعطي fatal error: concurrent map read and map write. وفي خادم HTTP يحدث هذا بمجرد أن يلمس طلبان map مشتركة، لأن كل طلب يعمل في goroutine خاصة به. شغّل اختباراتك باستخدام go test -race لاكتشاف السباقات التي يفوّتها تحقق وقت التشغيل.

والحل المعتاد هو وضع الـ map و sync.Mutex في نوع واحد، وعدم لمس الـ map إلا عبر دوال تابعة تأخذ القفل:

example.gogo
type HitCounter struct {
	mu   sync.Mutex
	hits map[string]int
}

func NewHitCounter() *HitCounter {
	return &HitCounter{hits: make(map[string]int)}
}

func (c *HitCounter) Inc(path string) {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.hits[path]++
}

func (c *HitCounter) Get(path string) int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.hits[path]
}

الآن تنتهي الـ goroutines الثماني نفسها ويعيد c.Get("/api/orders") القيمة 80000. استخدم sync.RWMutex مع RLock في Get حين تفوق القراءات الكتابات بكثير وتؤدي كل قراءة عملًا حقيقيًا.

النوع sync.Map هو map متزامنة في المكتبة القياسية. وتقول وثائقه إنه مُحسَّن لحالتين: مفاتيح تُكتب مرة واحدة وتُقرأ مرات كثيرة، مثل ذاكرة تخزين مؤقت (cache) تكبر ولا تصغر، و goroutines تعمل على مجموعات مفاتيح منفصلة (وثائق sync.Map). وخارج هاتين الحالتين تكون map عادية مع mutex أبسط في الغالب وبالسرعة نفسها، وتحتفظ بأنواع المفاتيح والقيم الخاصة بك بدلًا من any.

كيف تنشئ مجموعة (set) في Go؟

لا يوجد في Go نوع للمجموعات. والأسلوب المعتاد هو map قيمتها struct فارغة. فالنوع struct{} يشغل صفر بايت، لذا لا تخزّن الـ map إلا المفاتيح. وهذا الكود يزيل تكرار الطلبات الواردة حسب المعرّف:

example.gogo
requestIDs := []string{"req_7f", "req_a1", "req_7f", "req_c3", "req_a1"}

seen := make(map[string]struct{}, len(requestIDs))
for _, id := range requestIDs {
	if _, dup := seen[id]; dup {
		fmt.Println("duplicate, skipping", id)
		continue
	}
	seen[id] = struct{}{}
}
fmt.Println(len(seen), "unique requests")
example.texttext
duplicate, skipping req_7f
duplicate, skipping req_a1
3 unique requests

يعمل map[string]bool أيضًا وقراءته أسهل قليلًا، لأن if seen[id] لا تحتاج إلى comma-ok. لكنه يكلّف بايتًا واحدًا لكل مدخل ويسمح بحالة ثالثة، أي مفتاح مخزّن بالقيمة false. اختر أحدهما والتزم به في قاعدة الكود كلها. ويشرح مقال الكلمة المفتاحية struct في Go النوع struct{} واستخدامه الآخر إشارةً على القنوات.

ماذا تفعل الحزمة maps؟

وصلت الحزمة maps في Go 1.21 مع دوال مساعدة عامة (generic)، وأضاف Go 1.23 إليها دوال المُكرِّرات (pkg.go.dev/maps). وهذه الدوال التي ستصادفها أكثر من غيرها:

example.gogo
defaults := map[string]string{"region": "eu-west-1", "log_level": "info"}

cfg := maps.Clone(defaults)
cfg["log_level"] = "debug"
cfg["debug_token"] = "tok_123"

fmt.Println(maps.Equal(cfg, defaults))

maps.DeleteFunc(cfg, func(k, v string) bool {
	return strings.HasPrefix(k, "debug_")
})
fmt.Println(slices.Sorted(maps.Keys(cfg)))
fmt.Println(defaults["log_level"])

roles := maps.Collect(slices.All([]string{"admin", "editor", "viewer"}))
fmt.Println(roles[1])
example.texttext
false
[log_level region]
info
editor
  • تنشئ maps.Clone نسخة سطحية. فتغيير cfg لم يمسّ defaults. لكن القيم نفسها لا تُنسخ نسخًا عميقًا، لذا تظل map من الـ slices تشارك تلك الـ slices.
  • تقارن maps.Equal المفاتيح والقيم باستخدام ==. ولا يمكنك مقارنة map بأخرى باستخدام == مباشرةً، لأن الـ maps لا تُقارَن إلا بـ nil.
  • تحذف maps.DeleteFunc كل مدخل يطابق دالة شرطية (predicate).
  • تعيد maps.Keys و maps.Values و maps.All مُكرِّرات. مرّرها إلى slices.Sorted أو slices.Collect أو حلقة for range.
  • تبني maps.Collect map من أي مُكرِّر أزواج مفتاح وقيمة. وهي هنا تحوّل slice إلى map من الفهرس إلى القيمة. أما maps.Copy(dst, src) فتدمج map في أخرى.

كيف تُنفَّذ الـ maps في Go؟

منذ Go 1.24 تستخدم الـ map المدمجة تصميم Swiss Table، وهو جدول تجزئة بالعنونة المفتوحة (open addressing) يفحص مجموعات من الخانات دفعة واحدة. وتذكر ملاحظات الإصدار انخفاضًا في استهلاك المعالج لعمليات الـ map (ملاحظات إصدار Go 1.24). ولم تتغير اللغة، لذا يعمل الكود المكتوب للـ maps القديمة المبنية على الدلاء (buckets) كما كان.

وما زالت الـ maps لا تتقلص أبدًا. وحذف المدخلات يحرّر الخانات لإعادة استخدامها لكنه يحتفظ بالذاكرة. فذاكرة التخزين المؤقت التي حملت يومًا مليون جلسة تحتفظ بتلك الذاكرة بعد حذفها. ولا تحرّرها clear أيضًا. وإذا ارتفع حجم map طويلة العمر ثم انخفض، فانسخ المدخلات المتبقية إلى map جديدة باستخدام maps.Clone أو make جديد، واترك جامع القمامة يتخلص من القديمة.

أين يأتي دور LevelUpGo

يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تغطي دورة Composite Types الـ maps إلى جانب الـ slices والـ structs، بما في ذلك البحث بصيغة comma-ok والعدّ والحذف والتكرار بترتيب ثابت. وتغطي دورة Concurrency Fundamentals الـ goroutines و WaitGroups والـ mutexes التي تحتاج إليها حين تصبح map مشتركة بينها. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.

الأسئلة الشائعة

هل map كلمة مفتاحية في Go؟

نعم. map واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك تسمية متغير أو دالة map. وهي تظهر في أنواع الـ map مثل map[string]int، وفي الصيغ الحرفية للـ map، ووسيطًا لـ make.

هل الـ map في Go مرتبة؟

لا. تترك المواصفات ترتيب التكرار غير محدد، ويجعله وقت التشغيل عشوائيًا في كل range. ولمعالجة المفاتيح بالترتيب، رتّبها باستخدام slices.Sorted(maps.Keys(m)). أما fmt.Println و json.Marshal فتطبعان مفاتيح الـ map مرتبة أصلًا.

هل الـ maps في Go آمنة مع الخيوط (thread-safe)؟

لا. القراءات المتزامنة آمنة، لكن أي كتابة إلى جانب قراءة أو كتابة أخرى قد تُسقط البرنامج بالرسالة fatal error: concurrent map writes. احمِ الـ map باستخدام sync.Mutex أو sync.RWMutex، أو استخدم sync.Map مع ذاكرات التخزين المؤقت التي تُكتب مرة واحدة ومع مجموعات المفاتيح المنفصلة.

كيف تحصل على طول map في Go؟

استدعِ len(m). فهي تعيد عدد المدخلات، وتعمل مع map قيمتها nil حيث تعيد 0. ولا تدعم الـ maps الدالة cap.

هل يوجد في Go نوع للمجموعات (set)؟

لا. استخدم map[T]struct{} الذي لا يخزّن إلا المفاتيح، وتحقق من العضوية باستخدام _, ok := set[k]. ويعمل map[T]bool أيضًا إذا كنت تفضّل if set[k] على comma-ok.

ماذا يحدث حين تقرأ مفتاحًا غير موجود؟

تحصل على القيمة الصفرية لنوع القيمة، مثل 0 أو "" أو false أو nil، دون أي خطأ. استخدم صيغة القيمتين v, ok := m[k] حين تحتاج إلى التمييز بين مفتاح غير موجود وقيمة صفرية مخزّنة.

المصادر

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

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

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