Go: وظائف لغة Go

تُعد الدوال بمثابة «مكعبات الليغو» في كود لغة Go — حيث تأخذ لغة Go مفهوم «الدوال من الدرجة الأولى» إلى أقصى حد، مما يجعل البرمجة التوليفية أمرًا بسيطًا.

فلسفة تصميم الدوال في لغة Go: تعظيم قابلية إعادة الاستخدام بأقل قدر ممكن من الصياغة. القيم المرتجعة المتعددة، والقيم المرتجعة المسماة، والوسائط المتغيرة العدد، والإغلاقات، وتمرير الدوال بالقيمة — هذه الميزات التي تتطلب «إطار عمل» أو «مكتبة» في لغات البرمجة الأخرى، تُعد جزءًا من الصياغة الأصلية للغة Go.

1. ستتعلم



2. قصة حقيقية لمهندس بيانات

(1) المشكلة: لا يمكن لدوال جافا إلا أن تُرجع قيمة واحدة

بوب هو مهندس بيانات احتاج مؤخرًا إلى كتابة دالة لمعالجة البيانات:

"أحتاج إلى كتابة دالة parseUserData تُرجع كلاً من كائن المستخدم الذي تم تحليله ورمز الخطأ؛ كما يجب أن تتتبع وقت التحليل وتُرجع رسائل التحذير — ولكن بما أن لغة جافا لا تسمح إلا بقيمة إرجاع واحدة، فأنا مضطر إلى حشر كل شيء في كائن Result واحد، مما يجعل الكود يبدو كفوضى متشابكة تشبه معكرونة السباغيتي."

فتح ملف كود جافا:

JAVA
// Java: Can only return one value, so it must be packaged
public class ParseResult {
    public User user;
    public int errorCode;
    public long durationMs;
    public List<String> warnings;
}

public ParseResult parseUserData(String raw) {
    // All 5 return values are listed here
    return new ParseResult(...);
}

خلال عملية مراجعة، اشتكى أحد الزملاء قائلاً: «وظيفتك تشبه مجموعة الدمى الروسية المتداخلة — فإذا احتجت إلى خمسة حقول، يتعين عليّ إزالة طبقة تلو الأخرى».

(2) الحل بلغة Go

تدعم لغة Go القيم المرتجعة المتعددة بشكل أصلي:

GO
// user_parser.go
package main

import (
    "fmt"
    "strconv"
    "strings"
    "time"
)

// Multiple return values: user + errorCode + durationMs + warnings
func parseUserData(raw string) (User, int, time.Duration, []string) {
    start := time.Now()
    var warnings []string

    parts := strings.Split(raw, ",")
    if len(parts) != 3 {
        return User{}, 400, time.Since(start), []string{"Format error: 3 fields required"}
    }

    age, err := strconv.Atoi(parts[1])
    if err != nil {
        return User{}, 400, time.Since(start), []string{"Invalid age format"}
    }

    if age < 0 || age > 150 {
        warnings = append(warnings, "Age Anomaly")
    }

    user := User{Name: parts[0], Age: age, City: parts[2]}
    return user, 200, time.Since(start), warnings
}

type User struct {
    Name string
    Age  int
    City string
}

func main() {
    user, code, duration, warnings := parseUserData("Alice,28,Shanghai")

    fmt.Printf("Status: %d\n", code)
    fmt.Printf("User: %+v\n", user)
    fmt.Printf("Duration: %v\n", duration)
    fmt.Printf("Warnings: %v\n", warnings)
}

الناتج:

TEXT 📖 للعرض فقط
Status: 200
User: {Name:Alice Age:28 City:Shanghai}
Duration: 12.5µs
Warnings: []

(3) الأداء: دوال Go مقارنةً باللغات الأخرى

