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

الكلمة المفتاحية else في Go: if/else و else if والإرجاع المبكر والنطاق

كيف تعمل الكلمة المفتاحية else في Go: صياغة if/else، ولماذا يجب أن تكون else على سطر القوس المغلق نفسه، وسلاسل else if، ونطاق جملة if، والإرجاع المبكر، وغياب المعامل الثلاثي.

الكلمة المفتاحية else في Go: if/else و else if والإرجاع المبكر والنطاق

تنفّذ الكلمة المفتاحية else كتلة من الكود حين يكون شرط if الذي يسبقها خاطئًا. واكتب else if لاختبار شرط آخر، ويمكنك ربط ما تشاء منها. ويجب أن تأتي else على السطر نفسه مع القوس المغلق لكتلة if، وإلا فلن يُترجَم الكود. ويستخدم كود Go الاصطلاحي else أقل مما تستخدمها معظم اللغات. فحين تنتهي كتلة if بـ return، يكون الكود الذي يلي if هو حالة else أصلًا، لذا يعود كود Go مبكرًا ويترك المسار الناجح دون إزاحة (مواصفات Go).

الخلاصة

  • تحتاج if cond { ... } else { ... } إلى أقواس معقوفة حول الكتلتين، ولا تحتاج إلى أقواس حول الشرط.
  • يجب أن تكون } else { على سطر واحد. فالسطر الجديد بعد } ينهي جملة if، ويبلّغ المترجم عن الخطأ syntax error: unexpected keyword else, expected }.
  • تُفحص سلاسل else if من الأعلى إلى الأسفل، ويفوز أول شرط صحيح. والسلاسل الطويلة تُقرأ بشكل أفضل على شكل switch دون تعبير.
  • المتغير المُصرَّح عنه في الجملة القصيرة لـ if (if v, err := f(); err != nil) مرئي في كل فرع else if و else، ويختفي بعد } الأخيرة.
  • حين تعيد كتلة if قيمة بـ return، احذف else. وتنبّه أداة الفحص revive إلى الشكل المتبقي بالرسالة if block ends with a return statement, so drop this else and outdent its block.
  • لا يوجد في Go المعامل ?:. استخدم if/else، أو cmp.Or حين تحتاج فقط إلى قيمة افتراضية بدل القيمة الصفرية.

كيف تكتب if/else في Go؟

تأخذ if شرطًا منطقيًا وكتلة. وتعمل كتلة else التي تليها حين يكون الشرط خاطئًا. وهذا هو القرار الذي تتخذه حلقة إعادة المحاولة بعد فشل طلب:

example.gogo
package main

import "fmt"

func main() {
	attempt := 3
	maxAttempts := 5

	if attempt < maxAttempts {
		fmt.Printf("attempt %d failed, retrying\n", attempt)
	} else {
		fmt.Printf("attempt %d failed, giving up\n", attempt)
	}
}
example.texttext
attempt 3 failed, retrying

تختلف ثلاث قواعد عن C و Java و JavaScript:

  1. لا أقواس حول الشرط. يُترجَم if (attempt < maxAttempts) {، لكن gofmt يزيل الأقواس.
  2. الأقواس المعقوفة إلزامية، حتى لجسم من سطر واحد. يفشل if code >= 500 fmt.Println("server error") بالخطأ syntax error: unexpected name fmt, expected {.
  3. يجب أن يكون الشرط من النوع bool. لا توجد في Go قيم تُعامَل على أنها صحيحة ضمنيًا (truthy)، لذا فإن if len(items) و if user أخطاء في النوع. اكتب if len(items) > 0 و if user != nil.

وتنطبق قاعدة الأقواس المعقوفة نفسها على else. فالجملة المجرّدة بعد else تفشل بالخطأ syntax error: else must be followed by if or statement block. ولا يمكن أن يليها إلا if أخرى أو كتلة { ... }. ويشرح مقال الكلمة المفتاحية if في Go الشروط وجمل التهيئة من جهة if.

لماذا يجب أن تكون else على السطر نفسه مع }؟

تستخدم قواعد Go النحوية الفواصل المنقوطة لإنهاء الجمل، لكنك لا تكتبها تقريبًا أبدًا. فالمحلل المعجمي (lexer) يُدرج فاصلة منقوطة في نهاية السطر حين يكون آخر رمز فيه معرّفًا، أو قيمة حرفية، أو إحدى الكلمات المفتاحية break أو continue أو fallthrough أو return، أو معاملًا مثل ++، أو قوسًا مغلقًا ) أو ] أو } (مواصفات Go، الفواصل المنقوطة).

