الكتاب الأساسي

الدوال في Go

سُبِق أن شُرحت تعريفات الدوال واستدعاءاتها. وستتناول هذه المقالة المزيد من المفاهيم والتفاصيل المتعلقة بالدوال في Go.

في الواقع، الدوال أحد أنواع المواطنين من الدرجة الأولى (first-class citizen) في Go. بعبارة أخرى، يمكننا استخدام الدوال كقيم. ورغم أن Go لغة ساكنة (static language)، فإن دوال Go مرنة جدًا. ويشبه الإحساس باستخدام دوال Go كثيرًا الإحساس باستخدام العديد من اللغات الديناميكية.

هناك بعض الدوال المدمجة في Go. وهذه الدوال موثّقة في حزمتَي الشيفرة القياسيتين builtin وunsafe. وتختلف الدوال المدمجة عن الدوال المخصّصة في عدة نواحٍ. وستُذكر هذه الفروق أدناه.

توقيعات الدوال وأنواع الدوال

تتكوّن القيمة الحرفية لنوع الدالة من الكلمة المفتاحية func وقيمة حرفية لتوقيع دالة. ويتكوّن توقيع الدالة من قائمتَي أنواع: إحداهما قائمة أنواع وسائط الإدخال، والأخرى قائمة أنواع نتائج الإخراج. ويمكن أن تظهر أسماء الوسائط والنتائج في القيم الحرفية لأنواع الدوال والتوقيعات، لكن الأسماء ليست مهمة.

عمليًا، يمكن أن تظهر الكلمة المفتاحية func في القيم الحرفية للتوقيعات أو لا تظهر. ولهذا السبب، يمكننا اعتبار نوع الدالة وتوقيع الدالة المفهوم نفسه.

إليك قيمة حرفية لنوع دالة:


func (a int, b string, c string) (x int, y int, z bool)

تعلّمنا من مقالة تعريفات الدوال واستدعاءات الدوال أنه يمكن تعريف وسائط ونتائج متتالية من النوع نفسه معًا. لذا فإن القيمة الحرفية أعلاه مكافئة لـ


func (a int, b, c string) (x, y int, z bool)

وبما أن أسماء الوسائط وأسماء النتائج ليست مهمة في القيم الحرفية (طالما لا توجد أسماء غير فارغة مكرّرة)، فإن القيم أعلاه مكافئة للقيمة التالية.


func (x int, y, z string) (a, b int, c bool)

يمكن أن تكون أسماء المتغيرات (الوسائط والنتائج) المعرّف الفارغ _. والقيم أعلاه مكافئة للقيمة التالية.


func (_ int, _, _ string) (_, _ int, _ bool)

يجب أن تكون أسماء الوسائط إما كلها موجودة أو كلها غائبة (مجهولة). وتنطبق القاعدة نفسها على أسماء النتائج. والقيم أعلاه مكافئة للقيم التالية.


func (int, string, string) (int, int, bool) // الصيغة القياسية
func (a int, b string, c string) (int, int, bool)
func (x int, _ string, z string) (int, int, bool)
func (int, string, string) (x int, y int, z bool)
func (int, string, string) (a int, b int, _ bool)

تدل جميع القيم الحرفية لأنواع الدوال أعلاه على نوع الدالة نفسه (غير المسمّى).

يجب حصر كل قائمة وسائط بين () في القيمة الحرفية، حتى لو كانت قائمة الوسائط فارغة. وإذا كانت قائمة نتائج نوع دالة فارغة، فيمكن حذفها من القيمة الحرفية لنوع الدالة. وعندما تحتوي قائمة النتائج على نتيجة واحدة على الأكثر، فلا حاجة إلى حصر قائمة النتائج بين () إذا كانت القيمة الحرفية لقائمة النتائج لا تحتوي على أسماء نتائج.


// أنواع الدوال الثلاثة التالية متطابقة.
func () (x int)
func () (int)
func () int

// نوعا الدالة التاليان متطابقان.
func (a int, b string) ()
func (a int, b string)

الوسائط المتغيرة وأنواع الدوال المتغيرة الوسائط

يمكن أن يكون الوسيط الأخير في الدالة وسيطًا متغيرًا (variadic parameter). ويمكن أن يكون لكل دالة وسيط متغير واحد على الأكثر. ونوع الوسيط المتغير هو دائمًا نوع شريحة. وللإشارة إلى أن الوسيط الأخير متغير، يكفي أن تسبق نوع عنصره في نوعه (الشريحة) بثلاث نقاط ... في تعريفه. مثال:


