تصرّح الكلمة المفتاحية func عن كل سلوك في برنامج Go. فالسطر func parse(body []byte) (Request, error) دالة. والسطر func (s *Server) Start() error دالة تابعة (method)، لأن له مستقبِلًا (receiver) قبل اسمه. أما func(w http.ResponseWriter, r *http.Request) { ... } بلا اسم فهي دالة حرفية (function literal)، ويمكنها التقاط المتغيرات المحيطة بها، وبها تكتب Go الـ closures. كلمة مفتاحية واحدة تغطي الحالات الثلاث، وقواعد المعاملات والنتائج هي نفسها في كل منها (مواصفات Go).
الخلاصة
- تصريح الدالة هو
func name(params) results { body }. والمعاملات المتتالية من النوع نفسه تشترك في كتابته مرة واحدة، كما فيclamp(n, lo, hi int). - لا يوجد في Go تحميل زائد (overloading) ولا وسائط افتراضية. فدالتان بالاسم نفسه تفشلان بالخطأ
redeclared in this block. استخدم اسمًا مختلفًا أو struct للخيارات أو الخيارات الوظيفية (functional options). - يمكن للدوال أن تعيد عدة قيم. والزوج
(T, error)هو طريقة Go في الإبلاغ عن الفشل، و_يتجاهل نتيجة لا تحتاج إليها. - توثّق النتائج المُسمّاة ما تعيده الدالة، وتتيح لـ closure مؤجَّل أن يضيف سياقًا إلى
errقبل الخروج. - تجعل
...Tالدالة متغيرة الوسائط (variadic). وداخل الدالة يكون المعامل[]T، وتفردparts...شريحة (slice) داخل الاستدعاء. - الدالة التابعة دالة لها مستقبِل. استخدم مستقبِلًا من نوع مؤشر حين تغيّر الدالة التابعة القيمة أو حين يحتوي النوع على
sync.Mutex. - الدالة التابعة ذات المستقبِل من نوع مؤشر ليست ضمن مجموعة الدوال التابعة لنوع القيمة، لذا لا يحقق
API{}الـ interfacehttp.Handlerحين يملك*APIوحدهServeHTTP. - الدوال قيم. يمكنك تخزينها وتمريرها إلى
slices.SortFuncأوhttp.HandleFunc، ولا يمكنك مقارنتها إلا بـnil. - تلتقط الدالة الحرفية المتغيرات نفسها، لا نسخًا منها. ومنذ Go 1.22 تحصل كل دورة من الحلقة على متغير حلقة خاص بها، لذا ترى الـ goroutines التي تبدأ داخل حلقة القيمة التي تتوقعها.
- تمنح الخيارات الوظيفية (
NewServer(addr, opts ...Option)) دوال الإنشاء إعدادات اختيارية. ومنذ Go 1.27 يمكن للدوال التابعة أن تصرّح عن معاملات أنواع خاصة بها، كما فيfunc (c *Client) GetJSON[T any](path string) (T, error).
كيف تصرّح عن دالة في Go؟
اكتب func ثم اسمًا ثم قائمة المعاملات ثم أنواع النتائج. ومحلّل رمز الوصول (token) مثال نموذجي على دالة صغيرة:
example.gogopackage main import ( "errors" "fmt" "strings" ) func parseBearer(header string) (string, error) { token, ok := strings.CutPrefix(header, "Bearer ") if !ok || token == "" { return "", errors.New("missing bearer token") } return token, nil } func clamp(n, lo, hi int) int { return max(lo, min(n, hi)) } func main() { token, err := parseBearer("Bearer eyJhbGciOi") fmt.Println(token, err) _, err = parseBearer("Basic dXNlcjpwYXNz") fmt.Println(err) fmt.Println(clamp(250, 1, 100)) }
example.texttexteyJhbGciOi <nil> missing bearer token 100
تأتي الأنواع بعد الأسماء، في المعاملات والنتائج على حد سواء. وحين تشترك عدة معاملات متتالية في نوع واحد، تكتبه مرة واحدة، لذا تأخذ clamp(n, lo, hi int) ثلاث قيم int. والنتيجة الواحدة غير المُسمّاة لا تحتاج إلى أقواس. أما نتيجتان أو أكثر فتحتاج إليها.
تتبع أسماء الدوال قاعدة الظهور نفسها التي يتبعها كل معرّف آخر في Go. فالدالة ParseBearer ستكون مُصدَّرة ويمكن استدعاؤها من حزم أخرى، أما parseBearer فتبقى داخل حزمتها. ويشرح مقال الكلمة المفتاحية package في Go الأسماء المُصدَّرة بتفصيل أكبر.
هل تدعم Go التحميل الزائد للدوال (overloading) أو الوسائط الافتراضية؟
لا هذا ولا ذاك. لا يمكن أن تحتوي الحزمة إلا على دالة واحدة باسم معيّن، مهما كانت معاملاتها:
example.gogofunc dial(addr string) error { return nil } func dial(addr string, timeout time.Duration) error { return nil }
example.texttext./main.go:7:6: dial redeclared in this block ./main.go:5:6: other declaration of dial
أما القيم الافتراضية فتفشل عند المحلّل النحوي (parser)، قبل أن يبدأ فحص الأنواع أصلًا:
example.texttext./main.go:5:46: syntax error: unexpected = in parameter list; possibly missing comma or )
تُظهر المكتبة القياسية ما تفعله Go بدلًا من ذلك. فالدالتان net.Dial و net.DialTimeout اسمان لسلوكين مختلفين. والنوع http.Server هو struct تعني حقوله الصفرية «استخدم القيمة الافتراضية»، لذا لا تضبط إلا الحقول التي تهمك. أما دوال الإنشاء ذات الإعدادات الاختيارية الكثيرة، فيستخدم كود Go معها الخيارات الوظيفية، التي نشرحها لاحقًا. والاسم في كل موضع استدعاء يخبرك بالدالة التي ستعمل.
كيف تعمل القيم المُعادة المتعددة في Go؟
يمكن للدالة أن تعيد أي عدد من القيم، ويستقبلها المستدعي بإسناد متعدد القيم. والشكل الأكثر شيوعًا نتيجة مع error، كما في parseBearer أعلاه. يفحص المستدعي الخطأ قبل أن يستخدم النتيجة، ويتأكد المترجم من أن الاستدعاء الذي يعيد نتيجتين لا يُستخدم حيث تُتوقَّع قيمة واحدة.
وحين لا تحتاج إلى إحدى القيم، أسندها إلى المعرّف الفارغ _. ولا يمكنك إسقاطها كليًا. فالسطر token := parseBearer(h) يفشل بالخطأ assignment mismatch: 1 variable but parseBearer returns 2 values.
متى تستخدم معاملات النتائج المُسمّاة؟
تعطي النتائج المُسمّاة كل نتيجة اسمًا وتصرّح عنها كمتغير يبدأ بقيمته الصفرية. وعندها تعيد return بلا قيم، وتُسمّى الإعادة المجرّدة (bare return)، ما تحمله تلك المتغيرات:
example.gogofunc splitHostPort(addr string) (host, port string) { for i := len(addr) - 1; i >= 0; i-- { if addr[i] == ':' { host, port = addr[:i], addr[i+1:] return } } host = addr return }
يعيد splitHostPort("api.levelupgo.dev:443") القيمتين api.levelupgo.dev و 443. تفيد الأسماء هنا لأنه لولاها لما دلّت نتيجتان من النوع string على أيهما أيهما. وتقترح Go Code Review Comments الإعادة المجرّدة في الدوال القصيرة مثل هذه فقط. ففي دالة من 60 سطرًا تخفي return المجرّدة ما يُعاد.
تظهر أهمية النتائج المُسمّاة أكثر ما تظهر حين تحتاج دالة مؤجَّلة إلى تعديل الخطأ قبل أن يراه المستدعي:
example.gogofunc loadConfig(path string) (cfg Config, err error) { defer func() { if err != nil { err = fmt.Errorf("load config %s: %w", path, err) } }() data, err := os.ReadFile(path) if err != nil { return Config{}, err } err = json.Unmarshal(data, &cfg) return cfg, err }
example.texttextload config /etc/levelupgo/config.json: open /etc/levelupgo/config.json: no such file or directory
كل return تسند قيمها أولًا إلى cfg و err، ثم تعمل الدالة المؤجَّلة. ولأن err نتيجة مُسمّاة، يستطيع الـ closure أن يغلّفها مرة واحدة لكل مسارات الخروج. ولولا الاسم لما استطاعت الدالة المؤجَّلة الوصول إلى القيمة المُعادة.
ما الدوال متغيرة الوسائط (variadic) في Go؟
المعامل الأخير المكتوب على شكل ...T يقبل صفرًا أو أكثر من الوسائط من النوع T. وداخل الدالة يكون []T عاديًا:
example.gogofunc buildURL(base string, segments ...string) string { return strings.TrimSuffix(base, "/") + "/" + strings.Join(segments, "/") } func main() { fmt.Println(buildURL("https://api.levelupgo.dev/", "v1", "users", "42")) fmt.Println(buildURL("https://api.levelupgo.dev")) parts := []string{"v1", "orders", "1001"} fmt.Println(buildURL("https://api.levelupgo.dev", parts...)) }
example.texttexthttps://api.levelupgo.dev/v1/users/42 https://api.levelupgo.dev/ https://api.levelupgo.dev/v1/orders/1001
تمرّر parts... شريحة موجودة بوصفها الوسيط المتغير دون نسخها. ولا يمكنك الخلط بين الصيغتين، لذا يفشل buildURL(base, "v2", parts...):
example.texttext./main.go:18:58: too many arguments in call to buildURL have (string, string, []string...) want (string, ...string)
والمكتبة القياسية مليئة بها. فكل من fmt.Println(a ...any) و append(s, elems...) و errors.Join(errs ...error) وأزواج المفتاح والقيمة في slog.Info(msg, args ...any) يأخذ هذا الشكل.
كيف تعمل الدوال التابعة في Go؟
الدالة التابعة دالة لها مستقبِل، يُكتب بين قوسين بين func والاسم. وغالبًا ما تكون المستقبِلات structs، ويشرحها مقال الكلمة المفتاحية struct في Go من الحقول إلى التضمين. ويمكن أن يكون المستقبِل النوع نفسه أو مؤشرًا إليه، وهذا الاختيار يحدد هل تعمل الدالة التابعة على قيمة المستدعي أم على نسخة منها. ويوضح محدِّد معدّل (rate limiter) بمستقبِل بالقيمة الفرق:
example.gogotype RateLimiter struct { limit int used int } func (r RateLimiter) Allow() bool { if r.used >= r.limit { return false } r.used++ return true } func main() { limiter := RateLimiter{limit: 2} for range 4 { fmt.Print(limiter.Allow(), " ") } fmt.Println() }
example.texttexttrue true true true
لا يمنع هذا المحدِّد أي شيء. فكل استدعاء يحصل على نسخة جديدة من limiter، ويزيد used في النسخة ثم يتخلص منها. وتغيير المستقبِل إلى func (r *RateLimiter) Allow() bool يجعل الدالة التابعة تحدّث القيمة الأصلية، ويصبح الناتج true true false false. ولا تحتاج إلى كتابة (&limiter).Allow(). فـ Go تأخذ العنوان نيابةً عنك حين يكون المتغير قابلًا للعنونة (addressable).
متى تستخدم مستقبِلًا من نوع مؤشر؟
استخدم مستقبِلًا من نوع مؤشر حين يتحقق أي مما يلي:
- تغيّر الدالة التابعة المستقبِل، مثل
Allowأعلاه. - يحتوي النوع على
sync.Mutexأو على قيمة أخرى يجب ألا تُنسخ. - الـ struct كبيرة، فيكلّف نسخها في كل استدعاء أكثر من تتبّع مؤشر.
المستقبِل بالقيمة func (r T) | المستقبِل من نوع مؤشر func (r *T) | |
|---|---|---|
| يغيّر قيمة المستدعي | لا، يعمل على نسخة | نعم |
آمن مع حقل sync.Mutex | لا، يُنسخ القفل | نعم |
يمكن استدعاؤه على متغير من النوع T | نعم | نعم، تأخذ Go العنوان |
ضمن مجموعة الدوال التابعة لـ T | نعم | لا |
ضمن مجموعة الدوال التابعة لـ *T | نعم | نعم |
حالة الـ mutex خطأ يكتشفه go vet:
example.gogotype SessionCache struct { mu sync.Mutex sessions map[string]string } func (c SessionCache) Get(token string) (string, bool) { c.mu.Lock() defer c.mu.Unlock() user, ok := c.sessions[token] return user, ok }
example.texttextmain.go:13:9: Get passes lock by value: app.SessionCache contains sync.Mutex
كل استدعاء يقفل نسخته الخاصة من الـ mutex، فلا تنتظر الـ goroutines التي تستدعي Get بعضها بعضًا أبدًا، ولا يحمي القفل شيئًا. أما الأنواع الصغيرة غير القابلة للتغيير مثل time.Time أو UserID فلا مشكلة في أن تكون مستقبِلاتها بالقيمة. وتطلب Code Review Comments أيضًا ألا تخلط بين النوعين على نوع واحد. فإذا احتاجت دالة تابعة واحدة إلى مستقبِل من نوع مؤشر، فأعطِ الدوال التابعة كلها مستقبِلًا من نوع مؤشر (ويكي Go).
لماذا لا يحقق نوعي interface ما؟
من الأسباب الشائعة المستقبِل من نوع مؤشر. فمجموعة الدوال التابعة لنوع القيمة T لا تضم إلا الدوال التابعة ذات المستقبِل T. أما مجموعة الدوال التابعة لـ *T فتضم النوعين كليهما. لذا حين يكون مستقبِل ServeHTTP من نوع مؤشر، يكون *API وحده http.Handler:
example.gogotype API struct { version string } func (a *API) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, a.version) } func main() { var h http.Handler = API{version: "v1"} _ = h }
example.texttext./main.go:17:23: cannot use API{…} (value of struct type API) as http.Handler value in variable declaration: API does not implement http.Handler (method ServeHTTP has pointer receiver)
كتابة &API{version: "v1"} تحل المشكلة. وتوجد هذه القاعدة لأن الـ interface يمكن أن تحمل نسخة من قيمة، والدالة التابعة ذات المستقبِل من نوع مؤشر إذا استُدعيت على تلك النسخة ستغيّر شيئًا لا يراه المستدعي أبدًا. ويعرض مقال الكلمة المفتاحية var في Go النمط var _ http.Handler = (*T)(nil) الذي يفحص هذا وقت الترجمة، ويشرح مقال الكلمة المفتاحية interface في Go مجموعات الدوال التابعة والتحقيق الضمني بالتفصيل.
ما الـ method values والـ method expressions؟
srv.health دون أقواس هي method value. إنها دالة رُبط بها المستقبِل مسبقًا، لذا يمكنك تمريرها مباشرةً إلى أي شيء يتوقع دالة عادية:
example.gogotype Server struct { version string } func (s *Server) health(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "ok %s\n", s.version) } func main() { srv := &Server{version: "1.4.2"} mux := http.NewServeMux() mux.HandleFunc("GET /health", srv.health) rec := httptest.NewRecorder() mux.ServeHTTP(rec, httptest.NewRequest("GET", "/health", nil)) fmt.Print(rec.Body.String()) handle := (*Server).health fmt.Printf("%T\n", handle) }
example.texttextok 1.4.2 func(*main.Server, http.ResponseWriter, *http.Request)
وبهذه الطريقة تسجّل خوادم Go كثيرة مساراتها. فالمعالجات (handlers) دوال تابعة على struct تحمل قاعدة البيانات والمسجّل (logger) والإعدادات، وتستقبل mux.HandleFunc الـ method values المربوطة. أما (*Server).health فهي method expression. وهي تحوّل المستقبِل إلى معامل أول عادي، وهذا مفيد حين يختار جدول الدالة التابعة التي تُستدعى وقت التشغيل.
ما قيم الدوال وأنواع الدوال؟
الدوال قيم في Go. يمكنك إسنادها إلى متغيرات، وتخزينها في حقول structs وفي maps، وتمريرها وسائط، وإعادتها من دوال أخرى. ونوع قيمة الدالة هو توقيعها:
example.gogotype Order struct { ID int64 Total int64 } func main() { orders := []Order{{ID: 1, Total: 4999}, {ID: 2, Total: 1250}, {ID: 3, Total: 8900}} slices.SortFunc(orders, func(a, b Order) int { return cmp.Compare(b.Total, a.Total) }) fmt.Println(orders) bigOrder := func(o Order) bool { return o.Total > 5000 } fmt.Println(slices.IndexFunc(orders, bigOrder)) fmt.Printf("%T\n", bigOrder) }
example.texttext[{3 8900} {1 4999} {2 1250}] 0 func(main.Order) bool
لا تعرف slices.SortFunc كيف ترتّب الـ structs الخاصة بك، لذا تمرّر إليها دالة مقارنة. والفكرة نفسها حاضرة في المكتبة القياسية كلها: تأخذ http.HandleFunc دالة معالجة، وتأخذ strings.FieldsFunc دالة تختبر الفواصل، وتأخذ sync.OnceValue الدالة التي تعمل مرة واحدة.
القيمة الصفرية لنوع الدالة هي nil، واستدعاء دالة قيمتها nil يسبب panic. وهذه أيضًا المقارنة الوحيدة التي تسمح بها Go:
example.texttext./main.go:9:5: invalid operation: health == ready (func can only be compared to nil)
وبما أنك لا تستطيع مقارنة دالتين، فلا يمكن أن تكونا مفاتيح في map أيضًا. وحين تريد مجموعة من المعالجات، اجعل مفتاح الـ map هو الاسم بدلًا من ذلك.
ويمكن أيضًا أن يحصل التوقيع على اسم خاص به باستخدام type، ويمكن أن تكون لنوع الدالة المُسمّى دوال تابعة. وبهذه الطريقة يحوّل http.HandlerFunc دالة عادية إلى http.Handler، ويشرح مقال الكلمة المفتاحية type في Go ذلك خطوة بخطوة مع مثال RetryPolicy.
كيف تعمل الـ closures في Go؟
يمكن للدالة الحرفية أن تستخدم متغيرات من الدالة المحيطة بها. وهي لا تنسخها، بل تلتقط المتغيرات نفسها، لذا يرى الطرفان أي تغيير يحدث في أي منهما. ودالة مساعدة لإعادة المحاولة (retry) توضح ذلك:
example.gogovar errUnavailable = errors.New("503 service unavailable") func retry(attempts int, fn func() error) error { var err error for range attempts { if err = fn(); err == nil { return nil } } return err } func main() { calls := 0 err := retry(3, func() error { calls++ return errUnavailable }) fmt.Println(calls, err) }
example.texttext3 503 service unavailable
يعيش calls في main، لكن الدالة الحرفية تزيده، وترى main القيمة 3 بعد ذلك. ويبقى المتغير الملتقَط حيًّا ما دام أي closure يشير إليه، حتى بعد أن تعود الدالة المحيطة. وينقل المترجم هذه المتغيرات إلى الـ heap حين يحتاج إلى ذلك.
والـ middleware في HTTP من أكثر الـ closures شيوعًا في كود Go. فالمعالج المُعاد يحتفظ بـ logger و next من الاستدعاء الذي بناه:
example.gogofunc logRequests(logger *slog.Logger, next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, r) logger.Info("request", "method", r.Method, "path", r.URL.Path) }) }
وإذا غلّفت به معالجًا وأرسلت عبره DELETE /sessions/42، فسيسجّل level=INFO msg=request method=DELETE path=/sessions/42. وكل استدعاء لـ logRequests ينتج closure منفصلًا له next خاص به، لذا يمكنك تغليف كل مسار بمعالج مختلف. ولمعرفة الطبقات الكاملة لبيئة الإنتاج المبنية من closures كهذا، بما في ذلك الترتيب والتعافي من panic والاختبارات، راجع أفضل ممارسات الـ middleware في Go.
وسترى أيضًا صيغتين قصيرتين كثيرًا. الأولى defer func() { ... }()، وهي تشغّل closure حين تعود الدالة، وبها يضيف مثال loadConfig سياقًا إلى خطئه، وبها تُستدعى recover. والثانية go func() { ... }()، وهي تبدأ closure في goroutine جديدة. والقوسان () في النهاية يستدعيان الدالة الحرفية. وإذا حذفتهما، يعترض المترجم بأن التعبير في defer أو go يجب أن يكون استدعاء دالة.
هل أصلحت Go 1.22 الـ closures داخل الحلقات؟
نعم. قبل Go 1.22 كان لحلقة for متغير حلقة واحد تتشاركه كل الدورات، وكانت الـ closures تلتقط ذلك المتغير الوحيد. فمدقّق الحالة (health checker) هذا كان يبدأ ثلاث goroutines تطبع في الغالب كلها آخر URL:
example.gogourls := []string{"/health", "/ready", "/metrics"} var wg sync.WaitGroup for _, url := range urls { wg.Add(1) go func() { defer wg.Done() fmt.Println("checking", url) }() } wg.Wait()
مع go 1.21 في go.mod، طبعت كل الأسطر checking /metrics في ثلاث مرات تشغيل منفصلة، وحذّر go vet بالرسالة loop variable url captured by func literal. ومع go 1.22 أو ما بعده، يكون لكل دورة url خاص بها، ويفحص الكود نفسه المسارات الثلاثة كلها بأي ترتيب تنتهي به الـ goroutines (مدونة Go). ويرتبط هذا التغيير بسطر go في go.mod، لذا تحتفظ الوحدة (module) القديمة بالسلوك القديم إلى أن ترفع الإصدار. ولم تعد هناك حاجة إلى النسخ القديم url := url داخل الحلقة، ويزيله المُحدِّث forvar في go fix. ويتناول مقال الكلمة المفتاحية for في Go بقية قواعد متغيرات الحلقة، مثل سبب عدم تغيّر الـ slice عند تعديل قيمة range.
ما الخيارات الوظيفية (functional options) في Go؟
تجمع الخيارات الوظيفية بين المعاملات متغيرة الوسائط والـ closures لتمنح دالة الإنشاء إعدادات اختيارية دون تحميل زائد أو وسائط افتراضية. وكل خيار دالة تغيّر القيمة التي يجري بناؤها:
example.gogotype Server struct { addr string readTimeout time.Duration maxBodyBytes int64 logger *slog.Logger } type Option func(*Server) func WithReadTimeout(d time.Duration) Option { return func(s *Server) { s.readTimeout = d } } func WithMaxBodyBytes(n int64) Option { return func(s *Server) { s.maxBodyBytes = n } } func NewServer(addr string, opts ...Option) *Server { s := &Server{ addr: addr, readTimeout: 5 * time.Second, maxBodyBytes: 1 << 20, logger: slog.Default(), } for _, opt := range opts { opt(s) } return s } func main() { a := NewServer(":8080") b := NewServer(":8080", WithReadTimeout(30*time.Second), WithMaxBodyBytes(10<<20)) fmt.Println(a.readTimeout, a.maxBodyBytes) fmt.Println(b.readTimeout, b.maxBodyBytes) }
example.texttext5s 1048576 30s 10485760
يحصل NewServer(":8080") على كل القيم الافتراضية، ولا يسمّي المستدعون إلا ما يغيّرونه. وإضافة خيار لاحقًا لا تكسر أي استدعاء موجود. وتستخدم grpc.NewServer(opts ...ServerOption) في gRPC وكثير من عملاء قواعد البيانات هذا النمط. أما النوع الذي له إعدادان أو ثلاثة، فالـ struct العادية للإعدادات أبسط وأسهل قراءة، لذا الجأ إلى الخيارات حين تظل قائمة الإعدادات في ازدياد.
كيف تعمل الدوال العامة (generic functions) في Go؟
يمكن للدالة أن تصرّح عن معاملات أنواع بين أقواس مربعة بين اسمها ومعاملاتها. وتعمل Map مع أي نوع للمدخلات وأي نوع للمخرجات:
example.gogofunc Map[T, U any](items []T, fn func(T) U) []U { out := make([]U, 0, len(items)) for _, item := range items { out = append(out, fn(item)) } return out } emails := Map(users, func(u User) string { return u.Email })
يستنتج المترجم أن T هو User وأن U هو string من الوسائط، لذا لا يحتاج الاستدعاء إلى أقواس مربعة. والحزم slices و maps و cmp في المكتبة القياسية مبنية من دوال عامة كهذه.
حتى Go 1.27 لم يكن بإمكان الدوال التابعة أن تصرّح عن معاملات أنواع خاصة بها. وكل ما أمكنها هو استخدام معاملات الأنواع الخاصة بنوع المستقبِل. وتزيل Go 1.27 هذا القيد، فيمكن لعميل أن يملك دالة تابعة عامة تفك الترميز إلى أي نوع يطلبه المستدعي (ملاحظات إصدار Go 1.27):
example.gogotype Client struct { baseURL string http *http.Client } func (c *Client) GetJSON[T any](path string) (T, error) { var out T resp, err := c.http.Get(c.baseURL + path) if err != nil { return out, err } defer resp.Body.Close() err = json.NewDecoder(resp.Body).Decode(&out) return out, err } u, err := c.GetJSON[User]("/users/1")
مع خادم اختبار يعيد مستخدمًا واحدًا، يطبع هذا {1 [email protected]} <nil>. ويجب كتابة وسيط النوع صراحةً هنا، لأنه لا شيء في "/users/1" يخبر المترجم بما هو T. ويشرح مقال ما الجديد في Go 1.27 الدوال التابعة العامة وحدودها، بما في ذلك سبب عدم قدرتها على تحقيق الدوال التابعة في الـ interfaces.
ما main و init في Go؟
لاسمين من أسماء الدوال معنى خاص. فالدالة func main() في package main هي نقطة بداية البرنامج، وينتهي البرنامج حين تعود. وهي لا تأخذ أي وسائط ولا تعيد شيئًا:
example.texttext./main.go:9:6: func main must have no arguments and no return values
تأتي وسائط سطر الأوامر من os.Args أو من الحزمة flag، ويأتي رمز الخروج من os.Exit. ويشرح مقال الكلمة المفتاحية package في Go الحزمة package main بالتفصيل.
تعمل func init() تلقائيًا بعد تهيئة متغيرات الحزمة وقبل main. ويمكن أن تحتوي الحزمة على عدة دوال init، حتى في ملف واحد، وتعمل بترتيب ظهورها. ولا يمكنك استدعاء أي منها بنفسك. فكتابة init() في كودك تفشل بالخطأ undefined: init. ومعظم استخدامات init للتسجيل، مثل مشغّل قاعدة بيانات يضيف نفسه إلى database/sql. ويشرح مقال الكلمة المفتاحية var في Go ترتيب التهيئة ومتى يكون var على مستوى الحزمة الخيار الأوضح.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تقدّم دورة Go Basics الدوال والقيم المُعادة المتعددة والأخطاء بوصفها قيمًا. وتتعمق دورة Go Language Deep Dives أكثر في الدوال متغيرة الوسائط والنتائج المُسمّاة و panic و recover. ويقدّم Training Ground تمارين قصيرة مستقلة على التوقيعات والدوال التابعة والـ closures للتدرّب خارج أي دورة. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل func كلمة مفتاحية في Go؟
نعم. func واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك استخدامها اسمًا. وهي تصرّح عن الدوال والدوال التابعة، وتبدأ الدوال الحرفية وأنواع الدوال. وعادةً ما يسمّي كود Go المتغير الذي يحمل دالة fn.
هل تدعم Go التحميل الزائد للدوال أو المعاملات الافتراضية؟
لا هذا ولا ذاك. لا يمكن التصريح عن كل اسم إلا مرة واحدة في كل حزمة، لذا تفشل func dial(...) الثانية بالخطأ dial redeclared in this block، والقيمة الافتراضية في قائمة المعاملات خطأ نحوي. وتستخدم Go بدلًا من ذلك أسماء مختلفة، مثل net.Dial و net.DialTimeout، أو struct للإعدادات تعني حقولها الصفرية «استخدم القيمة الافتراضية»، مثل http.Server، أو الخيارات الوظيفية.
هل أستخدم مستقبِلًا بالقيمة أم مستقبِلًا من نوع مؤشر في Go؟
استخدم مستقبِلًا من نوع مؤشر حين تعدّل الدالة التابعة المستقبِل، أو حين يحتوي النوع على sync.Mutex أو ما يشبهه، أو حين تكون الـ struct كبيرة. واستخدم مستقبِلًا بالقيمة للأنواع الصغيرة التي تتصرف كقيم، مثل time.Time. واجعل كل الدوال التابعة على النوع الواحد متسقة. وتذكّر أن *T وحده يضم الدوال التابعة ذات المستقبِل من نوع مؤشر في مجموعة دواله التابعة، وهذا مهم لتحقيق الـ interfaces.
ما func() في Go؟
func() هو نوع دالة لا تأخذ أي وسائط ولا تعيد شيئًا. وأي توقيع يُكتب دون اسم هو نوع دالة، مثل func(context.Context) error. وتستخدم هذه الأنواع لدوال الاستدعاء الراجع (callbacks) وحقول الـ structs والمعاملات مثل sync.OnceFunc(f func()). أما func() { ... } مع جسم فهي دالة حرفية من ذلك النوع.
هل يمكنك مقارنة الدوال في Go؟
بـ nil فقط. يفشل health == ready بالخطأ func can only be compared to nil، وللسبب نفسه لا يمكن أن يكون نوع الدالة مفتاحًا في map. وحين تحتاج إلى البحث عن المعالجات، خزّنها في map[string]http.HandlerFunc مفتاحها الاسم.
المصادر
- The Go Programming Language Specification, Function declarations: https://go.dev/ref/spec#Function_declarations
- The Go Programming Language Specification, Method declarations: https://go.dev/ref/spec#Method_declarations
- The Go Programming Language Specification, Method sets: https://go.dev/ref/spec#Method_sets
- The Go Programming Language Specification, Function literals: https://go.dev/ref/spec#Function_literals
- The Go Programming Language Specification, Passing arguments to ... parameters: https://go.dev/ref/spec#Passing_arguments_to_..._parameters
- Effective Go, Functions: https://go.dev/doc/effective_go#functions
- Go Code Review Comments, Named result parameters and Receiver type: https://go.dev/wiki/CodeReviewComments
- The Go Blog, Fixing For Loops in Go 1.22: https://go.dev/blog/loopvar-preview
- Go 1.27 Release Notes: https://go.dev/doc/go1.27