وهذه القاعدة هي سبب فشل هذا الكود:

example.gogo
	if retries > 0 {
		fmt.Println("retrying")
	}
	else {
		fmt.Println("giving up")
	}
example.texttext
./main.go:10:2: syntax error: unexpected keyword else, expected }

يحصل السطر المنتهي بـ } على فاصلة منقوطة، فتنتهي جملة if. ثم يبدأ السطر التالي جملة جديدة بـ else، ولا يمكن لأي جملة أن تبدأ بـ else. والحل هو وضع } else { على سطر واحد، وهذا أيضًا ما ينتجه gofmt. وللسبب نفسه تضع Go القوس المعقوف الافتتاحي على سطر if نفسه.

يصطدم مبرمجو C و Java الذين يفضّلون نمط Allman (القوس المعقوف على سطر مستقل) بهذه المشكلة في أسبوعهم الأول. والجانب الإيجابي أن كل قاعدة كود مكتوبة بـ Go تنسّق if/else بالطريقة نفسها، فلا يدور حول هذا التنسيق أي نقاش في مراجعة الكود.

كيف تعمل else if في Go؟

ليست else if كلمة مفتاحية مستقلة. إنها else تليها جملة if أخرى، وهو ما تسمح به القواعد النحوية مباشرة: "else" ( IfStmt | Block ). ويمكنك أن تربط منها في السلسلة ما تشاء. تفحصها Go من الأعلى إلى الأسفل وتنفّذ أول فرع يكون شرطه صحيحًا.

تصنّف هذه الحلقة أزمنة استجابة الطلبات (latencies) في فئات للمقاييس:

example.gogo
package main

import (
	"fmt"
	"time"
)

func main() {
	latencies := []time.Duration{
		40 * time.Millisecond,
		250 * time.Millisecond,
		3 * time.Second,
		90 * time.Millisecond,
	}

	var fast, slow, timedOut int
	for _, d := range latencies {
		if d < 100*time.Millisecond {
			fast++
		} else if d < time.Second {
			slow++
		} else {
			timedOut++
		}
	}
	fmt.Printf("fast=%d slow=%d timed_out=%d\n", fast, slow, timedOut)
}
example.texttext
fast=2 slow=1 timed_out=1

الترتيب مهم. بدّل أول شرطين بحيث يأتي d < time.Second أولًا، فيصبح الناتج fast=0 slow=3 timed_out=1. فكل طلب أقل من 100ms هو أيضًا أقل من ثانية واحدة، لذا يلتقطه الفحص الأوسع أولًا ولا يُنفَّذ فرع fast أبدًا. ضع النطاق الأضيق أولًا.

وحين تتجاوز السلسلة ثلاثة فروع، تكون switch دون تعبير أسهل قراءة في الغالب. فكل case شرط، وتُفحص الحالات بالترتيب نفسه من الأعلى إلى الأسفل، وتحل default محل else الأخيرة:

example.gogo
	for _, d := range latencies {
		switch {
		case d < 100*time.Millisecond:
			fast++
		case d < time.Second:
			slow++
		default:
			timedOut++
		}
	}

الناتج هو نفسه. راجع قسم switch في دليل الكلمات المفتاحية في Go لمعرفة كيف تختلف switch في Go عنها في C. وتستخدم switch و select الكلمة default لحالة «لم يطابق أي شيء آخر»، لذا لا تظهر else إلا بعد if.

ما نطاق المتغير المُصرَّح عنه في جملة if؟

يمكن أن تبدأ if بجملة قصيرة، تكون في الغالب تصريحًا بـ :=. ونطاق المتغيرات التي تصرّح عنها هو جملة if كاملة، بما في ذلك كل فرع else if و else. فالمواصفات تضع كل if في كتلة ضمنية خاصة بها، وتقع فروع else داخلها (مواصفات Go، الكتل).

وهنا يصعب الاستغناء عن else. ففحص قيمة PORT مقروءة من متغيرات البيئة يحتاج إلى الرقم المحلَّل في كل فرع:

example.gogo
package main

import (
	"fmt"
	"strconv"
)

func main() {
	for _, raw := range []string{"8080", "80", "http"} {
		if port, err := strconv.Atoi(raw); err != nil {
			fmt.Printf("PORT=%q is not a number: %v\n", raw, err)
		} else if port < 1024 {
			fmt.Printf("PORT=%d is privileged (below 1024)\n", port)
		} else {
			fmt.Printf("PORT=%d ok\n", port)
		}
	}
}
example.texttext
PORT=8080 ok
PORT=80 is privileged (below 1024)
PORT="http" is not a number: strconv.Atoi: parsing "http": invalid syntax

المتغيران port و err متاحان في الفروع الثلاثة. وبعد القوس المعقوف المغلق لا يعودان موجودين. أضف fmt.Println("listening on", port) بعد if وسيفشل البناء بالخطأ undefined: port. وإذا احتجت إلى القيمة بعد ذلك، فصرّح عنها في سطر مستقل قبل if.

فخ الحجب (shadowing)

لأن الجملة القصيرة تصرّح عن متغيرات جديدة، فقد تحجب متغيرًا خارجيًا يحمل الاسم نفسه. وهذا الكود يُترجَم ويجتاز go vet:

example.gogo
	var cfg Config
	if cfg, err := loadConfig(data); err != nil {
		log.Fatal(err)
	} else {
		fmt.Printf("loaded config, addr=%q\n", cfg.Addr)
	}
	fmt.Printf("starting server, addr=%q\n", cfg.Addr)
example.texttext
loaded config, addr=":8443"
starting server, addr=""

المتغير cfg داخل if متغير جديد. ويبقى cfg الخارجي على قيمته الصفرية، فيبدأ الخادم بعنوان فارغ. اكتب cfg, err := loadConfig(data) في سطر مستقل، وافحص err بجملة if عادية. ويشرح مقال الكلمة المفتاحية var في Go الحجب والمحلّل shadow الذي يكتشفه.

لماذا تتجنب Go الاصطلاحية else؟

معظم دوال Go سلسلة من الخطوات يمكن أن تفشل كل منها. فإذا تداخلت كل خطوة داخل else الخاصة بالخطوة السابقة، ينزاح المسار الناجح نحو اليمين، وتنتهي معالجة الخطأ بعيدًا عن الفحص الذي تسبب فيه. وهذا معالج طلبات (handler) لإنشاء الطلبيات مكتوب بهذه الطريقة:

example.gogo
func handleCreateOrder(w http.ResponseWriter, r *http.Request) {
	var req CreateOrderRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
		if req.Quantity > 0 {
			if id, err := saveOrder(req); err == nil {
				w.WriteHeader(http.StatusCreated)
				json.NewEncoder(w).Encode(map[string]string{"id": id})
			} else {
				http.Error(w, "could not save order", http.StatusInternalServerError)
			}
		} else {
			http.Error(w, "quantity must be positive", http.StatusBadRequest)
		}
	} else {
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
	}
}

