Go: وظائف لغة Go
تُعد الدوال بمثابة «مكعبات الليغو» في كود لغة Go — حيث تأخذ لغة Go مفهوم «الدوال من الدرجة الأولى» إلى أقصى حد، مما يجعل البرمجة التوليفية أمرًا بسيطًا.
فلسفة تصميم الدوال في لغة Go: تعظيم قابلية إعادة الاستخدام بأقل قدر ممكن من الصياغة. القيم المرتجعة المتعددة، والقيم المرتجعة المسماة، والوسائط المتغيرة العدد، والإغلاقات، وتمرير الدوال بالقيمة — هذه الميزات التي تتطلب «إطار عمل» أو «مكتبة» في لغات البرمجة الأخرى، تُعد جزءًا من الصياغة الأصلية للغة Go.
1. ستتعلم
- العناصر الأربعة لتعريف الدالة
func - قيم الإرجاع المتعددة وقيم الإرجاع المسماة
- استخدام المتغيرات كمعلمات
... - الدوال المجهولة والإغلاقات
- الدالة
initوتهيئة الحزمة - الدوال كمعاملات وقيم مرتجعة (الدوال ذات الترتيب الأعلى)
- إنشاء مسارات معالجة البيانات باستخدام الدوال
2. قصة حقيقية لمهندس بيانات
(1) المشكلة: لا يمكن لدوال جافا إلا أن تُرجع قيمة واحدة
بوب هو مهندس بيانات احتاج مؤخرًا إلى كتابة دالة لمعالجة البيانات:
"أحتاج إلى كتابة دالة
parseUserDataتُرجع كلاً من كائن المستخدم الذي تم تحليله ورمز الخطأ؛ كما يجب أن تتتبع وقت التحليل وتُرجع رسائل التحذير — ولكن بما أن لغة جافا لا تسمح إلا بقيمة إرجاع واحدة، فأنا مضطر إلى حشر كل شيء في كائنResultواحد، مما يجعل الكود يبدو كفوضى متشابكة تشبه معكرونة السباغيتي."
فتح ملف كود جافا:
// 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 القيم المرتجعة المتعددة بشكل أصلي:
// 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)
}
الناتج:
Status: 200
User: {Name:Alice Age:28 City:Shanghai}
Duration: 12.5µs
Warnings: []
(3) الأداء: دوال Go مقارنةً باللغات الأخرى
| ميزة | C | Java | Python | Go |
|---|---|---|---|---|
| قيم إرجاع متعددة | ❌ يتطلب غلافًا من نوع «struct» | ❌ يتطلب فئة غلاف | ✅ توبول | ✅ أصلي |
| اسم قيمة الإرجاع | ❌ | ❌ | ❌ | ✅ أصلي |
| الدوال كقيم | مؤشرات الدوال | لامدا | عناصر من الدرجة الأولى | ✅ عناصر من الدرجة الأولى |
| الإغلاقات | ✅ (معقد) | لامدا | ✅ | ✅ موجز |
| الحجج ذات الطول المتغير | ✅ (stdarg) | ✅ varargs | ✅ *args | ✅ ... |
func foo() (T, error)). وهذا هو حجر الزاوية في فلسفة معالجة الأخطاء في لغة Go.
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) العناصر الأربعة لتعريف الدالة
func functionName(param1 type1, param2 type2) returnType {
// Function Body
return value
}
| العنصر | الكلمة المفتاحية | هل هو مطلوب؟ |
|---|---|---|
| اسم الوظيفة | funcName |
نعم |
| قائمة المعلمات | (param type, ...) |
نعم |
| نوع القيمة المرجعة | returnType |
لا (يمكن حذفها في حالة عدم وجود قيمة مرجعة) |
| نص الدالة | { ... } |
نعم |
▶ مثال: تُعرَّف الدوال الأساسية بأربعة أشكال
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>
}
الناتج:
Hello!
Hello, Alice!
5
5.00, err=<nil>
(3) اختصارات المعلمات
يمكن دمج المعلمات المتسلسلة من نفس النوع في نوع واحد:
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) تحديد قيم إرجاع متعددة
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)
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)
}
الناتج:
Found: User-1
Ignored: err=user 999 not found
(3) تسمية قيمة الإرجاع
يتم إعلان قيم الإرجاع المسماة في بداية الدالة؛ ويقوم البيان return بإرجاع هذه المتغيرات تلقائيًا:
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)
}
الناتج:
sum=13, diff=7, product=30
(4) قيم الإرجاع المتعددة مقابل قيم الإرجاع المسماة
| السيناريو | التوصية |
|---|---|
| تُرجع قيمة واحدة | قيمة الإرجاع القياسية |
| تُرجع قيمتين (القيمة + الخطأ) | قيمة الإرجاع القياسية |
| تُرجع 3 قيم أو أكثر | قم بتسمية القيم المُرجعة (للتوضيح) |
يجب تعديل قيمة الإرجاع في defer |
يجب تسمية قيمة الإرجاع |
defer (كما هو موضح في المثال الشامل لهذا الدرس).
5. المعلمات المتغيرة العدد
(1) بناء الجملة ذات الحجج المتغيرة ...
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
}
الناتج:
6
30
0
15
▶ مثال: المعلمات المتغيرة + السلاسل المنسقة
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")
}
الناتج:
[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) الدوال المجهولة (الدوال الحرفية)
الدوال المجهولة لا تحمل أسماءً ويمكن تعيينها إلى متغير أو استدعاؤها مباشرةً:
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)
}
الناتج:
5
Anonymous function: x=42
(2) عمليات الإغلاق: التقاط المتغيرات الخارجية
الإغلاق = دالة + المتغيرات الخارجية التي تشير إليها. تتيح الإغلاقات للدوال «تذكر» البيئة التي تم إنشاؤها فيها:
▶ مثال: تنفيذ عداد باستخدام الإغلاقات
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)
}
الناتج:
1
2
3
1
(4) الدوال المغلقة مقابل الدوال العادية
| البعد | الدالة العادية | الإغلاق |
|---|---|---|
| ذات حالة | بلا حالة | ذات حالة (متغيرات محفوظة) |
| الذاكرة | ثابتة | في كل مرة يتم فيها إنشاء مثيل جديد |
| حالات الاستخدام | الحساب البحت | المصانع، والزخارف، ووظائف الاستدعاء |
7. الدالة init وتهيئة الحزمة
(1) خصائص دالة init
| الميزة | الوصف |
|---|---|
| لا توجد معلمات، ولا قيمة إرجاع | func init() |
| الاستدعاء التلقائي | يتم تنفيذه تلقائيًا عند استيراد الحزمة |
| متعددة | يمكن أن تحتوي الحزمة على عدة دوال تهيئة (يتم تنفيذها بالترتيب الذي تم الإعلان عنها به) |
| قبل main | يتم تنفيذه قبل main() |
▶ مثال: تهيئة الحزمة (السجل)
// 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)
}
الناتج:
[init] registry initialized
[init] debug enabled
Registry: map[debug:1 max_connections:100 version:1]
(3) init مقابل main
| الوظيفة | متى يتم استدعاؤها | الغرض |
|---|---|---|
init() |
يتم تنفيذه تلقائيًا عند استيراد الحزمة | يقوم بتهيئة المتغيرات العالمية وتسجيل برامج التشغيل |
main() |
يتم تنفيذه عند بدء تشغيل البرنامج (مرة واحدة) | نقطة دخول البرنامج |
8. الدوال كمعاملات وقيم إرجاع (الدوال ذات الترتيب الأعلى)
تُعتبر الدوال في لغة Go عناصر من الدرجة الأولى — حيث يمكن تعيينها إلى متغيرات، وتمريرها كمعاملات، وإرجاعها كقيم.
(1) الدوال كمعاملات (دوال الاستدعاء)
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)
}
الناتج:
doubled: [2 4 6 8 10]
squared: [1 4 9 16 25]
(2) الدوال كقيم إرجاع (المصنع)
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
}
الناتج:
15
105
▶ مثال: أنواع الدوال + الدوال ذات الترتيب الأعلى (map/filter/reduce)
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)
}
الناتج:
doubled: [2 4 6 8 10 12 14 16 18 20]
evens: [2 4 6 8 10]
sum: 55
9. مثال كامل: مسار معالجة البيانات
اجمع بين جميع ميزات هذه الوظيفة لإنشاء خط أنابيب بيانات ETL (استخراج - تحويل - تحميل):
// 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)
}
النتيجة المتوقعة:
=== 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 وvar — var على مستوى الحزمة، بينما init على مستوى الدالة.
❓ أسئلة شائعة
func foo(a ...int) أو أسماء دالات مختلفة.return التلقائية على تقليل حجم الكود؛ (2) يمكن تعديل قيم الإرجاع في defer (مهم! يتم استخدام هذا في المثال الشامل في هذا الدرس)؛ (3) التوثيق — تعمل توقيعات الدوال كوثيقة توضيحية.init يدويًّا؟init إلا تلقائيًّا بواسطة بيئة تشغيل Go، ولا يتم تنفيذ init لكل حزمة إلا مرة واحدة. وستؤدي محاولة استدعاء init يدويًّا إلى حدوث خطأ في الترجمة.nums ...int هي في جوهرها []int مقطع داخل الدالة، لكن قواعد الاستدعاء تختلف: foo(1,2,3) مقابل foo([]int{1,2,3}...). الأولى هي مجرد «سكر نحوي».📖 ملخص
- يتكون تعريف الدالة من 4 عناصر:
func+ الاسم + قائمة المعلمات + نوع القيمة المرجعة - تعد القيم المتعددة للعودة سمة مميزة للغة Go، وتُستخدم عادةً في نمط معالجة الأخطاء
(value, error) - إن تسمية قيم الإرجاع تجعل عبارات
returnأكثر إيجازًا وتسمح لـdeferبتعديل قيمة الإرجاع - الحجج المتغيرة العدد
...كبديل للتحميل الزائد؛ حيث يتم تعريف الشريحة داخل الدالة - تلتقط الإغلاقات المتغيرات الخارجية، مما يتيح تنفيذ الدوال ذات الحالة (مثل العدادات ودوال المصنع)
- يتم تنفيذ
initتلقائيًا عند استيراد الحزمة، ويُستخدم لتهيئة الحالة العامة - تُعتبر الدوال كيانات من الدرجة الأولى: يمكن تعيين قيمها للمتغيرات، وتمريرها كمعاملات، وإرجاعها؛ كما أنها تدعم الدوال ذات الترتيب الأعلى (map/filter/reduce)
📝 تمارين
-
المسألة الأساسية (صعوبة ⭐): اكتب دالة
max(nums ...int) intتُرجع القيمة الأكبر بين جميع المعلمات. يجب أن تستخدم الدالة معلمات متغيرة العدد؛ ويجب أن يُنتج استدعاءmax(1, 5, 3, 9, 2)القيمة 9. -
مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ
makeBankAccount(initial int) (deposit func(int) int, withdraw func(int) (int, bool), balance func() int)لمحاكاة حساب مصرفي:depositتزيد الرصيد وتُرجع الرصيد الجديد؛withdrawتخصم أموالًا (وتُرجعfalseإذا كان الرصيد غير كافٍ)؛balanceتستعلم عن الرصيد. يجب عليك استخدام الإغلاقات. -
مشكلة التحدي (الصعوبة ⭐⭐⭐): قم بتنفيذ خط أنابيب بيانات وظيفي: عند إعطاء المدخلات
[]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.