ميزة C Java Python Go
قيم إرجاع متعددة ❌ يتطلب غلافًا من نوع «struct» ❌ يتطلب فئة غلاف ✅ توبول ✅ أصلي
اسم قيمة الإرجاع ✅ أصلي
الدوال كقيم مؤشرات الدوال لامدا عناصر من الدرجة الأولى ✅ عناصر من الدرجة الأولى
الإغلاقات ✅ (معقد) لامدا ✅ موجز
الحجج ذات الطول المتغير ✅ (stdarg) ✅ varargs ✅ *args ✅ ...
💡 نصيحة: أدى دعم لغة Go للقيم المتعددة المُرجعة إلى أن يصبح استخدام «return result + error» أسلوبًا شائعًا (func foo() (T, error)). وهذا هو حجر الزاوية في فلسفة معالجة الأخطاء في لغة Go.

100%
sequenceDiagram
    participant Caller
    participant Function as parseUserData()
    participant Parser as Internal Logic
    Caller->>Function: parseUserData(raw)
    Function->>Parser: Split fields
    Parser-->>Function: name, age, city
    Function-->>Caller: user + 200 + duration + warnings
    Note over Caller: Multiple return values: receive<br/>user/status/time/warnings simultaneously


3. تعريف الدالة func

(1) العناصر الأربعة لتعريف الدالة

GO
func functionName(param1 type1, param2 type2) returnType {
    // Function Body
    return value
}
العنصر الكلمة المفتاحية هل هو مطلوب؟
اسم الوظيفة funcName نعم
قائمة المعلمات (param type, ...) نعم
نوع القيمة المرجعة returnType لا (يمكن حذفها في حالة عدم وجود قيمة مرجعة)
نص الدالة { ... } نعم

▶ مثال: تُعرَّف الدوال الأساسية بأربعة أشكال

GO
package main

import "fmt"

// Form 1: No parameters, no return value
func sayHello() {
    fmt.Println("Hello!")
}

// Form 2: With parameters but no return value
func greet(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

// Form 3: With parameters and a return value
func add(a, b int) int {
    return a + b
}

// Form 4: Multiple Parameters and Multiple Return Values
func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero")
    }
    return a / b, nil
}