func (values ...int64) (sum int64)
func (sep string, tokens ...string) string

يمكن تسمية نوع الدالة الذي يحتوي على وسيط متغير نوع دالة متغيرة الوسائط. ونوع الدالة المتغيرة الوسائط ونوع الدالة غير المتغيرة الوسائط ليسا متطابقين إطلاقًا.

ستُعرض بعض الأمثلة على الدوال المتغيرة الوسائط في قسم أدناه.

أنواع الدوال أنواع غير قابلة للمقارنة

ذُكر عدة مرات في Go 101 أن أنواع الدوال أنواع غير قابلة للمقارنة. لكن، مثل قيم الخرائط والشرائح، يمكن مقارنة قيم الدوال بالمعرّف nil العاري غير المنمَّط. (ستُشرح قيم الدوال في القسم الأخير من هذه المقالة.)

وبما أن أنواع الدوال أنواع غير قابلة للمقارنة، فلا يمكن استخدامها كأنواع مفاتيح أنواع الخرائط.

+++ النماذج الأولية للدوال

يتكوّن النموذج الأولي للدالة (function prototype) من اسم دالة ونوع دالة (أو توقيع). وتتكوّن قيمته الحرفية من الكلمة المفتاحية func واسم دالة والقيمة الحرفية لتوقيع دالة.

مثال على قيمة حرفية لنموذج أولي لدالة:


func Double(n int) (result int)

بعبارة أخرى، النموذج الأولي للدالة هو تعريف دالة بدون جزء الجسم. ويتكوّن تعريف الدالة من نموذج أولي للدالة وجسم للدالة.

تعريفات الدوال المتغيرة الوسائط واستدعاءات الدوال المتغيرة الوسائط

شُرحت تعريفات الدوال واستدعاءاتها العامة في تعريفات الدوال واستدعاءات الدوال. وهنا نقدّم كيفية تعريف الدوال المتغيرة الوسائط واستدعائها.

تعريفات الدوال المتغيرة الوسائط

تعريفات الدوال المتغيرة الوسائط مشابهة لتعريفات الدوال العامة. والفرق هو أن الوسيط الأخير في الدالة المتغيرة الوسائط يجب أن يكون وسيطًا متغيرًا. لاحظ أن الوسيط المتغير في الدالة المتغيرة الوسائط سيُعامَل كشريحة داخل جسم الدالة المتغيرة الوسائط.


// اجمع الأعداد المُدخَلة وأرجِع مجموعها.
func Sum(values ...int64) (sum int64) {
	// نوع values هو []int64.
	sum = 0
	for _, v := range values {
		sum += v
	}
	return
}

// دالة غير كفؤة لدمج النصوص.
func Concat(sep string, tokens ...string) string {
	// نوع tokens هو []string.
	r := ""
	for i, t := range tokens {
		if i != 0 {
			r += sep
		}
		r += t
	}
	return r
}

من تعريفي الدالة المتغيرة الوسائط أعلاه، نجد أنه إذا عُرّف وسيط متغير بجزء النوع ...T، فإن نوع الوسيط هو []T في الواقع.

في الواقع، الدوال Print وPrintln وPrintf في حزمة fmt القياسية كلها دوال متغيرة الوسائط.


func Print(a ...interface{}) (n int, err error)
func Printf(format string, a ...interface{}) (n int, err error)
func Println(a ...interface{}) (n int, err error)

أنواع الوسائط المتغيرة في الدوال الثلاث كلها []interface{}، ونوع عنصرها interface{} هو نوع واجهة. وستُشرح أنواع الواجهات وقيمها في الواجهات في Go لاحقًا.

استدعاءات الدوال المتغيرة الوسائط

هناك طريقتان لتمرير المعطيات إلى وسيط متغير من النوع []T:

  1. تمرير قيمة شريحة كمعطى وحيد. ويجب أن تكون الشريحة قابلة للإسناد إلى قيم من النوع []T، ويجب أن تتبع الشريحة ثلاث نقاط .... وتُسمّى الشريحة المُمرَّرة معطى متغيرًا.
  2. تمرير صفر أو أكثر من المعطيات القابلة للإسناد إلى قيم من النوع T. وستُنسخ هذه المعطيات (أو تُحوَّل) لتصبح عناصر لقيمة شريحة جديدة مُخصَّصة من النوع []T، ثم تُمرَّر الشريحة الجديدة المُخصَّصة إلى الوسيط المتغير.

