طرق Go
الفرق بين الطرق والدوال هو أن الطرق لها مُستقبِل، بينما الدوال لا، وفقط الأنواع المخصصة يمكن أن يكون لها طرق. لنرى مثالاً أولاً:
type IntSlice []int
func (i IntSlice) Get(index int) int {
return i[index]
}
func (i IntSlice) Set(index, val int) {
i[index] = val
}
func (i IntSlice) Len() int {
return len(i)
}أولاً تم تعريف نوع IntSlice، نوعه الأساسي هو []int، ثم تم تعريف ثلاث طرق: Get و Set و Len، شكل الطرق لا يختلف كثيراً عن الدوال، فقط هناك جزء إضافي (i IntSlice). i هو المُستقبِل، و IntSlice هو نوع المُستقبِل، المُستقبِل مشابه لـ this أو self في اللغات الأخرى، لكن في Go يجب تحديده بشكل صريح:
func main() {
var intSlice IntSlice
intSlice = []int{1, 2, 3, 4, 5}
fmt.Println(intSlice.Get(0))
intSlice.Set(0, 2)
fmt.Println(intSlice)
fmt.Println(intSlice.Len())
}استخدام الطرق مشابه لاستدعاء طريقة عضو في فئة: أولاً التعريف، ثم التهيئة، ثم الاستدعاء.
مُستقبِل القيمة
المُستقبِل ينقسم لنوعين: مُستقبِل قيمة ومُستقبِل مؤشر، لنرى مثالاً:
type MyInt int
func (i MyInt) Set(val int) {
i = MyInt(val) // تم التعديل، لكن لن يؤثر على شيء
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)
}بعد تشغيل الكود أعلاه، ستجد أن قيمة myInt لا تزال 1، ولم يتم تعديلها لـ 2. عند استدعاء الطريقة، يتم تمرير قيمة المُستقبِل للطريقة، المُستقبِل في المثال أعلاه هو مُستقبِل قيمة، يمكن اعتباره ببساطة معاملاً شكلياً، وتعديل قيمة المعامل الشكلي لن يؤثر على أي شيء خارج الطريقة، فماذا لو استدعيناها عبر مؤشر؟
func main() {
myInt := MyInt(1)
(&myInt).Set(2)
fmt.Println(myInt)
}للأسف، هذا الكود لا يزال لا يمكنه تعديل القيمة الداخلية، لمطابقة نوع المُستقبِل، Go ستقوم بفك المرجع، وتفسره كـ (*(&myInt)).Set(2).
مُستقبِل المؤشر
بتعديل بسيط، يمكن تعديل قيمة myInt بشكل طبيعي:
type MyInt int
func (i *MyInt) Set(val int) {
*i = MyInt(val)
}
func main() {
myInt := MyInt(1)
myInt.Set(2)
fmt.Println(myInt)
}المُستقبِل الآن هو مُستقبِل مؤشر، ورغم أن myInt نوع قيمة، عند استدعاء طريقة مُستقبِل المؤشر عبر نوع القيمة، ستفسرها Go كـ (&myint).Set(2). لذا عندما يكون مُستقبِل الطريقة مؤشراً، سواء كان المستدعي مؤشراً أم لا، يمكن تعديل القيمة الداخلية.
في عملية تمرير معاملات الدالة، يتم نسخ القيمة، إذا تم تمرير عدد صحيح، يتم نسخ هذا العدد الصحيح، وإذا كانت شريحة، يتم نسخ هذه الشريحة، لكن إذا كان مؤشراً، يتم نسخ المؤشر فقط، ومن الواضح أن تمرير مؤشر يستهلك موارد أقل من تمرير شريحة، والمُستقبِل ليس استثناءً، مُستقبِل القيمة ومُستقبِل المؤشر نفس المبدأ. في معظم الحالات، يُنصح باستخدام مُستقبِل المؤشر، لكن لا يجب خلط الاستخدام، إما استخدام جميعها أو عدم استخدام أي منها، لنرى مثالاً:
TIP
يجب أولاً فهم الواجهات
type Animal interface {
Run()
}
type Dog struct {
}
func (d *Dog) Run() {
fmt.Println("Run")
}
func main() {
var an Animal
an = Dog{}
// an = &Dog{} الطريقة الصحيحة
an.Run()
}هذا الكود لن يتم ترجمته، المُترجم سيُظهر الخطأ التالي:
cannot use Dog{} (value of type Dog) as type Animal in assignment:
Dog does not implement Animal (Run method has pointer receiver)الترجمة: لا يمكن استخدام Dog{} لتهيئة متغير من نوع Animal، لأن Dog لا تُنفذ Animal، هناك حلان: الأول تغيير مُستقبِل المؤشر لمُستقبِل قيمة، والثاني تغيير Dog{} لـ &Dog{}، لنشرح كل منهما:
type Dog struct {
}
func (d Dog) Run() { // تم تغييره لمُستقبِل قيمة
fmt.Println("Run")
}
func main() { // يمكن تشغيله بشكل طبيعي
var an Animal
an = Dog{}
// an = &Dog{} يمكن أيضاً
an.Run()
}في الكود الأصلي، مُستقبِل طريقة Run هو *Dog، وبطبيعة الحال من يُنفذ واجهة Animal هو مؤشر Dog وليس هيكل Dog، فهذان نوعان مختلفان، لذا يرى المُترجم أن Dog{} ليس تنفيذاً لـ Animal، وبالتالي لا يمكن إسناده للمتغير an، لذا الحل الثاني هو إسناد مؤشر Dog للمتغير an. لكن عند استخدام مُستقبِل قيمة، لا يزال مؤشر Dog يمكن إسناده بشكل طبيعي لـ animal، لأن Go ستقوم بفك المرجع للمؤشر في الحالات المناسبة، لأنه من خلال المؤشر يمكن العثور على هيكل Dog، لكن العكس غير صحيح، لا يمكن العثور على مؤشر Dog من هيكل Dog. إذا تم خلط مُستقبِل القيمة ومُستقبِل المؤشر داخل الهيكل فقط فلا مشكلة، لكن عند استخدامه مع الواجهات، ستظهر أخطاء، والأفضل إما استخدام مُستقبِل قيمة دائماً أو مُستقبِل مؤشر دائماً، لتكوين مواصفات جيدة، وتقليل عبء الصيانة اللاحقة.
هناك حالة أخرى، وهي عندما يكون مُستقبِل القيمة قابلاً للعنونة، ستقوم Go تلقائياً بإدراج عامل المؤشر للاستدعاء، مثلاً الشريحة قابلة للعنونة، ولا يزال يمكن تعديل قيمها الداخلية عبر مُستقبِل قيمة. مثلاً الكود التالي:
type Slice []int
func (s Slice) Set(i int, v int) {
s[i] = v
}
func main() {
s := make(Slice, 1)
s.Set(0, 1)
fmt.Println(s)
}المخرجات:
[1]لكن هذا سيؤدي لمشكلة أخرى، إذا أضفنا عناصر، فالأمر مختلف. انظر المثال التالي:
type Slice []int
func (s Slice) Set(i int, v int) {
s[i] = v
}
func (s Slice) Append(a int) {
s = append(s, a)
}
func main() {
s := make(Slice, 1, 2)
s.Set(0, 1)
s.Append(2)
fmt.Println(s)
}[1]مخرجاته لا تزال كما كانت، دالة append لها قيمة مُرجعة، بعد إضافة عنصر للشريحة يجب الكتابة فوق الشريحة الأصلية، خاصة بعد التوسع، تعديل مُستقبِل القيمة داخل الطريقة لن يؤثر على أي شيء، وهذا ما أدى للنتيجة في المثال، بتغييره لمُستقبِل مؤشر يصبح صحيحاً:
type Slice []int
func (s *Slice) Set(i int, v int) {
(*s)[i] = v
}
func (s *Slice) Append(a int) {
*s = append(*s, a)
}
func main() {
s := make(Slice, 1, 2)
s.Set(0, 1)
s.Append(2)
fmt.Println(s)
}المخرجات:
[1 2]