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

الكلمة المفتاحية for في Go: الحلقات و range ومتغيرات الحلقة والتسميات

كيف تعمل الكلمة المفتاحية for في Go: أشكال الحلقة الثلاثة، و for range على الـ slices والـ maps والسلاسل النصية والـ channels والأعداد الصحيحة والمكرِّرات، ومتغيرات الحلقة في Go 1.22، و break و continue مع التسميات، والأخطاء الشائعة في الحلقات.

الكلمة المفتاحية for في Go: الحلقات و range ومتغيرات الحلقة والتسميات

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.gogo
package 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.texttext
attempt 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.gogo
	for (i := 0; i < 3; i++) {
example.texttext
./main.go:6:9: syntax error: unexpected :=, expected )

الحلقة ذات الشرط فقط (while)

احذف جملة التهيئة والجملة اللاحقة فتصبح for حلقة while. وهذه الحلقة تعالج طابور مهام (job queue) حتى يفرغ:

example.gogo
package 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.texttext
processing 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.gogo
	for {
		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.gogo
package 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.texttext
0 /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.gogo
package 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.texttext
main.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.gogo
package 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.texttext
false false
true true

تعيّن الحلقة الأولى Paid في نسخة تُرمى في نهاية كل دورة. أما الحلقة الثانية فتصل إلى الـ slice عبر الفهرس وتغيّر العنصر الحقيقي. ولم تغيّر Go 1.22 هذا السلوك. فتعديل 1.22 أعطى كل دورة متغيرها الخاص، وهذا المتغير ما زال نسخة. وفي slice من المؤشرات ([]*Invoice)، تكون النسخة مؤشرًا، فتحدّث inv.Paid = true الفاتورة فعلًا.

الاحتفاظ بمؤشر إلى عنصر

يعطيك &invoices[i] مؤشرًا إلى المصفوفة الأساسية (backing array) للـ slice. ويصبح هذا المؤشر قديمًا إذا تجاوز حجم الـ slice سعتها لاحقًا، لأن append تنسخ العناصر إلى مصفوفة جديدة:

example.gogo
	var 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.texttext
4200 0

ما زال large يشير إلى المصفوفة القديمة، لذا لا تصل الكتابة إلى invoices أبدًا. احتفظ بالفهرس (largeIdx = i) بدل المؤشر حين يمكن أن يكبر الـ slice.

كيف تعمل break و continue في حلقة for؟

تغادر break أعمق for أو switch أو select. وتنتقل continue إلى الدورة التالية من أعمق for. وتقبل كلتاهما تسمية اختيارية تشير إلى حلقة خارجية.

التسمية معرّف تليه نقطتان، وتوضع على السطر الذي يسبق الحلقة. وهذا الفحص يتوقف عند أول خطأ عبر عدة ملفات سجلات:

example.gogo
package 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.texttext
file 1: first error "ERROR db timeout"

دون التسمية، لن تغادر break إلا الحلقة الداخلية، وسينتقل الفحص إلى الملف الثالث. أما continue files فتتخطى بقية الملف الحالي وتنتقل إلى الملف التالي.

break داخل select

هذا الخطأ يُترجَم دون مشاكل، ويبدو صحيحًا عند قراءة سريعة. عامل (worker) يقرأ المهام حتى يُلغى الـ context الخاص به:

example.gogo
func 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.texttext
main.go:20:2: unreachable code

ويُبلغ staticcheck عن جملة break نفسها:

example.texttext
main.go:15:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)

والحل هو وضع تسمية على الحلقة:

example.gogo
func 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.texttext
handled email:1
handled email:2
worker stopping: context deadline exceeded
worker flushed metrics

وتنجح return في حالة ctx.Done() أيضًا، حين لا يوجد ما يُنفَّذ بعد الحلقة. وينطبق الفخ نفسه على break داخل switch ضمن حلقة.

أخطاء شائعة في حلقات for

defer داخل حلقة

تُنفَّذ defer حين تعود الدالة، لا حين تنتهي الدورة. فالحلقة التي تفتح ملفًا وتؤجّل Close تُبقي كل الملفات مفتوحة حتى تنتهي الدالة كلها:

example.gogo
	for _, 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.gogo
	urls := []string{"/", "/about"}
	for _, u := range urls {
		urls = append(urls, u+"/sitemap.xml")
	}
	fmt.Println(len(urls), urls)
example.texttext
4 [/ /about //sitemap.xml /about/sitemap.xml]

نُفّذت الحلقة مرتين، مرة لكل عنصر أصلي. وهذا يجعلها آمنة، لكنه يعني أيضًا أن زاحف الويب (crawler) لا يستطيع اكتشاف عناوين URL جديدة بهذه الطريقة. استخدم حلقة شرطية على طابور، مثل for len(queue) > 0 أعلاه، حين تكبر قائمة العمل أثناء التنفيذ.

تعديل map أثناء range

حذف المفتاح الحالي أثناء المرور على map مسموح وآمن. وهذه الحلقة تحذف الجلسات الخاملة:

example.gogo
	for 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.gogo
	cursor := ""
	for {
		page := fetchPage(cursor)
		fmt.Println("fetched", page.Items)
		cursor = page.Next
		if cursor == "" {
			break
		}
	}
example.texttext
fetched [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 داخل الحلقة.

المصادر

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

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

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