func main() {
    sayHello()                              // Hello!
    greet("Alice")                          // Hello, Alice!
    fmt.Println(add(2, 3))                  // 5
    result, err := divide(10.0, 2.0)
    fmt.Printf("%.2f, err=%v\n", result, err)  // 5.00, err=<nil>
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
Hello!
Hello, Alice!
5
5.00, err=<nil>

(3) اختصارات المعلمات

يمكن دمج المعلمات المتسلسلة من نفس النوع في نوع واحد:

GO
func add(a, b int) int  // equivalent to a int, b int
func rect(w, h int) (int, int)  // both parameters are int


4. القيم المرتجعة المتعددة

(1) تحديد قيم إرجاع متعددة

GO
package main

import "fmt"

func swap(a, b string) (string, string) {
    return b, a
}

func main() {
    x, y := swap("hello", "world")
    fmt.Println(x, y)  // world hello
}

▶ مثال: القيمة + الخطأ (نمط التوقيع في لغة Go)

GO
package main

import (
    "errors"
    "fmt"
)

func findUser(id int) (string, error) {
    if id <= 0 {
        return "", errors.New("invalid id")
    }
    if id == 999 {
        return "", fmt.Errorf("user %d not found", id)
    }
    return fmt.Sprintf("User-%d", id), nil
}

func main() {
    user, err := findUser(1)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("Found: %s\n", user)

    // To ignore a return value: use _
    _, err = findUser(999)
    fmt.Printf("Ignored: err=%v\n", err)
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
Found: User-1
Ignored: err=user 999 not found

(3) تسمية قيمة الإرجاع

يتم إعلان قيم الإرجاع المسماة في بداية الدالة؛ ويقوم البيان return بإرجاع هذه المتغيرات تلقائيًا:

GO
package main

import "fmt"

func calc(a, b int) (sum, diff, product int) {
    sum = a + b
    diff = a - b
    product = a * b
    return  // bare return, automatically returns sum/diff/product
}

func main() {
    s, d, p := calc(10, 3)
    fmt.Printf("sum=%d, diff=%d, product=%d\n", s, d, p)
}

الناتج:

TEXT 📖 للعرض فقط
sum=13, diff=7, product=30

(4) قيم الإرجاع المتعددة مقابل قيم الإرجاع المسماة

السيناريو التوصية
تُرجع قيمة واحدة قيمة الإرجاع القياسية
تُرجع قيمتين (القيمة + الخطأ) قيمة الإرجاع القياسية
تُرجع 3 قيم أو أكثر قم بتسمية القيم المُرجعة (للتوضيح)
يجب تعديل قيمة الإرجاع في defer يجب تسمية قيمة الإرجاع
💡 نصيحة: إن تسمية قيمة الإرجاع تجعل توقيع الدالة أطول، لكنها ضرورية لتعديل قيمة الإرجاع باستخدام defer (كما هو موضح في المثال الشامل لهذا الدرس).



5. المعلمات المتغيرة العدد

(1) بناء الجملة ذات الحجج المتغيرة ...

GO
package main

import "fmt"

func sum(nums ...int) int {
    total := 0
    for _, n := range nums {
        total += n
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3))        // 6
    fmt.Println(sum(10, 20))         // 30
    fmt.Println(sum())               // 0

    // Break the slice apart and pass it in: nums...
    nums := []int{1, 2, 3, 4, 5}
    fmt.Println(sum(nums...))        // 15
}

الناتج:

TEXT 📖 للعرض فقط
6
30
0
15

▶ مثال: المعلمات المتغيرة + السلاسل المنسقة

GO
package main

import "fmt"

// Similar to `fmt.Printf`: The first argument is fixed, and the rest are variable.
func logMessage(level string, args ...interface{}) {
    fmt.Printf("[%s] ", level)
    fmt.Println(args...)  // spread slice and pass to Println
}

func main() {
    logMessage("INFO", "Server started", "on port", 8080)
    logMessage("ERROR", "Database connection failed:", "timeout 5s")
    logMessage("DEBUG")
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
[INFO] Server started on port 8080
[ERROR] Database connection failed: timeout 5s
[DEBUG]

(3) القيود المفروضة على الحجج المتغيرة

القيد الوصف
ما يصل إلى حجة متغيرة واحدة func foo(a int, b ...int)
يجب أن تكون المعلمات المتغيرة العدد هي الأخيرة func foo(a ...int, b int)
يجب أن تكون الأنواع متسقة للسماح بأنواع متعددة، استخدم ...interface{}


6. الدوال المجهولة والإغلاقات

(1) الدوال المجهولة (الدوال الحرفية)

الدوال المجهولة لا تحمل أسماءً ويمكن تعيينها إلى متغير أو استدعاؤها مباشرةً:

GO
package main

import "fmt"

func main() {
    // Assign to a variable
    add := func(a, b int) int {
        return a + b
    }
    fmt.Println(add(2, 3))  // 5

    // Direct Call
    func(x int) {
        fmt.Printf("Anonymous function: x=%d\n", x)
    }(42)
}

الناتج:

TEXT 📖 للعرض فقط
5
Anonymous function: x=42

(2) عمليات الإغلاق: التقاط المتغيرات الخارجية

الإغلاق = دالة + المتغيرات الخارجية التي تشير إليها. تتيح الإغلاقات للدوال «تذكر» البيئة التي تم إنشاؤها فيها:

▶ مثال: تنفيذ عداد باستخدام الإغلاقات

GO
package main

import "fmt"

// Returns a closure: increments by 1 on each call
func makeCounter() func() int {
    count := 0  // variable captured by closure
    return func() int {
        count++
        return count
    }
}

func main() {
    counter := makeCounter()
    fmt.Println(counter())  // 1
    fmt.Println(counter())  // 2
    fmt.Println(counter())  // 3

    // Each counter is an independent closure.
    another := makeCounter()
    fmt.Println(another())  // 1 (restarts counting)
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
1
2
3
1

(4) الدوال المغلقة مقابل الدوال العادية

البعد الدالة العادية الإغلاق
ذات حالة بلا حالة ذات حالة (متغيرات محفوظة)
الذاكرة ثابتة في كل مرة يتم فيها إنشاء مثيل جديد
حالات الاستخدام الحساب البحت المصانع، والزخارف، ووظائف الاستدعاء


7. الدالة init وتهيئة الحزمة

(1) خصائص دالة init

الميزة الوصف
لا توجد معلمات، ولا قيمة إرجاع func init()
الاستدعاء التلقائي يتم تنفيذه تلقائيًا عند استيراد الحزمة
متعددة يمكن أن تحتوي الحزمة على عدة دوال تهيئة (يتم تنفيذها بالترتيب الذي تم الإعلان عنها به)
قبل main يتم تنفيذه قبل main()

▶ مثال: تهيئة الحزمة (السجل)

GO
// registry.go
package main

import "fmt"

var registry = make(map[string]int)

func init() {
    registry["version"] = 1
    registry["max_connections"] = 100
    fmt.Println("[init] registry initialized")
}

func init() {
    registry["debug"] = 1
    fmt.Println("[init] debug enabled")
}

func main() {
    fmt.Printf("Registry: %+v\n", registry)
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
[init] registry initialized
[init] debug enabled
Registry: map[debug:1 max_connections:100 version:1]

(3) init مقابل main

الوظيفة متى يتم استدعاؤها الغرض
init() يتم تنفيذه تلقائيًا عند استيراد الحزمة يقوم بتهيئة المتغيرات العالمية وتسجيل برامج التشغيل
main() يتم تنفيذه عند بدء تشغيل البرنامج (مرة واحدة) نقطة دخول البرنامج


8. الدوال كمعاملات وقيم إرجاع (الدوال ذات الترتيب الأعلى)

تُعتبر الدوال في لغة Go عناصر من الدرجة الأولى — حيث يمكن تعيينها إلى متغيرات، وتمريرها كمعاملات، وإرجاعها كقيم.

(1) الدوال كمعاملات (دوال الاستدعاء)

GO
package main

import "fmt"

// The second parameter is the function type: it accepts an `int` and returns an `int`.
func process(nums []int, callback func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = callback(n)
    }
    return result
}

func double(n int) int { return n * 2 }
func square(n int) int { return n * n }

func main() {
    nums := []int{1, 2, 3, 4, 5}

    doubled := process(nums, double)
    fmt.Println("doubled:", doubled)

    squared := process(nums, square)
    fmt.Println("squared:", squared)
}

الناتج:

TEXT 📖 للعرض فقط
doubled: [2 4 6 8 10]
squared: [1 4 9 16 25]

(2) الدوال كقيم إرجاع (المصنع)

GO
package main

import "fmt"

func makeAdder(x int) func(int) int {
    return func(y int) int {
        return x + y
    }
}

func main() {
    add10 := makeAdder(10)
    add100 := makeAdder(100)

    fmt.Println(add10(5))     // 15
    fmt.Println(add100(5))    // 105
}

الناتج:

TEXT 📖 للعرض فقط
15
105

▶ مثال: أنواع الدوال + الدوال ذات الترتيب الأعلى (map/filter/reduce)

GO
package main

import "fmt"

func mapFunc(nums []int, f func(int) int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = f(n)
    }
    return result
}

func filterFunc(nums []int, predicate func(int) bool) []int {
    var result []int
    for _, n := range nums {
        if predicate(n) {
            result = append(result, n)
        }
    }
    return result
}

func reduceFunc(nums []int, initial int, f func(int, int) int) int {
    acc := initial
    for _, n := range nums {
        acc = f(acc, n)
    }
    return acc
}

func main() {
    nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    // 1. Map: Each element *2
    doubled := mapFunc(nums, func(n int) int { return n * 2 })

    // 2. Filter: Keep even numbers
    evens := filterFunc(nums, func(n int) bool { return n%2 == 0 })

    // 3. Reduce: Sum
    sum := reduceFunc(nums, 0, func(acc, n int) int { return acc + n })

    fmt.Printf("doubled: %v\n", doubled)
    fmt.Printf("evens: %v\n", evens)
    fmt.Printf("sum: %d\n", sum)
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
doubled: [2 4 6 8 10 12 14 16 18 20]
evens: [2 4 6 8 10]
sum: 55


9. مثال كامل: مسار معالجة البيانات

اجمع بين جميع ميزات هذه الوظيفة لإنشاء خط أنابيب بيانات ETL (استخراج - تحويل - تحميل):

GO
// pipeline.go
package main

import (
    "fmt"
    "strings"
    "time"
)

// Data Sources
func extract() []string {
    return []string{
        "  Alice,28,Shanghai  ",
        "Bob,32,Beijing",
        "CHARLIE,45,Guangzhou",
        "",  // empty data
        "Dave,abc,ErrorCity",  // anomalous data
    }
}

// Step 1: Remove Spaces + Split Fields
func trim(s string) []string {
    return strings.Split(strings.TrimSpace(s), ",")
}

// Step 2: Convert to uppercase
func upper(s []string) []string {
    for i, v := range s {
        s[i] = strings.ToUpper(v)
    }
    return s
}

// Step 3: Verify the number of fields
func validate(s []string) (string, bool) {
    if len(s) != 3 || s[0] == "" {
        return "", false
    }
    return strings.Join(s, "|"), true
}

// Pipeline Function: Combining 3 Steps + Defer Report
func processPipeline(name string, data []string) (valid int, errors int) {
    defer func() {
        // Name the return value so that `defer` can modify the result
        fmt.Printf("[%s] Completed: valid=%d errors=%d duration=%v\n",
            name, valid, errors, time.Since(startTime))
    }()

    for _, raw := range data {
        s := trim(raw)
        if len(s) < 3 {
            errors++
            continue
        }
        s = upper(s)
        if result, ok := validate(s); ok {
            valid++
            fmt.Printf("  -> %s\n", result)
        } else {
            errors++
        }
    }
    return valid, errors
}

var startTime = time.Now()

func main() {
    fmt.Println("=== Start of Data Pipeline ===")

    // Combining Steps Using Functions as Arguments
    data := extract()
    valid, errors := processPipeline("ETL-1", data)

    fmt.Printf("\nSummary: valid=%d, errors=%d\n", valid, errors)
}

النتيجة المتوقعة:

TEXT 📖 للعرض فقط
=== Start of Data Pipeline ===
  -> ALICE|28|SHANGHAI
  -> BOB|32|BEIJING
  -> CHARLIE|45|GUANGZHOU
  -> DAVE|ABC|ERRORCITY
[ETL-1] Completed: valid=4 errors=1 duration=2.5µs

Summary: valid=4, errors=1
🔥 خطأ شائع: يجب إعلان startTime على مستوى الملف (لا يمكنك استخدام := داخل init). هذا هو الفرق الدقيق بين إعلانات الدالة init وvarvar على مستوى الحزمة، بينما init على مستوى الدالة.



❓ أسئلة شائعة

س هل تدعم دالات لغة Go التحميل الزائد؟
ج لا، لا تدعمه. تتعمد لغة Go عدم إدخال ميزة التحميل الزائد للدالات — لتجنب التعقيد الناجم عن «الغموض الناتج عن التحميل الزائد» الموجود في لغتي C++ وJava. إذا كنت بحاجة إلى سلوك مشابه، فاستخدم الحجج المتغيرة العدد func foo(a ...int) أو أسماء دالات مختلفة.
س ما الفرق بين القيم المرتجعة المتعددة وإرجاع بنية (struct)؟
ج تُعد القيم المرتجعة المتعددة مناسبة للحالات التي تتضمن قيمتين أو ثلاث قيم (خاصةً قيمة بالإضافة إلى خطأ)، لأنها تؤدي إلى كتابة كود أكثر إيجازًا؛ أما في حالة وجود أكثر من ثلاث قيم، فيُوصى باستخدام بنية (struct)، نظرًا لأن البنى تحتوي على أسماء حقول، مما يحسّن قابلية القراءة.
س ما هي الاستخدامات العملية لتسمية قيم الإرجاع؟
ج (1) تعمل عبارات return التلقائية على تقليل حجم الكود؛ (2) يمكن تعديل قيم الإرجاع في defer (مهم! يتم استخدام هذا في المثال الشامل في هذا الدرس)؛ (3) التوثيق — تعمل توقيعات الدوال كوثيقة توضيحية.
س هل يمكن أن تتسبب الإغلاقات في حدوث تسربات في الذاكرة؟
ج نعم. يتم تمديد عمر المتغيرات التي تلتقطها الإغلاق حتى تنتهي الإغلاق نفسها. ويؤدي الاحتفاظ بالإغلاق لفترة طويلة (مثلًا في ذاكرة التخزين المؤقت أو داخل goroutine) إلى منع أداة جمع القمامة من جمع هذه المتغيرات.
س هل يمكن استدعاء الدالة init يدويًّا؟
ج لا. لا يمكن استدعاء init إلا تلقائيًّا بواسطة بيئة تشغيل Go، ولا يتم تنفيذ init لكل حزمة إلا مرة واحدة. وستؤدي محاولة استدعاء init يدويًّا إلى حدوث خطأ في الترجمة.
س ما الفرق بين الوسيطات المتغيرة العدد والوسيطات المقطعية؟
ج الوسيطة المتغيرة العدد nums ...int هي في جوهرها []int مقطع داخل الدالة، لكن قواعد الاستدعاء تختلف: foo(1,2,3) مقابل foo([]int{1,2,3}...). الأولى هي مجرد «سكر نحوي».
س هل يؤدي استخدام الدوال كقيم إلى انخفاض في الأداء؟
ج تعمل المُجمِّعات الحديثة على تحسين أداء قيم الدوال (عن طريق استدعائها مباشرةً)، لذا لا يوجد أي عبء إضافي عمليًّا. ومع ذلك، فإن تخزين دالة في خريطة أو شريحة واستدعائها بشكل متكرر سيؤدي إلى عبء إضافي طفيف بسبب العنونة غير المباشرة — وهو ما يُعتبر عادةً ضئيلًا للغاية.

📖 ملخص


📝 تمارين

  1. المسألة الأساسية (صعوبة ⭐): اكتب دالة max(nums ...int) int تُرجع القيمة الأكبر بين جميع المعلمات. يجب أن تستخدم الدالة معلمات متغيرة العدد؛ ويجب أن يُنتج استدعاء max(1, 5, 3, 9, 2) القيمة 9.

  2. مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ makeBankAccount(initial int) (deposit func(int) int, withdraw func(int) (int, bool), balance func() int) لمحاكاة حساب مصرفي: deposit تزيد الرصيد وتُرجع الرصيد الجديد؛ withdraw تخصم أموالًا (وتُرجع false إذا كان الرصيد غير كافٍ)؛ balance تستعلم عن الرصيد. يجب عليك استخدام الإغلاقات.

  3. مشكلة التحدي (الصعوبة ⭐⭐⭐): قم بتنفيذ خط أنابيب بيانات وظيفي: عند إعطاء المدخلات []string (سلسلة من الأرقام)، استخدم الدوال ذات الترتيب الأعلى لتنفيذ الخطوات الأربع parse -> filter(>10) -> map(*2) -> sum بالتسلسل. يجب أن تكون كل خطوة دالة مستقلة، ويجب أن تنتج الدوال مجتمعة النتيجة النهائية. على سبيل المثال: ["1", "15", "3", "20"]parse [1,15,3,20]filter>10 [15,20]map*2 [30,40]sum 70.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%