for هي الكلمة المفتاحية الوحيدة للحلقات في Go. ولها ثلاثة أشكال: الحلقة ذات البنود الثلاثة for i := 0; i < n; i++، والحلقة ذات الشرط فقط for cond، وهي حلقة while في Go، والحلقة اللانهائية for { ... }. وأضف إليها range لتمرّ الكلمة المفتاحية نفسها على الـ slices والمصفوفات والـ maps والسلاسل النصية والـ channels والأعداد الصحيحة ودوال المكرِّرات (iterators). ولا توجد في Go الكلمات المفتاحية while أو do أو foreach أو loop (مواصفات Go، جمل For).
الخلاصة
for init; cond; post { ... }هي حلقة العدّ. لا أقواس حول البنود، والأقواس المعقوفة إلزامية.for cond { ... }حلقة while، وfor { ... }تستمر حتىbreakأوreturnأوpanic.- تمرّ
for i, v := range xعلى slice أو مصفوفة أو map أو سلسلة نصية أو channel أو عدد صحيح (Go 1.22) أو دالة مكرِّر (Go 1.23). وترتيب الـ map عشوائي. - منذ Go 1.22، تحصل كل دورة على متغير حلقة جديد إذا صرّح
go.modبـgo 1.22أو أحدث. فلم تعد الـ goroutines التي تبدأ داخل حلقة ترى كلها القيمة الأخيرة. - قيمة range نسخة. عيّن القيمة إلى
items[i]لتغيير الـ slice. - الـ
breakدون تسمية داخلselectأوswitchلا يغادر إلا تلك الجملة. استخدم تسمية لمغادرة الحلقة.
ما أشكال حلقة for في Go؟
تعرّف المواصفات جملة for واحدة بثلاثة أشكال. ولا تختلف إلا في البنود التي تكتبها.
الحلقة ذات البنود الثلاثة
تُنفَّذ جملة التهيئة مرة واحدة، ويُفحص الشرط قبل كل دورة، وتُنفَّذ الجملة اللاحقة بعد كل دورة. تعيد هذه الحلقة محاولة فحص صحة (health check) متقلّب مع تراجع أُسّي (exponential backoff):
example.gogopackage main import ( "errors" "fmt" "time" ) var errUnavailable = errors.New("503 service unavailable") func fetchStatus(attempt int) error { if attempt < 3 { return errUnavailable } return nil } func main() { backoff := 100 * time.Millisecond for attempt := 1; attempt <= 5; attempt++ { err := fetchStatus(attempt) if err == nil { fmt.Printf("attempt %d: ok\n", attempt) break } fmt.Printf("attempt %d: %v, retrying in %v\n", attempt, err, backoff) time.Sleep(backoff) backoff *= 2 } }
example.texttextattempt 1: 503 service unavailable, retrying in 100ms attempt 2: 503 service unavailable, retrying in 200ms attempt 3: ok
لا يوجد attempt إلا داخل الحلقة. ويمكن حذف أي من البنود الثلاثة. فالشكل for ; attempt <= 5; { صحيح، ويعيد gofmt كتابته إلى for attempt <= 5 {.
أما عادة C في إحاطة البنود بأقواس فلا تُترجَم:
example.gogofor (i := 0; i < 3; i++) {
example.texttext./main.go:6:9: syntax error: unexpected :=, expected )
الحلقة ذات الشرط فقط (while)
احذف جملة التهيئة والجملة اللاحقة فتصبح for حلقة while. وهذه الحلقة تعالج طابور مهام (job queue) حتى يفرغ:
example.gogopackage main import "fmt" func main() { queue := []string{"resize:42.png", "resize:43.png", "thumbnail:44.png"} for len(queue) > 0 { job := queue[0] queue = queue[1:] fmt.Println("processing", job) } fmt.Println("queue empty") }
example.texttextprocessing resize:42.png processing resize:43.png processing thumbnail:44.png queue empty
كتابة while بدلًا منها تعطي خطأ في التحليل، لأن while معرّف عادي في Go:
example.texttext./main.go:7:8: syntax error: unexpected name pending at end of statement
يقرأ المترجم while على أنها تعبير، ثم يجد اسمًا آخر بعدها فيتوقف. ولا تذكر الرسالة شيئًا عن الحلقات، ولهذا تربك القادمين من لغات أخرى.
الحلقة اللانهائية
دون أي بنود، تستمر for { ... } حتى يغادرها شيء ما من داخلها. وتستخدم الخوادم هذا الشكل لحلقات قبول الاتصالات (accept loops) وحلقات الاستطلاع (pollers):
example.gogofor { conn, err := ln.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return } log.Printf("accept: %v", err) continue } go handle(conn) }
تنتهي الحلقة حين يُغلَق المستمع (listener) وتعيد Accept القيمة net.ErrClosed. وتحل for {} محل while (true) و for (;;)، ويعرف المترجم أنها لا تنتهي من تلقاء نفسها. فالدالة التي تنتهي بـ for {} دون break لا تحتاج إلى return في آخرها.
كيف تعمل for range؟
تحوّل range حلقة for إلى مرور على قيمة. وما تحصل عليه يعتمد على النوع:
| المرور على | القيمة الأولى | القيمة الثانية |
|---|---|---|
| slice أو مصفوفة | الفهرس | نسخة من العنصر |
| سلسلة نصية | موضع البايت | rune (نقطة ترميز Unicode) |
| map | المفتاح | القيمة |
| channel | القيمة المستلَمة | لا شيء |
عدد صحيح n (Go 1.22) | من 0 إلى n-1 | لا شيء |
| دالة مكرِّر (Go 1.23) | ما تُنتجه | ما تُنتجه |
يمكنك حذف المتغير الثاني (for i := range items) أو تجاهل الأول بـ _ (for _, item := range items). وتنفّذ for range n دون متغيرات الكتلة n مرة.
example.gogopackage main import ( "fmt" "maps" "slices" ) func main() { endpoints := []string{"/health", "/orders", "/users"} for i, path := range endpoints { fmt.Println(i, path) } statusCounts := map[int]int{200: 812, 404: 17, 500: 3} for _, code := range slices.Sorted(maps.Keys(statusCounts)) { fmt.Printf("%d: %d\n", code, statusCounts[code]) } for offset, r := range "café" { fmt.Printf("%d:%c ", offset, r) } fmt.Println() for attempt := range 3 { fmt.Print(attempt, " ") } fmt.Println() results := make(chan string, 2) results <- "job 1 done" results <- "job 2 done" close(results) for msg := range results { fmt.Println(msg) } }
example.texttext0 /health 1 /orders 2 /users 200: 812 404: 17 500: 3 0:c 1:a 2:f 3:é 0 1 2 job 1 done job 2 done
حلقة الـ map هي التي تحتاج إلى انتباه. فبيئة التشغيل (runtime) تجعل ترتيب المرور على الـ map عشوائيًا عن قصد، حتى لا يعتمد عليه الكود. ولطباعة رموز الحالة بالترتيب، تعيد maps.Keys مكرِّرًا على المفاتيح، وتجمعها slices.Sorted وترتّبها. ويشرح مقال الكلمة المفتاحية map في Go المرور على الـ maps بتفصيل أكبر.
وحلقة السلسلة النصية تعطي قيمًا من نوع rune، والمواضع فيها مواضع بايتات، لذا يبدأ é عند الموضع 3 ويشغل بايتين. استخدم for i := 0; i < len(s); i++ مع s[i] حين تريد البايتات الخام.
أما حلقة الـ channel فتتوقف حين تُغلَق الـ channel. وإذا لم يغلقها أحد، تبقى الحلقة معلّقة إلى الأبد بعد أن يفرغ المخزن المؤقت. ويشرح مقال الكلمة المفتاحية chan في Go من يجب أن يغلق الـ channel.
تسمح دوال المكرِّرات (Go 1.23) لأي دالة بالتوقيع func(yield func(K, V) bool) بالعمل مع range. وتعيدها المكتبة القياسية من maps.Keys و maps.All و slices.All و slices.Backward و strings.Lines، ويمكنك كتابة مكرِّراتك الخاصة لنتائج API مقسّمة على صفحات أو لصفوف قاعدة البيانات. ولـ range قواعد أكثر مما يتسع له هذا المقال. وقسم range في دليل الكلمات المفتاحية في Go هو النسخة المختصرة.
ما الذي تغيّر في متغيرات الحلقة في Go 1.22؟
قبل Go 1.22، كانت حلقة for تصرّح عن متغيراتها مرة واحدة وتعيد استخدامها في كل دورة. فالـ closure أو الـ goroutine التي تلتقط المتغير ترى القيمة التي يحملها لحظة تنفيذها، وكانت في الغالب القيمة الأخيرة. وغيّرت Go 1.22 ذلك بحيث تحصل كل دورة على متغير جديد (مدونة Go، Fixing For Loops in Go 1.22).
يبدأ فاحص الصحة هذا goroutine واحدة لكل خادم قاعدة بيانات:
example.gogopackage main import ( "fmt" "sync" ) func main() { hosts := []string{"db-1", "db-2", "db-3"} var wg sync.WaitGroup for _, host := range hosts { wg.Add(1) go func() { defer wg.Done() fmt.Println("checking", host) }() } wg.Wait() }
مع go 1.21 في go.mod، طبعت ثلاثة تشغيلات السطر checking db-3 ثلاث مرات في كل منها، وأبلغ go vet عن:
example.texttextmain.go:16:28: loop variable host captured by func literal
ومع go 1.27 في go.mod، يفحص الكود نفسه الخوادم الثلاثة بأي ترتيب تنتهي به الـ goroutines، ولا يجد go vet ما يقوله.
يرتبط هذا التغيير بسطر go في go.mod، لا بسلسلة الأدوات (toolchain) التي تبني بها. فمترجم Go 1.27 ما زال يبني الوحدة (module) التي تصرّح بـ go 1.21 بالدلالات القديمة، لذا تستمر الاعتمادية المكتوبة قبل 1.22 في العمل كما اختبرها مؤلفوها. وينطبق التغيير على الشكل ذي البنود الثلاثة وعلى range معًا. والحل الالتفافي القديم host := host داخل الحلقة أصبح زائدًا عن الحاجة. ويحذفه عنك محدِّث الكود (modernizer) المسمى forvar في go fix. ويشرح مقال الكلمة المفتاحية func في Go الـ closures والمتغيرات الملتقَطة بتفصيل أكبر.
لماذا لا يغيّر تعديلُ قيمة range الـ slice؟
متغير range الثاني نسخة من العنصر. وفي slice من الـ structs، يترك تغيير حقل في النسخة الـ slice كما هي:
example.gogopackage main import "fmt" type Invoice struct { ID string Amount int Paid bool } func main() { invoices := []Invoice{ {ID: "inv-101", Amount: 4200}, {ID: "inv-102", Amount: 1800}, } for _, inv := range invoices { inv.Paid = true } fmt.Println(invoices[0].Paid, invoices[1].Paid) for i := range invoices { invoices[i].Paid = true } fmt.Println(invoices[0].Paid, invoices[1].Paid) }
example.texttextfalse false true true
تعيّن الحلقة الأولى Paid في نسخة تُرمى في نهاية كل دورة. أما الحلقة الثانية فتصل إلى الـ slice عبر الفهرس وتغيّر العنصر الحقيقي. ولم تغيّر Go 1.22 هذا السلوك. فتعديل 1.22 أعطى كل دورة متغيرها الخاص، وهذا المتغير ما زال نسخة. وفي slice من المؤشرات ([]*Invoice)، تكون النسخة مؤشرًا، فتحدّث inv.Paid = true الفاتورة فعلًا.
الاحتفاظ بمؤشر إلى عنصر
يعطيك &invoices[i] مؤشرًا إلى المصفوفة الأساسية (backing array) للـ slice. ويصبح هذا المؤشر قديمًا إذا تجاوز حجم الـ slice سعتها لاحقًا، لأن append تنسخ العناصر إلى مصفوفة جديدة:
example.gogovar large *Invoice for i := range invoices { if invoices[i].Amount > 4000 { large = &invoices[i] } } invoices = append(invoices, Invoice{"inv-103", 900}) large.Amount = 0 fmt.Println(invoices[0].Amount, large.Amount)
example.texttext4200 0
ما زال large يشير إلى المصفوفة القديمة، لذا لا تصل الكتابة إلى invoices أبدًا. احتفظ بالفهرس (largeIdx = i) بدل المؤشر حين يمكن أن يكبر الـ slice.
كيف تعمل break و continue في حلقة for؟
تغادر break أعمق for أو switch أو select. وتنتقل continue إلى الدورة التالية من أعمق for. وتقبل كلتاهما تسمية اختيارية تشير إلى حلقة خارجية.
التسمية معرّف تليه نقطتان، وتوضع على السطر الذي يسبق الحلقة. وهذا الفحص يتوقف عند أول خطأ عبر عدة ملفات سجلات:
example.gogopackage main import "fmt" func main() { logLines := [][]string{ {"INFO start", "INFO ready"}, {"INFO request", "ERROR db timeout", "INFO retry"}, {"INFO shutdown"}, } files: for file, lines := range logLines { for _, line := range lines { if line[:5] == "ERROR" { fmt.Printf("file %d: first error %q\n", file, line) break files } } } }
example.texttextfile 1: first error "ERROR db timeout"
دون التسمية، لن تغادر break إلا الحلقة الداخلية، وسينتقل الفحص إلى الملف الثالث. أما continue files فتتخطى بقية الملف الحالي وتنتقل إلى الملف التالي.
break داخل select
هذا الخطأ يُترجَم دون مشاكل، ويبدو صحيحًا عند قراءة سريعة. عامل (worker) يقرأ المهام حتى يُلغى الـ context الخاص به:
example.gogofunc worker(ctx context.Context, jobs <-chan string) { for { select { case <-ctx.Done(): fmt.Println("worker stopping:", ctx.Err()) break case job := <-jobs: fmt.Println("handled", job) } } fmt.Println("worker flushed metrics") }
تغادر break جملة select، لا حلقة for. فيطبع العامل worker stopping ويعود مباشرة إلى select، حيث ما زالت ctx.Done() مغلقة، فيطبع السطر نفسه مرة أخرى في حلقة محمومة. ولا تُفرَّغ المقاييس أبدًا. ويُبلغ go vet عن الكود الميت بعد الحلقة:
example.texttextmain.go:20:2: unreachable code
ويُبلغ staticcheck عن جملة break نفسها:
example.texttextmain.go:15:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)
والحل هو وضع تسمية على الحلقة:
example.gogofunc worker(ctx context.Context, jobs <-chan string) { loop: for { select { case <-ctx.Done(): fmt.Println("worker stopping:", ctx.Err()) break loop case job := <-jobs: fmt.Println("handled", job) } } fmt.Println("worker flushed metrics") }
example.texttexthandled email:1 handled email:2 worker stopping: context deadline exceeded worker flushed metrics
وتنجح return في حالة ctx.Done() أيضًا، حين لا يوجد ما يُنفَّذ بعد الحلقة. وينطبق الفخ نفسه على break داخل switch ضمن حلقة.
أخطاء شائعة في حلقات for
defer داخل حلقة
تُنفَّذ defer حين تعود الدالة، لا حين تنتهي الدورة. فالحلقة التي تفتح ملفًا وتؤجّل Close تُبقي كل الملفات مفتوحة حتى تنتهي الدالة كلها:
example.gogofor _, p := range paths { f, err := os.Open(p) if err != nil { return 0, err } defer f.Close() // ... }
لن تلاحظ المشكلة مع بضعة مسارات، لكن عشرة آلاف ملف سجلات ستستنفد واصفات الملفات (file descriptors) لدى العملية. ولا ينبّه إلى ذلك go vet ولا staticcheck. انقل جسم الحلقة إلى دالة حتى تُنفَّذ كل defer في نهاية استدعائها الخاص، أو استدعِ f.Close() مباشرة في نهاية الدورة. ويشرح مقال أخطاء Go الشائعة التي يجب تجنبها هذا الخطأ بمثال كامل.
الإلحاق بـ slice أثناء المرور عليها بـ range
تقيّم range المعامل الخاص بها مرة واحدة، قبل الدورة الأولى. والإلحاق داخل الحلقة لا يجعلها تدوم أطول:
example.gogourls := []string{"/", "/about"} for _, u := range urls { urls = append(urls, u+"/sitemap.xml") } fmt.Println(len(urls), urls)
example.texttext4 [/ /about //sitemap.xml /about/sitemap.xml]
نُفّذت الحلقة مرتين، مرة لكل عنصر أصلي. وهذا يجعلها آمنة، لكنه يعني أيضًا أن زاحف الويب (crawler) لا يستطيع اكتشاف عناوين URL جديدة بهذه الطريقة. استخدم حلقة شرطية على طابور، مثل for len(queue) > 0 أعلاه، حين تكبر قائمة العمل أثناء التنفيذ.
تعديل map أثناء range
حذف المفتاح الحالي أثناء المرور على map مسموح وآمن. وهذه الحلقة تحذف الجلسات الخاملة:
example.gogofor id, idle := range sessions { if idle > 30 { delete(sessions, id) } }
إضافة مفاتيح أثناء الحلقة مسموحة أيضًا، لكن المواصفات تقول إن المُدخل الجديد «قد يظهر أثناء المرور وقد يُتخطّى» ("may be produced during the iteration or may be skipped"). اجمع المفاتيح الجديدة في map منفصلة وادمجها بعد ذلك إذا كان يجب معالجة كل واحد منها.
أخذ عنوان متغير الحلقة
تعطيك ptrs = append(ptrs, &inv) داخل for _, inv := range invoices مؤشرات إلى نسخ، لا إلى عناصر الـ slice. وقبل Go 1.22 كانت كل المؤشرات تشير إلى المتغير نفسه وتعرض الفاتورة الأخيرة. ومنذ Go 1.22 يشير كل مؤشر إلى نسخته الخاصة، وهذا صحيح لكنه يبقى منفصلًا عن الـ slice. استخدم &invoices[i] إذا أردت تعديل العناصر الأصلية، وتذكّر تحذير السعة أعلاه.
هل توجد حلقة while أو do-while في Go؟
لا. يقول Effective Go إن حلقة for في Go «توحّد for و while، ولا توجد do-while». ويغطي الشكل ذو الشرط فقط ما تستخدم له اللغات الأخرى while.
تنفّذ do-while جسم الحلقة مرة واحدة قبل فحص الشرط. وفي Go تكتبها حلقة لانهائية يأتي الفحص في آخرها. واستدعاءات API المقسّمة على صفحات هي الحالة المعتادة، لأنك تجلب الصفحة الأولى دائمًا:
example.gogocursor := "" for { page := fetchPage(cursor) fmt.Println("fetched", page.Items) cursor = page.Next if cursor == "" { break } }
example.texttextfetched [order-1 order-2] fetched [order-3]
هذا أطول بسطرين مما كانت ستكون عليه do { } while (cursor != "")، وشرط الخروج في الأسفل حيث تتوقع أن تبحث عنه.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تحتوي دورة Go Basics على درسين عن for: أحدهما عن أشكال الحلقة الثلاثة، والآخر عن range و break و continue. وتغطي دورة Concurrency Fundamentals الحلقات التي توقع المبرمجين في الأخطاء في الكود المتزامن، مثل تشغيل goroutines داخل حلقة، والمرور على الـ channels، ومغادرة حلقة for/select عند الإلغاء. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل توجد حلقة while في Go؟
لا، لا توجد في Go الكلمة المفتاحية while. اكتب for cond { ... } لحلقة while، و for { ... } لحلقة لانهائية. وكتابة while تعطي خطأ نحويًا، لأن المترجم يعاملها كاسم عادي.
هل توجد حلقة foreach في Go؟
for ... range هي foreach في Go. تمرّ for _, v := range items على كل عنصر في slice، ويعمل الشكل نفسه مع المصفوفات والـ maps والسلاسل النصية والـ channels والأعداد الصحيحة ودوال المكرِّرات.
كيف أكرر حلقة N مرة في Go؟
منذ Go 1.22، تُنفَّذ for i := range n وقيمة i من 0 إلى n-1، وتُنفَّذ for range n عدد n من المرات دون متغير. ويعمل الشكل ذو البنود الثلاثة for i := 0; i < n; i++ في كل الإصدارات.
كيف أمرّ على map بترتيب مُرتَّب؟
اجمع المفاتيح ورتّبها، ثم مرّ عليها بـ range. ومنذ Go 1.23، تفعل for _, k := range slices.Sorted(maps.Keys(m)) الأمرين في سطر واحد. وترتيب المرور على الـ map عشوائي عن قصد، فلا تعتمد عليه أبدًا.
كيف أمرّ على slice بالعكس في Go؟
استخدم for i := len(s) - 1; i >= 0; i--، أو منذ Go 1.23، for i, v := range slices.Backward(s)، التي تُنتج الفهرس والقيمة من العنصر الأخير إلى الأول.
لماذا تطبع الـ goroutine داخل حلقة for القيمة نفسها؟
وحدتك (module) تستخدم إصدارًا من Go أقدم من 1.22، حيث تتشارك كل الدورات متغير الحلقة نفسه. غيّر سطر go في go.mod إلى go 1.22 أو أحدث، فتحصل كل دورة على متغيرها الخاص. وفي إصدار أقدم، مرّر القيمة كوسيط (go func(h string) { ... }(host)) أو انسخها بـ host := host داخل الحلقة.
المصادر
- The Go Programming Language Specification, For statements: https://go.dev/ref/spec#For_statements
- The Go Programming Language Specification, Break statements: https://go.dev/ref/spec#Break_statements
- Effective Go, For: https://go.dev/doc/effective_go#for
- The Go Blog, Fixing For Loops in Go 1.22: https://go.dev/blog/loopvar-preview
- Go 1.22 Release Notes: https://go.dev/doc/go1.22
- Go 1.23 Release Notes: https://go.dev/doc/go1.23
- The Go Blog, Range Over Function Types: https://go.dev/blog/range-functions
- staticcheck SA4011: https://staticcheck.dev/docs/checks/#SA4011