لمعرفة ما يحدث عند وصول JSON غير صالح، عليك أن تقرأ حتى نهاية الدالة. اعكس كل شرط بحيث تأتي حالة الخطأ أولًا وتعود:

example.gogo
func handleCreateOrder(w http.ResponseWriter, r *http.Request) {
	var req CreateOrderRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
		return
	}
	if req.Quantity <= 0 {
		http.Error(w, "quantity must be positive", http.StatusBadRequest)
		return
	}

	id, err := saveOrder(req)
	if err != nil {
		http.Error(w, "could not save order", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(map[string]string{"id": id})
}

تشغيل النسختين عبر httptest على أربعة أجسام للطلب (JSON معطوب، وكمية صفرية، ورمز SKU نفد من المخزون، وطلبية صالحة) يعطي رموز الحالة والأجسام نفسها تمامًا. والنسخة الثانية لا تحتوي على else إطلاقًا. فكل فحص يقع بجوار استجابة الخطأ الخاصة به، والسطران الأخيران هما مسار النجاح عند الهامش الأيسر. وإضافة خطوة تحقق رابعة تعني إضافة كتلة if أخرى، لا مستوى تداخل جديدًا.

يصف فريق Go هذا الأسلوب في Effective Go: «بما أن حالات الخطأ تنتهي عادةً بجمل return، فإن الكود الناتج لا يحتاج إلى جمل else». وتسمّيه صفحة Go Code Review Comments باسم Indent Error Flow: «حاول أن تُبقي مسار الكود العادي بأقل إزاحة ممكنة، وأن تُزيح معالجة الأخطاء وتتعامل معها أولًا».

ماذا تقول أدوات الفحص (linters) عن else

تفعّل أداة الفحص revive (البديل المُطوَّر حاليًا عن golint) قاعدتين خاصتين بـ else افتراضيًا. وعند تشغيلها على هذا الكود:

example.gogo
func readTimeout() (int, error) {
	secs, err := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS"))
	if err != nil {
		return 0, fmt.Errorf("parse TIMEOUT_SECONDS: %w", err)
	} else {
		return secs, nil
	}
}

func countValid(lines []string) int {
	n := 0
	for _, line := range lines {
		if line == "" {
			continue
		} else {
			n++
		}
	}
	return n
}

تبلّغ عن:

example.texttext
lint.go:13:9: if block ends with a return statement, so drop this else and outdent its block
lint.go:23:10: if block ends with a continue statement, so drop this else and outdent its block

تأتي الرسالة الأولى من القاعدة indent-error-flow، التي تعمل حين تنتهي كتلة if بـ return. وتأتي الثانية من القاعدة superfluous-else، التي تغطي continue و break و goto والاستدعاءات التي لا تعود أبدًا، مثل panic و os.Exit و log.Fatal. ولدى revive أيضًا قاعدة اختيارية باسم early-return للشكل المعكوس، حيث تكون كتلة else هي التي تعود. ويوفّر golangci-lint القواعد نفسها عبر أداة الفحص revive المدمجة فيه.

لا تنبّه أي من القاعدتين الافتراضيتين إلى المعالج المتداخل أعلاه، لأن أيًا من كتله لا يعود. فأداة الفحص لا تلتقط إلا الحالة الآلية، أما تسطيح هرم من فحوصات النجاح فهو إعادة هيكلة (refactor) عليك أن تقوم بها بنفسك.

متى تكون else الخيار الصحيح في Go؟

حذف else قاعدة تخص الكتل التي تخرج. أما حين يؤدي الفرعان عملًا حقيقيًا ولا يغادر أي منهما الدالة، فإن else هي الطريقة الأوضح لقول «واحد من هذين». ويحدث ذلك في ثلاث حالات.

الحالة الأولى نتيجتان يستمر بعدهما التنفيذ. فالبحث في ذاكرة التخزين المؤقت (cache) يسجّل إصابة أو إخفاقًا ثم يتابع، وبما أن أيًا من الفرعين لا يعود، فلا يوجد ما تُزال إزاحته:

example.gogo
	if entry, ok := cache[key]; ok {
		hits++
		resp = entry
	} else {
		misses++
		resp = fetchFromOrigin(key)
		cache[key] = resp
	}

الحالة الثانية هي الاختيار بين قيمتين. فإذا كانت القيمة رخيصة، عيّن القيمة الافتراضية ثم استبدلها عند الحاجة:

example.gogo
	level := slog.LevelInfo
	if debug {
		level = slog.LevelDebug
	}

وحين يكون لبناء القيمة الافتراضية تكلفة أو أثر جانبي، مثل فتح اتصال، استخدم if/else حتى لا يُنفَّذ إلا فرع واحد:

example.gogo
	var store Store
	if redisURL != "" {
		store = RedisStore{url: redisURL}
	} else {
		store = MemoryStore{}
	}

أسلوب «القيمة الافتراضية ثم الاستبدال» يعني هنا كتابة var store Store = MemoryStore{} ثم استبدالها. وهذا لا ينجح هنا إلا لأن MemoryStore{} لا يكلّف شيئًا. فلو كانت القيمة الافتراضية تفتح ملفًا أو مجمّع اتصالات (connection pool)، لرمى الاستبدال موردًا لم تستخدمه قط.

الحالة الثالثة متغير آتٍ من الجملة القصيرة. فحين يحتاج إليه كل فرع، كما في مثال PORT، تُبقيه else محصورًا في نطاق الفحص. والتصريح عنه قبل if ينجح أيضًا، لكنه يبقى حينها ضمن النطاق في بقية الدالة.

هل يوجد معامل ثلاثي (ternary operator) في Go؟

لا. لا يوجد في Go الشكل cond ? a : b. وتشرح صفحة الأسئلة الشائعة لـ Go (Go FAQ) السبب: «سبب غياب ?: عن Go هو أن مصممي اللغة رأوا هذه العملية تُستخدم كثيرًا لإنشاء تعابير معقدة يصعب فهمها. وشكل if-else، رغم أنه أطول، أوضح بلا شك. واللغة لا تحتاج إلا إلى بنية واحدة للتحكم الشرطي في التدفق».

البديل هو if/else أو أسلوب «القيمة الافتراضية ثم الاستبدال» من القسم السابق. وللحالة الشائعة «استخدم هذه القيمة ما لم تكن فارغة»، أضافت Go 1.22 الدالة cmp.Or، التي تعيد أول وسيط لها لا يساوي القيمة الصفرية:

example.gogo
package main

import (
	"cmp"
	"fmt"
	"os"
)

func main() {
	addr := cmp.Or(os.Getenv("LISTEN_ADDR"), ":8080")
	fmt.Println("listening on", addr)
}
example.texttext
listening on :8080

cmp.Or دالة وليست معاملًا، لذا تقيّم Go كل الوسائط قبل الاستدعاء. فالاستدعاء cmp.Or(cachedToken, fetchToken()) يستدعي fetchToken في كل مرة، حتى حين يكون cachedToken مُعيَّنًا. والدالة المساعدة العامة ternary(cond, a, b) تعاني المشكلة نفسها، وهذا أحد أسباب ندرة تعريفها في قواعد كود Go. وحين يكون لأحد الفرعين تكلفة، اكتب if.

أين يأتي دور LevelUpGo

يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تغطي دورة Go Basics كلًا من if و else if و switch والإرجاع المبكر عند الأخطاء، ضمن تعلّم اللغة من الصفر. وتحتوي دورة Simplification على دروس عن تسطيح الكود المتداخل، وتبسيط المنطق البولياني، واستبدال سلاسل else if بـ switch. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.

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

ماذا تفعل else في Go؟

تنفّذ else كتلة حين يكون شرط if الذي يسبقها خاطئًا. ويمكن أن تليها كتلة (else { ... }) أو if أخرى (else if cond { ... }). وهي إحدى الكلمات المفتاحية المحجوزة الـ 25 في Go، ولا يمكن أن تظهر إلا بعد كتلة if.

لماذا تعطي Go الخطأ "syntax error: unexpected keyword else"؟

لأن else موجودة على سطر جديد بعد القوس } المغلق لكتلة if. تُدرج Go فاصلة منقوطة بعد } في نهاية السطر، فتنتهي جملة if، ولا تجد else في السطر التالي ما ترتبط به. اكتب } else { على سطر واحد، أو شغّل gofmt.

هل توجد في Go elif أو elseif؟

لا. تكتبها Go كلمتين، else if، وهي else تليها جملة if جديدة. وللسلاسل الطويلة، تُقرأ switch دون تعبير بعد الكلمة المفتاحية بشكل أوضح.

هل أستخدم else بعد return في Go؟

لا. إذا انتهت كتلة if بـ return، فإن الكود الذي يلي if لا يُنفَّذ إلا حين يكون الشرط خاطئًا، فلا تضيف else سوى الإزاحة. وتنبّه revive إلى ذلك عبر قاعدتها indent-error-flow، وتوصي Go Code Review Comments بمعالجة الخطأ أولًا وإبقاء المسار العادي دون إزاحة.

هل يمكنني استخدام متغير من جملة if داخل كتلة else؟

نعم. المتغير المُصرَّح عنه في الجملة القصيرة، كما في if n, err := strconv.Atoi(s); err != nil، يكون ضمن النطاق في كتلة if وفي كل فرع else if و else. ويخرج من النطاق بعد القوس المعقوف المغلق الأخير.

كيف أكتب if/else في سطر واحد في Go؟

ليس في كود منسَّق. فجملة if/else في سطر واحد تُترجَم، لكن gofmt يوزّعها على عدة أسطر، ولا يوجد في Go معامل ثلاثي. استخدم if/else عادية، أو عيّن قيمة افتراضية واستبدلها داخل if، أو استخدم cmp.Or حين تحتاج إلى قيمة بديلة للقيمة الصفرية.

المصادر

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

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

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