لاحظ أنه لا يمكن خلط الطريقتين في استدعاء الدالة المتغيرة الوسائط نفسه.

برنامج مثال يستخدم بعض استدعاءات الدوال المتغيرة الوسائط:


package main

import "fmt"

func Sum(values ...int64) (sum int64) {
	sum = 0
	for _, v := range values {
		sum += v
	}
	return
}

func main() {
	a0 := Sum()
	a1 := Sum(2)
	a3 := Sum(2, 3, 5)
	// الأسطر الثلاثة أعلاه مكافئة
	// للأسطر الثلاثة التالية على التوالي.
	b0 := Sum([]int64{}...) // <=> Sum(nil...)
	b1 := Sum([]int64{2}...)
	b3 := Sum([]int64{2, 3, 5}...)
	fmt.Println(a0, a1, a3) // 0 2 10
	fmt.Println(b0, b1, b3) // 0 2 10
}

مثال آخر:


package main

import "fmt"

func Concat(sep string, tokens ...string) (r string) {
	for i, t := range tokens {
		if i != 0 {
			r += sep
		}
		r += t
	}
	return
}

func main() {
	tokens := []string{"Go", "C", "Rust"}
	// الطريقة 1
	langsA := Concat(",", tokens...)
	// الطريقة 2
	langsB := Concat(",", "Go", "C","Rust")
	fmt.Println(langsA == langsB) // true
}

لا يُصرَّف المثال التالي، لأن طريقتي استدعاء الدوال المتغيرة الوسائط مختلطتان.


package main

// راجع الأمثلة أعلاه لمعرفة التعريفين
// الكاملين للدالتين التاليتين.
func Sum(values ...int64) (sum int64)
func Concat(sep string, tokens ...string) string

func main() {
	// يفشل السطران التاليان في التصريف،
	// للخطأ نفسه:
	// عدد كبير جدًا من المعطيات في الاستدعاء.
	_ = Sum(2, []int64{3, 5}...)
	_ = Concat(",", "Go", []string{"C", "Rust"}...)
}

المزيد عن تعريفات الدوال واستدعاءاتها

دوال يمكن تكرار أسماءها

عمومًا، لا يمكن تكرار أسماء الدوال المعرَّفة في حزمة الشيفرة نفسها. لكن هناك استثناءان.

  1. الاستثناء الأول هو أنه يمكن لكل حزمة شيفرة تعريف عدة دوال بالاسم نفسه init والنوع نفسه func ().
  2. والاستثناء الآخر هو أنه يمكن تعريف عدة دوال بأسماء هي المعرّف الفارغ _، وفي هذه الحالات لا يمكن أبدًا استدعاء الدوال المعرَّفة.

بعض استدعاءات الدوال تُقيَّم في وقت التصريف

تُقيَّم معظم استدعاءات الدوال في وقت التشغيل. لكن استدعاءات دوال حزمة unsafe القياسية تُقيَّم دائمًا في وقت التصريف. واستدعاءات بعض الدوال المدمجة الأخرى، مثل len وcap، قد تُقيَّم إما في وقت التصريف أو في وقت التشغيل، حسب المعطيات المُمرَّرة. ويمكن إسناد نتائج استدعاءات الدوال المُقيَّمة في وقت التصريف إلى ثوابت.

جميع معطيات الدوال تُمرَّر بالنسخ

لنكرّرها مرة أخرى: مثل جميع إسنادات القيم في Go، تُمرَّر جميع معطيات الدوال في Go بالنسخ. وعند نسخ قيمة، يُنسخ جزءها المباشر فقط (أي نسخ سطحي).

تعريفات الدوال بدون أجسام

يمكننا تنفيذ دالة في تجميع Go. وتُخزَّن ملفات مصدر تجميع Go في ملفات *.a. ولا تزال الدالة المنفَّذة بتجميع Go بحاجة إلى تعريف في ملف *.go، لكن لا يُحتاج إلا إلى وجود النموذج الأولي للدالة. ويجب حذف جزء الجسم من تعريف الدالة في ملف *.go.

بعض الدوال ذات النتائج لا يُشترط أن تُرجِع

