تنفّذ الكلمة المفتاحية 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.gogopackage 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.texttextattempt 3 failed, retrying
تختلف ثلاث قواعد عن C و Java و JavaScript:
- لا أقواس حول الشرط. يُترجَم
if (attempt < maxAttempts) {، لكنgofmtيزيل الأقواس. - الأقواس المعقوفة إلزامية، حتى لجسم من سطر واحد. يفشل
if code >= 500 fmt.Println("server error")بالخطأsyntax error: unexpected name fmt, expected {. - يجب أن يكون الشرط من النوع
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.gogoif 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.gogopackage 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.texttextfast=2 slow=1 timed_out=1
الترتيب مهم. بدّل أول شرطين بحيث يأتي d < time.Second أولًا، فيصبح الناتج fast=0 slow=3 timed_out=1. فكل طلب أقل من 100ms هو أيضًا أقل من ثانية واحدة، لذا يلتقطه الفحص الأوسع أولًا ولا يُنفَّذ فرع fast أبدًا. ضع النطاق الأضيق أولًا.
وحين تتجاوز السلسلة ثلاثة فروع، تكون switch دون تعبير أسهل قراءة في الغالب. فكل case شرط، وتُفحص الحالات بالترتيب نفسه من الأعلى إلى الأسفل، وتحل default محل else الأخيرة:
example.gogofor _, 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.gogopackage 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.texttextPORT=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.gogovar 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.texttextloaded config, addr=":8443" starting server, addr=""
المتغير cfg داخل if متغير جديد. ويبقى cfg الخارجي على قيمته الصفرية، فيبدأ الخادم بعنوان فارغ. اكتب cfg, err := loadConfig(data) في سطر مستقل، وافحص err بجملة if عادية. ويشرح مقال الكلمة المفتاحية var في Go الحجب والمحلّل shadow الذي يكتشفه.
لماذا تتجنب Go الاصطلاحية else؟
معظم دوال Go سلسلة من الخطوات يمكن أن تفشل كل منها. فإذا تداخلت كل خطوة داخل else الخاصة بالخطوة السابقة، ينزاح المسار الناجح نحو اليمين، وتنتهي معالجة الخطأ بعيدًا عن الفحص الذي تسبب فيه. وهذا معالج طلبات (handler) لإنشاء الطلبيات مكتوب بهذه الطريقة:
example.gogofunc 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.gogofunc 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.gogofunc 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.texttextlint.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.gogoif entry, ok := cache[key]; ok { hits++ resp = entry } else { misses++ resp = fetchFromOrigin(key) cache[key] = resp }
الحالة الثانية هي الاختيار بين قيمتين. فإذا كانت القيمة رخيصة، عيّن القيمة الافتراضية ثم استبدلها عند الحاجة:
example.gogolevel := slog.LevelInfo if debug { level = slog.LevelDebug }
وحين يكون لبناء القيمة الافتراضية تكلفة أو أثر جانبي، مثل فتح اتصال، استخدم if/else حتى لا يُنفَّذ إلا فرع واحد:
example.gogovar 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.gogopackage main import ( "cmp" "fmt" "os" ) func main() { addr := cmp.Or(os.Getenv("LISTEN_ADDR"), ":8080") fmt.Println("listening on", addr) }
example.texttextlistening 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 حين تحتاج إلى قيمة بديلة للقيمة الصفرية.
المصادر
- The Go Programming Language Specification, If statements: https://go.dev/ref/spec#If_statements
- The Go Programming Language Specification, Semicolons: https://go.dev/ref/spec#Semicolons
- The Go Programming Language Specification, Blocks: https://go.dev/ref/spec#Blocks
- Effective Go, If: https://go.dev/doc/effective_go#if
- Go Code Review Comments, Indent Error Flow: https://go.dev/wiki/CodeReviewComments#indent-error-flow
- Go FAQ, Does Go have the ?: operator?: https://go.dev/doc/faq#Does_Go_have_a_ternary_form
- revive rules (indent-error-flow, superfluous-else, early-return): https://github.com/revive-lint/revive/blob/master/RULES_DESCRIPTIONS.md
- cmp package, Or: https://pkg.go.dev/cmp#Or
