Go: التحكم في تدفق التنفيذ: العبارات الأربع الرئيسية للتحكم…
تدفق التحكم هو ما يجعل البرامج «تفكر» — تغطي لغة Go جميع سيناريوهات تدفق التحكم باستخدام أربع كلمات رئيسية بسيطة فقط، مما يلغي التكرار في حلقات
whileوdo-while.
فلسفة تصميم تدفق التحكم في لغة Go: الأقل هو الأكثر. الكلمات الرئيسية الأربع if وswitch وfor وdefer، إلى جانب break وcontinue وlabel، كافية للتعامل مع 99% من منطق الأعمال. في هذا الدرس، ستتقن جميع المفاهيم الأساسية للتحكم في التدفق في لغة Go.
1. ستتعلم
- إذا كانت جملة شرطية (بما في ذلك عبارات التهيئة)
- التبديل بين الفروع (انقطاع تلقائي؛ لا يتطلب أي إجراء يدوي)
- الأشكال الأربعة لحلقة for (بدائل لحلقتي while و do-while)
- التأجيل: التنفيذ المؤجل وترتيب «آخر ما دخل أول ما يخرج» (LIFO)
- قفزات دقيقة باستخدام أوامر «break» و«continue» و«label»
- إنشاء نظام لتصحيح الامتحانات عبر الإنترنت باستخدام تدفق التحكم
2. قصة حقيقية عن منصة تعليمية عبر الإنترنت
(1) المشكلة: 500 سطر من كود لغة C++ لتصحيح الامتحانات، ومع ذلك تستمر الأخطاء في الظهور
تشارلي هو مهندس الخلفية في منصة تعليمية عبر الإنترنت. وقد تولى للتو مسؤولية كود نظام التصحيح التلقائي:
"قال رئيس الوزراء إن معالجة 1,000 ورقة امتحان تستغرق الآن 8 ساعات، وفي كل مرة يظهر فيها خطأ برمجي، يكون إما خطأً من نوع «خارج النطاق» أو عدم وجود
{}في جملة بلغة Cif، مما يؤدي إلى إفساد درجات الصف بأكمله..."
فتح ملف كود C++ الذي تركه سلفه وألقى نظرة عليه:
// C++ Style: Verbose + Prone to Errors
if (score >= 60) {
grade = "Passing Grade";
}
else // Note: C++ else on a different line is valid, but team style is inconsistent
grade = "Fail";
// Omission of a loop condition = an infinite loop
while (true) { // Wanted 3 iterations, but forgot the counter
// ... Didn't realize until 500 lines later
}
بعد توليه المسؤولية، قرر تشارلي إعادة كتابة البرنامج بلغة Go.
(2) الحل بلغة Go
يغطي لغة «Go» العملية بأكملها من خلال أربع كلمات رئيسية:
// score_evaluator.go
package main
import "fmt"
func evaluateScore(score int) string {
// (1) if contains an initialization statement
if grade := calculateGrade(score); grade != "" {
return fmt.Sprintf("Score: %d, Level: %s", score, grade)
}
return "Invalid Score"
}
func calculateGrade(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 60:
return "C"
default:
return "D"
}
}
func main() {
// (2) for 4 forms
scores := []int{95, 82, 67, 45, 88}
for i, s := range scores { // range form
fmt.Printf("Question %d: %d points -> %s\n", i+1, s, evaluateScore(s))
}
}
الناتج:
Question 1: 95 points -> Score: 95, Level A
Question 2: 82 points -> Score: 82, Level B
Question 3: 67 points -> Score: 67, Level C
Question 4: 45 points -> Score: 45, Level D
Question 5: 88 points -> Score: 88, Level B
(3) المزايا: مقارنة بين سهولة التحكم في العمليات
| اللغة | الكلمة الرئيسية «if» | الكلمة الرئيسية «loop» | التوقف التلقائي في «switch» | حجم الكود لنظام التقييم |
|---|---|---|---|---|
| C/C++ | if |
for/while/do-while |
يتطلب break يدويًّا |
~500 سطر |
| جافا | if |
for/while/do-while |
يتطلب إجراءً يدويًّا break |
حوالي 400 سطر |
| بايثون | if |
for/while |
غير متوفر (لا يوجد switch) |
حوالي 350 سطرًا |
| Go | if |
for (4 أشكال) |
فاصل تلقائي | ~200 سطر |
for وإزالة break من السلوك الافتراضي لـ switch — كلمة رئيسية أقل، وعشرة أنواع أقل من الأخطاء.
3. عبارة if
(1) جملة if / else الأساسية
package main
import "fmt"
func main() {
score := 85
if score >= 90 {
fmt.Println("Excellent")
} else if score >= 60 {
fmt.Println("Passing Grade")
} else {
fmt.Println("Fail")
}
}
الناتج:
Passing Grade
else في نفس السطر مع } (يلزم استخدام الأقواس المتعرجة). تؤدي صيغة }else{ إلى حدوث خطأ في الترجمة.
▶ مثال: تحديد الدرجة
package main
import "fmt"
func judgeGrade(score int) string {
if score >= 90 {
return "A"
} else if score >= 80 {
return "B"
} else if score >= 70 {
return "C"
} else if score >= 60 {
return "D"
} else {
return "F"
}
}
func main() {
fmt.Println(judgeGrade(95)) // A
fmt.Println(judgeGrade(72)) // C
fmt.Println(judgeGrade(50)) // F
}
الناتج:
A
C
F
(3) في حالة وجود عبارات التهيئة (ميزة في لغة Go)
يمكن لـ if تنفيذ عبارة تهيئة أولاً، تليها عملية فحص شرطي — مما يحد من نطاق المتغيرات:
package main
import (
"fmt"
"strconv"
)
func main() {
// Syntax: if initialization statement; condition { ... }
// The variable `err` is scoped only within the `if-else` block.
if n, err := strconv.Atoi("123"); err == nil {
fmt.Printf("Conversion successful: %d\n", n)
} else {
fmt.Printf("Conversion failed: %v\n", err)
}
}
الناتج:
Conversion successful: 123
▶ مثال: قراءة ملف + إغلاقه باستخدام defer
package main
import (
"fmt"
"os"
)
func readFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() // automatically close file before function returns
data := make([]byte, 1024)
n, _ := file.Read(data)
fmt.Printf("%d bytes read\n", n)
return nil
}
func main() {
readFile("config.txt")
}
(5) الممارسات الشائعة لمعالجة الأخطاء في عبارات if (إحدى السمات المميزة للغة Go)
تتم معالجة الأخطاء في لغة Go باستخدام عبارات if. الاستخدام التقليدي:
if err := doSomething(); err != nil {
return err // early return
}
// Normal Logic
error وأفضل الممارسات المتعلقة بهذا النمط.
4. تغيير الفرع (توقف تلقائي)
(1) أساسيات المفاتيح الكهربائية: القاطع التلقائي
بشكل افتراضي، لا تنتشر عبارة switch في لغة Go — فهي تنتهي تلقائيًا بعد مطابقة إحدى الحالات:
package main
import "fmt"
func main() {
day := "Monday"
switch day {
case "Monday":
fmt.Println("Monday: Meeting Day")
case "Tuesday":
fmt.Println("Tuesday: Development Day")
case "Friday":
fmt.Println("Friday: Demo Day")
default:
fmt.Println("Weekend: Day off")
}
}
الناتج:
Monday: Meeting Day
(2) مقارنةً بـ C/Java: لا حاجة إلى break
| اللغة | قواعد النحو | الفاصل التلقائي |
|---|---|---|
| C/C++ | case 1: ...; case 2: ... |
يتطلب دليلًا break; |
| جافا | case 1: ...; case 2: ... |
يتطلب دليلًا break; |
| انطلق | case "Monday": ... |
فاصل تلقائي |
إذا كنت ترغب في استخدام ميزة «fall-through»، فاستخدم الكلمة الرئيسية fallthrough:
switch n {
case 1:
fmt.Println("One")
fallthrough // force execution of the next case
case 2:
fmt.Println("Two")
}
الناتج:
One
Two
(3) تغيير شكل التعبير (بدون علامة)
لا تضع متغيرًا بعد switch؛ اكتب الشرط الخاص بكل case على حدة:
package main
import "fmt"
func gradeLevel(score int) string {
switch {
case score >= 90:
return "A"
case score >= 80:
return "B"
case score >= 60:
return "C"
default:
return "D"
}
}
func main() {
fmt.Println(gradeLevel(95))
fmt.Println(gradeLevel(72))
}
الناتج:
A
C
▶ مثال: محول مع مطابقة متعددة القيم + توجيه بناءً على رموز حالة HTTP
package main
import "fmt"
func routeStatus(code int) string {
switch code {
case 200, 201, 204:
return "Success"
case 301, 302:
return "Redirect"
case 400, 401, 403, 404:
return "Client Error"
case 500, 502, 503:
return "Server Error"
default:
return "Unknown"
}
}
func main() {
fmt.Println(routeStatus(200)) // Success
fmt.Println(routeStatus(404)) // Client Error
fmt.Println(routeStatus(500)) // Server Error
}
الناتج:
Success
Client Error
Server Error
5. الأشكال الأربعة لحلقة for
تُوحِّد لغة Go جميع الحلقات تحت بنية for، مستخدمةً صيغًا مختلفة لتنفيذ while وdo-while والحلقات اللانهائية.
(1) حلقة "for" الكلاسيكية المكونة من ثلاثة أجزاء
package main
import "fmt"
func main() {
for i := 1; i <= 3; i++ {
fmt.Printf("The %dth time\n", i)
}
}
الناتج:
The 1th time
The 2th time
The 3th time
▶ مثال: نمط "while" (حلقة شرطية)
package main
import "fmt"
func main() {
n := 1
for n < 5 { // equivalent to while (n < 5)
fmt.Printf("n=%d\n", n)
n++
}
}
الناتج:
n=1
n=2
n=3
n=4
(3) حلقة لا نهائية
package main
import (
"fmt"
"time"
)
func main() {
// Classic Infinite Loop (In real-world scenarios, it would use `break` or `return`)
for {
fmt.Println("Print once per second...")
time.Sleep(1 * time.Second)
break // prevent actual infinite loop, for demo only
}
}
الناتج:
Print once per second...
▶ مثال: التكرار على شريحة أو خريطة أو سلسلة باستخدام range
package main
import "fmt"
func main() {
scores := []int{95, 82, 67}
// Iterate through the slice: index, value
for i, score := range scores {
fmt.Printf("Problem %d: %d points\n", i+1, score)
}
// If only value is present: use _ to ignore index
for _, score := range scores {
fmt.Printf("Score: %d\n", score)
}
// Iterate through a map (key-value)
ages := map[string]int{"Alice": 28, "Bob": 32}
for name, age := range ages {
fmt.Printf("%s is %d years old\n", name, age)
}
// Iterate over a string (by rune, not by byte)
for i, r := range "Go" {
fmt.Printf("Position %d: %c (code point %d)\n", i, r, r)
}
}
الناتج (ترتيب الخريطة عشوائي):
Problem 1: 95 points
Problem 2: 82 points
Problem 3: 67 points
Score: 95
Score: 82
Score: 67
Alice is 28 years old
Bob is 32 years old
Position 0: G (code point 71)
Position 1: o (code point 111)
range map غير مضمون — فقد يختلف من مرة إلى أخرى. إذا كنت بحاجة إلى ترتيب ثابت، فقم بفرز المفاتيح أولاً قبل البدء في التكرار.
(5) مرجع سريع لأشكال كلمة "for" الأربعة
| النموذج | الصيغة | البديل |
|---|---|---|
| المعيار الخاص بـ | for i := 0; i < n; i++ {} |
C/Java for |
| نمط "while" | for condition {} |
C/Java while |
| إنفينيت لوب | for {} |
while(true) |
| تكرار النطاق | for i, v := range slice {} |
بايثون for...in |
flowchart LR
A[for init; condition; post] --> B{condition true?}
B -->|yes| C[execute loop body]
C --> D[execute post statement]
D --> B
B -->|no| E[exit loop]
style A stroke-width:2px
style E stroke:#f44,stroke-width:2px
6. defer: التنفيذ المؤجل (ترتيب LIFO)
تؤجل defer تنفيذ عبارة ما حتى اللحظة التي تسبق عودة الدالة الحالية مباشرةً. وتُستخدم عادةً لتنظيف الموارد (إغلاق الملفات، وإلغاء قفل الموارد، وإغلاق الاتصالات).
(1) تأجيل «الأساسيات»
package main
import "fmt"
func main() {
fmt.Println("1: Start of function")
defer fmt.Println("3: defer execution (before the function returns)")
fmt.Println("2: Function Body")
}
الناتج:
1: Start of function
2: Function Body
3: defer execution (before the function returns)
(2) تأجيل ترتيب LIFO (آخر ما يدخل، أول ما يخرج)
package main
import "fmt"
func main() {
defer fmt.Println("1")
defer fmt.Println("2")
defer fmt.Println("3")
fmt.Println("Main logic complete")
}
الناتج:
Main logic complete
3
2
1
لماذا طريقة LIFO؟ لأن defer تحاكي «إدارة الموارد القائمة على المكدس» — أي أن الموارد التي تم فتحها أخيرًا هي التي تُغلق أولاً.
▶ مثال: الاستخدام العملي لـ defer — إغلاق اتصال قاعدة البيانات
package main
import (
"database/sql"
"fmt"
"log"
)
func queryUser(db *sql.DB, userID int) {
// Get the connection
rows, err := db.Query("SELECT name FROM users WHERE id = ?", userID)
if err != nil {
log.Fatal(err)
}
// Key Point: `defer` releases resources (LIFO)
defer rows.Close() // Close result set when function returns
// Business logic...
for rows.Next() {
var name string
rows.Scan(&name)
fmt.Printf("User: %s\n", name)
}
}
func main() {
fmt.Println("Example of Resource Management with `defer`")
// Real-world scenario: db, _ := sql.Open("mysql", "...")
// queryUser(db, 1)
}
defer file.Close() حتى لو تم استدعاء return في منتصف العملية — وهذه حالة استخدام مميزة لإدارة الموارد في لغة Go. ملاحظة: قم فقط defer بتنظيف الموارد التي فتحتها في هذه الدالة — لا تقم defer db.Close() هنا لأن db تم تمريرها إلى الدالة ويتم إدارة دورة حياتها بواسطة المُستدعي.
7. break / continue / label: القفزات الدقيقة
(1) break: الخروج من الحلقة
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
if i == 5 {
break // exit the entire loop
}
fmt.Printf("%d ", i)
}
fmt.Println("End of loop")
}
الناتج:
1 2 3 4 End of loop
▶ مثال: الاستمرار في تخطي الأعداد الفردية وزيادتها
package main
import "fmt"
func sumOdd(n int) int {
sum := 0
for i := 1; i <= n; i++ {
if i%2 == 0 {
continue // skip even numbers
}
sum += i
}
return sum
}
func main() {
fmt.Printf("1-10: Sum of odd numbers: %d\n", sumOdd(10))
}
الناتج:
1-10: Sum of odd numbers: 25
(3) استخدم label للخروج من حلقة متداخلة
label يحدد اسم الحلقة؛ break label يخرج من الحلقة المحددة:
package main
import "fmt"
func main() {
outer:
for i := 1; i <= 3; i++ {
for j := 1; j <= 3; j++ {
if i*j == 6 {
fmt.Printf("Hit: i=%d, j=%d\n", i, j)
break outer // directly exit the outer loop
}
fmt.Printf("(%d, %d) ", i, j)
}
}
fmt.Println("Done")
}
الناتج:
(1, 1) (1, 2) (1, 3) (2, 1) (2, 2) (2, 3) (3, 1) (3, 2) Hit: i=3, j=2
Done
(4) مقارنة بين الأنواع الثلاثة من عمليات إعادة التوجيه
| الكلمة المفتاحية | الوظيفة | خارج النطاق |
|---|---|---|
break |
يخرج من العبارة الحالية من نوع loop أو switch | مستوى واحد |
continue |
تخطي ما تبقى من هذه الدورة | مستوى واحد |
break label |
الخروج من الحلقة عند العلامة المحددة | متداخلة |
8. مثال كامل: نظام تصحيح الامتحانات عبر الإنترنت
اربط جميع مسارات التحكم في هذا الدرس معًا لتنفيذ برنامج تصحيح امتحانات متعدد الأبعاد:
// exam_evaluator.go
package main
import "fmt"
// Structure of the Problem
type Question struct {
Type string // single/multi/judge
Correct string // correct answer
Student string // student answer
Score float64 // score value
}
// Scoring Function: Initialized with an `if` statement + `switch` expression + `for range`
func evaluateExam(name string, questions []Question) (total float64, details []string) {
defer func() {
// defer: Collect results (ensures that results are collected even if a return statement is encountered midway)
details = append(details, fmt.Sprintf("[%s] Total score: %.1f", name, total))
}()
for i, q := range questions {
var earned float64
// switch expression format: multi-condition branching
switch q.Type {
case "single":
if q.Student == q.Correct {
earned = q.Score
}
case "judge":
if q.Student == q.Correct {
earned = q.Score
} else {
earned = 0
}
case "multi":
// Multiple-choice question: Points are awarded only for exact matches
if q.Student == q.Correct {
earned = q.Score
} else {
earned = 0
}
default:
continue // skip unknown question type
}
total += earned
details = append(details,
fmt.Sprintf("Problem %d (%s): Scored %.1f points", i+1, q.Type, earned))
}
return total, details
}
func main() {
questions := []Question{
{Type: "single", Correct: "B", Student: "B", Score: 2},
{Type: "judge", Correct: "true", Student: "true", Score: 1},
{Type: "multi", Correct: "A,B,C", Student: "A,B", Score: 3},
{Type: "single", Correct: "D", Student: "D", Score: 2},
}
// Use a `for range` loop to iterate through the student list
students := []string{"Alice", "Bob", "Charlie"}
for _, name := range students {
total, details := evaluateExam(name, questions)
fmt.Println("========================================")
for _, d := range details {
fmt.Println(d)
}
}
}
النتائج المتوقعة (الدرجات التفصيلية لكل طالب + ملخص التأجيل):
========================================
Problem 1 (single): Scored 2.0 points
Problem 2 (judge): Scored 1.0 points
Problem 3 (multi): Scored 0.0 points
Problem 4 (single): Scored 2.0 points
[Alice] Total score: 5.0
========================================
Problem 1 (single): Scored 2.0 points
Problem 2 (judge): Scored 1.0 points
Problem 3 (multi): Scored 0.0 points
Problem 4 (single): Scored 2.0 points
[Bob] Total score: 5.0
========================================
Problem 1 (single): Scored 2.0 points
Problem 2 (judge): Scored 1.0 points
Problem 3 (multi): Scored 0.0 points
Problem 4 (single): Scored 2.0 points
[Charlie] Total score: 5.0
defer حتى تصبح مرئية في النطاق الخارجي — في المثال أعلاه، يستخدم details قيمة الإرجاع المسماة (total float64, details []string)، ولهذا السبب يسري مفعول append داخل defer.
❓ أسئلة شائعة
while؟for لجميع الحلقات: for condition {} تعادل while، وfor {} تعادل while(true). وبفضل وجود كلمة رئيسية أقل، لا تضطر الفرق إلى الجدال حول ما إذا كان يجب استخدام while أم do-while.switch لا يتم تجاوزها افتراضيًّا؟switch يجب أن تكون قائمة بذاتها — فمشكلة «نسيان كتابة break، مما يؤدي إلى تجاوزها» في لغتي C وJava تُعدّ من الأخطاء الكلاسيكية. تستخدم لغة Go fallthrough للإشارة صراحةً إلى نية التخطي، وهو ما يعتبر أكثر أمانًا.for range؟keys := make([]string, 0, len(m)); for k := range m { keys = append(keys, k) }; sort.Strings(keys).defer؟ ما هو ترتيب تنفيذ أوامر defer المتعددة؟defer قبل أن تعود الدالة الحالية (أي بعد تنفيذ الأمر return ولكن قبل خروج الدالة فعليًّا). يتم تنفيذ تعليمات defer المتعددة بترتيب LIFO (آخر دخول، أول خروج)، مما يحاكي إدارة الموارد القائمة على المكدس: حيث يتم تحرير الموارد التي تم دفعها إلى المكدس أخيرًا أولاً.if؟if لا تكون مرئية إلا داخل الكتلة if-else؛ وتصبح غير صالحة بمجرد الخروج من الكتلة if-else. وهذا يمنع حدوث الخطأ الشائع المتمثل في «تسرب المتغيرات إلى النطاق الخارجي».label كثيرًا في المشاريع العملية؟label بشكل أساسي للخروج من الحلقات المتداخلة؛ وفي 99% من الحالات، يكفي استخدام break مع return. الإفراط في استخدام label يقلل من قابلية قراءة الكود — وهو درس مستفاد من عصر goto.switch في لغة Go التحقق من أنواع البيانات؟switch x := y.(type) للتحقق من أنواع الواجهات؛ وسنتناول هذا الموضوع بمزيد من التفصيل في الدرس 7. ومع ذلك، فإن هذه الصيغة لا تنطبق إلا على المتغيرات من النوع interface{}.continue وbreak في عبارة switch؟break يخرج من عبارة switch؛ أما في حلقة for، فيخرج من حلقة for. لا يمكن استخدام continue إلا في حلقة for؛ ولا يمكن استخدامه في عبارة switch (نظرًا لأن switch لا يُعتبر حلقة).📖 ملخص
- يغطي لغة Go جميع هياكل التحكم في التدفق باستخدام أربع كلمات رئيسية فقط:
if/switch/for/defer - يدعم
ifعبارات التهيئة، التي تقصر نطاق المتغيرات على كتلةif-else - يتم إيقاف
switchتلقائيًا بشكل افتراضي؛ استخدمfallthroughبشكل صريح عندما تحتاج إلى متابعة تنفيذ العبارة - أربعة أشكال من
forيمكن أن تحل محلwhileوdo-while:for init;cond;post{}/for cond{}/for{}/for range - يتم تنفيذ
deferقبل أن تعود الدالة؛ أما عباراتdeferالمتعددة فيتم تنفيذها بترتيب LIFO (آخر ما يدخل، أول ما يخرج). breakيخرج من الحلقة، وcontinueيتخطى التكرار الحالي، وbreak labelيخرج من الحلقة عند علامة محددة- فلسفة لغة Go في تصميم التحكم في التدفق: عدد أقل من الكلمات المفتاحية +
breakالتلقائي + إدارة الموارد بنظام LIFO
📝 تمارين
-
المسألة الأساسية (مستوى الصعوبة ⭐): اكتب برنامج «FizzBuzz» باستخدام
forوswitch: اطبع الأعداد من 1 إلى 100. عند مواجهة مضاعف العدد 3، اطبع «Fizz»؛ وعند مواجهة مضاعف العدد 5، اطبع «Buzz»؛ وعند مواجهة مضاعف العدد 15، اطبع «FizzBuzz». -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ الدالة
countPrimes(n int) int، التي تُرجع عدد الأعداد الأولية بين 1 و n. المتطلبات: (1) استخدم حلقةfor؛ (2) استخدم عبارةifللتحقق من الأعداد الأولية؛ (3) استخدمdeferلعرض وقت التنفيذ (باستخدامtime.Now()) قبل أن تُرجع الدالة النتيجة. -
مشكلة التحدي (الصعوبة ⭐⭐⭐): قم بتنفيذ محلل لدرجات الطلاب: عند إعطاء مجموعة من الدرجات ([]float64)، قم بإخراج (1) متوسط الدرجات؛ (2) معدل النجاح (≥60 نقطة)؛ (3) أعلى وأدنى الدرجات؛ (4) توزيع الدرجات حسب الفئة (A/B/C/D/F). المتطلبات: (1) استخدم
for range؛ (2) استخدم عبارةswitchلتحديد الدرجات؛ (3) استخدمdeferلعرض المعلومات الموجزة قبل أن تعود الدالة.