إذا كانت للدالة نتائج إرجاع، فيجب أن تكون الجملة الأخيرة في جسم تعريفها جملة مُنهية. وبالإضافة إلى الجملة المنيّة return، هناك أنواع أخرى من الجمل المنيّة. لذا لا يُشترط أن يحتوي جسم الدالة على جملة return. على سبيل المثال،


func fa() int {
	a:
	goto a
}

func fb() bool {
	for{}
}

يمكن إهمال نتائج استدعاءات الدوال المخصّصة، بخلاف استدعاءات بعض الدوال المدمجة

يمكن إهمال جميع نتائج الإرجاع لاستدعاء دالة مخصّصة معًا. أما نتائج الإرجاع لاستدعاءات الدوال المدمجة، باستثناء recover وcopy، فلا يمكن إهمالها، رغم أنه يمكن تجاهلها بإسنادها إلى بعض المعرّفات الفارغة. واستدعاءات الدوال التي لا يمكن إهمال نتائجها لا يمكن استخدامها كاستدعاءات دوال مؤجَّلة أو استدعاءات goroutine.

استخدام استدعاءات الدوال كتعبيرات

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

إذا لم تُهمَل نتائج الإرجاع لاستدعاء دالة متعددة النتائج، فلا يمكن استخدام الاستدعاء كتعبير متعدد القيم إلا في حالتين.

  1. يمكن استخدام الاستدعاء في إسناد كقيم مصدر. لكن لا يمكن خلط الاستدعاء مع قيم مصدر أخرى في الإسناد.
  2. يمكن تضمين الاستدعاء في استدعاء دالة آخر كمعطيات. لكن لا يمكن خلط الاستدعاء مع معطيات أخرى.

مثال:


package main

func HalfAndNegative(n int) (int, int) {
	return n/2, -n
}

func AddSub(a, b int) (int, int) {
	return a+b, a-b
}

func Dummy(values ...int) {}

func main() {
	// تُصرَّف هذه الأسطر دون مشكلة.
	AddSub(HalfAndNegative(6))
	AddSub(AddSub(AddSub(7, 5)))
	AddSub(AddSub(HalfAndNegative(6)))
	Dummy(HalfAndNegative(6))
	_, _ = AddSub(7, 5)

	// تفشل الأسطر التالية في التصريف.
	/*
	_, _, _ = 6, AddSub(7, 5)
	Dummy(AddSub(7, 5), 9)
	Dummy(AddSub(7, 5), HalfAndNegative(6))
	*/
}

لاحظ أن بعض الدوال المدمجة تخالف عمومية القواعد الموصوفة أعلاه، بالنسبة إلى مصرِّف Go القياسي.

قيم الدوال

كما ذُكر أعلاه، أنواع الدوال أحد أنواع الأنواع في Go. وتُسمّى قيمة نوع الدالة قيمة دالة. وتُمثَّل القيم الصفرية لأنواع الدوال بالمعرّف nil المُعرَّف مسبقًا.

عندما نعرّف دالة مخصّصة، فإننا نعرّف في الواقع أيضًا قيمة دالة غير قابلة للتعديل. وتُعرَّف قيمة الدالة باسم الدالة. ويُمثَّل نوع قيمة الدالة كقيمة حرفية بحذف اسم الدالة من القيمة الحرفية للنموذج الأولي للدالة.

لاحظ أنه لا يمكن استخدام الدوال المدمجة كقيم. ولا يمكن أيضًا استخدام دوال init كقيم.

يمكن استدعاء أي قيمة دالة تمامًا كدالة معرَّفة. ومن الخطأ الفادح استدعاء دالة nil لبدء goroutine جديدة. والخطأ الفادح غير قابل للاستعادة وسيجعل البرنامج كله ينهار. أما في الحالات الأخرى، فسيُنتج استدعاء قيم الدوال nil حالات panic قابلة للاستعادة، بما في ذلك استدعاءات الدوال المؤجَّلة.

نعلم من مقالة أجزاء القيمة أن قيم الدوال غير nil قيم متعددة الأجزاء. وبعد إسناد قيمة دالة إلى أخرى، تتشارك الدالتان أجزاءها الكامنة نفسها. بعبارة أخرى، تمثّل الدالتان كائن الدالة الداخلي نفسه. وتأثير استدعاء الدالتين واحد.

مثال:


package main

import "fmt"

func Double(n int) int {
	return n + n
}

func Apply(n int, f func(int) int) int {
	return f(n) // نوع f هو "func(int) int"
}

func main() {
	fmt.Printf("%T\n", Double) // func(int) int
	// Double = nil // خطأ: Double غير قابل للتعديل.

	var f func(n int) int // القيمة الافتراضية هي nil.
	f = Double
	g := Apply // دع المصرِّف يستنتج نوع g
	fmt.Printf("%T\n", g) // func(int, func(int) int) int

	fmt.Println(f(9))         // 18
	fmt.Println(g(6, Double)) // 12
	fmt.Println(Apply(6, f))  // 12
}

في المثال أعلاه، g(6, Double) وApply(6, f) متكافئان.

عمليًا، كثيرًا ما نُسند دوال مجهولة إلى متغيرات دوال، حتى نتمكن من استدعاء الدوال المجهولة عدة مرات.


package main

import "fmt"

func main() {
	// تُرجِع هذه الدالة دالة (إغلاقًا).
	isMultipleOfX := func (x int) func(int) bool {
		return func(n int) bool {
			return n%x == 0
		}
	}

	var isMultipleOf3 = isMultipleOfX(3)
	var isMultipleOf5 = isMultipleOfX(5)
	fmt.Println(isMultipleOf3(6))  // true
	fmt.Println(isMultipleOf3(8))  // false
	fmt.Println(isMultipleOf5(10)) // true
	fmt.Println(isMultipleOf5(12)) // false

	isMultipleOf15 := func(n int) bool {
		return isMultipleOf3(n) && isMultipleOf5(n)
	}
	fmt.Println(isMultipleOf15(32)) // false
	fmt.Println(isMultipleOf15(60)) // true
}

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

التنقل عبر الدوال (منذ Go 1.23)

منذ Go 1.23، يمكن التنقل بقيم أنواع الدوال التي تكون أنواعها الكامنة أيًا مما يلي باستخدام حلقات for-range.


// K وV نوعان ما.

func(yield func() bool)

func(yield func(V) bool)

func(yield func(K, V) bool)

وتُسمّى هذه القيم المكرِّرات الدافعة (push iterators)، وغالبًا تُختصر إلى المكرِّرات.

عند استخدام حلقة for-range تعمل على قيمة دالة مكرِّر كهذه، ستُستدعى قيمة دالة المكرِّر (مرة واحدة) بدالة رد نداء yield مُنشأة ضمنيًا. وتُرجِع دالة رد النداء yield نتيجة bool. وعندما تُرجِع false، فلا ينبغي لدالة المكرِّر أن تستدعي الدالة yield بعد ذلك.

فيما يلي بعض الأمثلة التي تستخدم مثل هذه المكرِّرات.


package main

import "fmt"

func Loop3(yield func() bool) {
	if (!yield()) {
		return
	}
	if (!yield()) {
		return
	}
	yield()
}

func OneDigitNumbers(onValue func(int) bool) {
	for i := range 10 {
		if (!onValue(i)) {
			return
		}
	}
}

func SquareLessThan50(onKeyValue func(int, int) bool) {
	for i := range 8 {
		if (!onKeyValue(i, i*i)) {
			return
		}
	}
}

func main() {
	var n = 0
	for range Loop3 {
		fmt.Print(n)
		n++
	}
	fmt.Println()
	// المخرجات: 012

	for i := range OneDigitNumbers {
		fmt.Print(i)
	}
	fmt.Println()
	// المخرجات: 0123456789

	for i, ii := range SquareLessThan50 {
		fmt.Printf("%v:%v ", i, ii)
	}
	fmt.Println()
	// المخرجات: 0:0 1:1 2:4 3:9 4:16 5:25 6:36 7:49
}

بالنسبة إلى الحالات المحددة المعروضة في المثال أعلاه، فإن حلقات for-range مكافئة لاستدعاءات الدوال التالية، على التوالي.


func main() {
	var n = 0
	Loop3(func() bool {
		fmt.Print(n)
		n++
		return true
	})
	fmt.Println()

	OneDigitNumbers(func(i int) bool {
		fmt.Print(i)
		return true
	})
	fmt.Println()

	SquareLessThan50(func(i, ii int) bool {
		fmt.Printf("%v:%v ", i, ii)
		return true
	})
	fmt.Println()
}

لكن، يُرجى ملاحظة أنه في بعض الحالات الأخرى، قد لا تصح هذه المكافأة.

يُرجى قراءة مقالة المدونة الرسمية هذه وهذه المقالة لمعرفة المزيد عن المكرِّرات في Go.