اذهب إلى المحتوى

المؤشّر هو عنوان يشير إلى موقع في الذاكرة، وتُستخدم المؤشرات عادةً للسماح للدوالّ أو هياكل البيانات بالحصول على معلومات عن الذاكرة وتعديلها دون الحاجة إلى نسخ الذاكرة المشار إليها، والمؤشّرات قابلة للاستخدام سواءٌ مع الأنواع الأوليّة (المٌضمّنة) أو الأنواع التي يعرّفها المستخدم.

تتضمن البرامج المكتوبة بلغة جو عادةً دوالًا وتوابعًا Methods نمرر لها بيانات مثل وسطاء، ونحتاج أحيانًا إلى إنشاء نسخة محلية من تلك البيانات بحيث تظل النسخة الأصلية من البيانات دون تغيير. مثلًا، لو كان لديك برنامج يمثّل مصرفًا bank، وتريد أن يُظهر هذا البرنامج التغيرات التي تطرأ على رصيد المستخدم تبعًا للخطة التوفيرية التي يختارها، فهنا قد تحتاج إلى بناء دالة تنجز هذا الأمر من خلال تمرير الرصيد الحالي للمستخدم إضافةً إلى الخطة التي يريدها. لا نريد هنا تغيير الرصيد الأساسي وإنما نريد فقط إظهار التعديل الذي سيطرأ على الرصيد، وبالتالي يجب أن نأخذ نسخةً من رصيد المستخدم مثل وسيط للدالة ونعدّل على هذه النسخة. تسمى هذه النسخة نسخةً محلية، وندعو عملية التمرير هذه "التمرير بالقيمة passing by value" لأننا لانرسل المتغير نفسه وإنما قيمته فقط.

هناك حالات أخرى قد تحتاج فيها إلى تعديل البيانات الأصلية؛ بمعنى آخر قد نحتاج إلى تغيير قيمة المتغير الأصلي مباشرةً من خلال الدالة، فمثلًا، عندما يودع المستخدم رصيدًا إضافيًّا في حسابه، فهنا تحتاج إلى جعل الدالة قادرةً على تعديل قيمة الرصيد الأصلي وليس نسخةً منه (نحن نريد إضافة مال إلى رصيده السابق). ليس ضروريًا هنا تمرير البيانات الفعلية إلى الدالة، إذ يمكنك ببساطة إخبار الدالة بالمكان الذي توجد به البيانات في الذاكرة من خلال "مؤشر" يحمل عنوان البيانات الموجودة في الذاكرة. لا يحمل المؤشر القيمة، وإنما فقط عنوان أو مكان وجود القيمة، وتتمكن الدالة من خلال هذا المؤشر من التعديل على البيانات الأصلية مباشرةً. يسمى هذا "التمرير بالمرجع passing by reference"، لأن قيمة المتغير لا تُمرّر إلى الدالة، بل إلى موقعها فقط.

سننشئ في هذا المقال المؤشرات ونستخدمها لمشاركة الوصول إلى الذاكرة المُخصصة لمتغير ما.

تعريف واستخدام المؤشرات

يوجد عنصرا صيغة مختلفان يختصان باستخدام مؤشّر لمتغيرٍ variable ما، وهما: معامل العنونة Address-of operator وهو "&" الذي يعيد عنوان المتغيّر الذي يوضع أمامه في الذاكرة، ومعامل التحصيل Dereference، وهو "*" الذي يعيد قيمة المتغير الموجود في العنوان المحدّد بواسطة عامله.

ملاحظة: يُستخدم رمز النجمة * أيضًا للتصريح عن مؤشّر لمجرد التوضيح بأنه مؤشر، ولا ينبغي أن تخلط بينه وبين عامل التحصيل الذي يُستخدم للحصول على القيمة الموجودة في عنوان محدّد، فهما شيئان مختلفان مُمثّلان بنفس الرمز.

مثال:

var myPointer *int32 = &someint

صرّحنا هنا عن متغير يُسمّى myPointer يُمثّل مؤشرًا لمتغير من نوع العدد الصحيح int32، وهيأنا المؤشر بعنوان someint، فالمؤشر هنا يحمل عنوان المتغير int32 وليس قيمته.

دعنا نلقي الآن نظرةً على مؤشر لسلسلة، إذ تُصرّح الشيفرة التالية عن متغير يُمثّل سلسلة ومتغير آخر يُمثّل مؤشرًا على تلك السلسلة:

package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)
}

شغّل البرنامج بالأمر التالي:

$ go run main.go

سيطبع البرنامج عند تشغيله قيمة المتغير إضافةً إلى عنوان تخزين المتغير (عنوان المؤشر). عنوان الذاكرة هو سلسلة أرقام مكتوبة بنظام العد السداسي عشري لأسباب عتادية وبرمجية لا تهمنا الآن، وطبعنا القيم هنا للتوضيح فقط. ستلاحظ تغيُّر العنوان المطبوع في كل مرة تُشغّل فيها البرنامج، لأنه يُهيّأ من جديد وتأخذ فيه المتغيرات أماكن غير محددة في الذاكرة؛ فكل برنامج ينشئ مساحته الخاصة من الذاكرة عند تشغيله. إذًا، سيكون خرج الشيفرة السابقة مختلفًا لديك عند تشغيله:

creature = shark
pointer = 0xc0000721e0

عرّفنا المتغير الأول creature من النوع string وهيّأناه بالقيمة shark. أنشأنا أيضًا متغيرًا يُسمّى pointer يُمثّل مؤشرًا على عنوان متغير سلسلة نصية، أي يحمل عنوان متغير نوعه string، وهيّأناه بعنوان السلسلة النصية المُمثّلة بالمتغير creature وذلك من خلال وضع المعامل "&" قبل اسمه.

إذًا، سيحمل pointer عنوان الذاكرة التي يوجد بها creature وليس قيمته. هذا هو السبب وراء الحصول على القيمة 0xc0000721e0 عندما طبعنا قيمة المؤشر، وهو عنوان مكان تخزين متغير creature حاليًا في ذاكرة الحاسب.

يمكنك الوصول إلى قيمة المتغير مباشرةً من خلال نفس المؤشر باستخدام معامل التحصيل "*" كما يلي:

package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)

    fmt.Println("*pointer =", *pointer)
}

ويكون الخرج على النحو التالي:

creature = shark
pointer = 0xc000010200
*pointer = shark

يُمثّل السطر الإضافي المطبوع ماتحدّثنا عنه (الوصول لقيمة المتغير من خلال المؤشر). نسمي ذلك "التحصيل" إشارةً إلى الوصول لقيمة المتغير من خلال عنوانه. يمكننا استخدام هذه الخاصية أيضًا في تعديل قيمة المتغير المُشار إليه:

package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)

    fmt.Println("*pointer =", *pointer)

    *pointer = "jellyfish"
    fmt.Println("*pointer =", *pointer)
}

وسيكون الخرج على النحو التالي:

creature = shark
pointer = 0xc000094040
*pointer = shark
*pointer = jellyfish

لاحظ أننا في السطر "pointer = "jellyfish* وضعنا معامل التحصيل * قبل المؤشر للإشارة إلى أننا نريد تعديل القيمة التي يُشير إلى عنوانها المؤشر. أسندنا القيمة "jellyfish" إلى موقع الذاكرة التي يُِشير لها pointer، وهذا يُكافئ تعديل قيمة المتغير creature. لاحظ أنه عند طباعة القيمة التي يُشير لها المؤشر سنحصل على القيمة الجديدة.

كما ذكرنا؛ فهذا يُكافئ تعديل قيمة المتغير creature، وبالتالي لو حاولنا طباعة قيمة المتغير creature سنحصل على القيمة "jellyfish" لأننا نُعدّل على الموقع الذاكري نفسه. سنضيف الآن سطرًا يطبع قيمة المتغير creature إلى الشيفرة السابقة:

package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)

    fmt.Println("*pointer =", *pointer)

    *pointer = "jellyfish"
    fmt.Println("*pointer =", *pointer)

    fmt.Println("creature =", creature)
}

وسيكون الخرج كما يلي:

creature = shark
pointer = 0xc000010200
*pointer = shark
*pointer = jellyfish
creature = jellyfish

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

ضع في الحسبان أننا نطبع قيمة المؤشر لتوضيح أنه مؤشر، فلن تستخدم عمليًا قيمة المؤشر إلا للإشارة إلى القيمة الأساسية لاستردادها أو تعديلها.

مستقبلات مؤشرات الدوال

عند كتابة دالة، يمكنك تعريف بعض الوسطاء لكي تُمررهم لها إما بالقيمة أو بالمرجع؛ فعندما تُمرر وسيطًا ما بالقيمة، فهذا يعني أنك تُرسل نسخةً مُستقلة من قيمة هذا الوسيط إلى الدالة، وبالتالي فإن أي تغيير يحدث لهذه النسخة لن يؤثر على النسخة الأساسية من البيانات، لأن كل التعديلات ستجري على نُسخة من البيانات؛ أما عندما تُمرر وسيطًا بالمرجع، فهذا يعني أنك تُرسل مؤشرًا يحمل عنوان ذلك الوسيط -أي مكان تواجد البيانات في الذاكرة- إلى الدالة، وبالتالي أصبح لديك القدرة على الوصول إلى البيانات الأصلية من داخل الدالة والتعديل عليها مباشرةً.

يمكنك الاطلاع على المقالة التالية إذا أردت معرفة المزيد عن الدوال وطرق استدعائها في لغة جو.

يمكنك طبعًا تمرير أي وسيط بالطريقة التي تُريدها (بالقيمة أو بالمرجع)، فهذا يعتمد على ما تحتاجه؛ فإذا كنت تريد أن تُعدّل الدالة على البيانات الأصلية نُمرر الوسيط بالمرجع وإلا بالقيمة.

لمعرفة الفرق بدقة، دعنا أولًا نلقي نظرة على دالة تمرّر وسيطًا بالقيمة:

package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

ويكون الخرج على النحو التالي:

1) {Species:shark}
2) {Species:jellyfish}
3) {Species:shark}

أنشأنا بدايةً نوع بيانات مخصص أسميناه Creature، يحتوي على حقل واحد يُسمى Species من نوع سلسلة نصية string، وأنشأنا داخل الدالة الرئيسية main متغير من النوع Creature اسمه creature وأسندنا السلسلة shark إلى الحقل Species. بعد ذلك طبعنا المتغير creature لإظهار القيمة التي يتضمنها في الوقت الحالي، ثم مرّرنا المتغير creature (تمرير بالقيمة أي نُسخة) إلى الدالة changeCreature والتي بدورها تطبع قيمة المتغير المُمرر لها بعد إسناد السلسلة "jellyfish" إلى الحقل Species (هنا نطبعه من داخل الدالة أي محليًّا). بعد ذلك طبعنا قيمة المتغير creature مرةً أخرى (خارج الدالة السابقة).

لاحظ أنه يوجد لدينا ثلاث تعليمات طباعة؛ جرى السطر الأول والثالث من الخرج ضمن نطاق الدالة main بينما كان السطر الثاني ضمن نطاق الدالة changeCreature. لاحظ أيضًا أنه في البداية كانت قيمة المتغير creature هي "shark" وبالتالي عند تنفيذ تعليمة الطباعة الأولى سيطبع:

(1) {Species:shark}

أما تعليمة الطباعة في السطر الثاني والموجودة ضمن نطاق الدالة changeCreature، فسنلاحظ أنها ستطبع القيمة:

(2) {Species:jellyfish}

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

سنأخذ الآن نفس المثال، لكن سنغيّر عملية التمرير إلى الدالة changeCreature لتصبح تمرير بالمرجع، وذلك من خلال تغيير النوع من creature إلى مؤشر باستخدام المعامل "*"، فبدلًا من تمرير creature، سنمرّر الآن مؤشرًا إلى creature أو creature*. كان creature في المثال السابق من النوع struct ويحتوي قيمة الحقل Species وهي "shark"، أما creature* فهو مؤشر وليس struct، وبالتالي قيمته هي موقع الذاكرة وهذا ما مرّرناه إلى الدالة ()changeCreature. لاحظ أننا نضع المعامل "&" عند تمرير المتغير creature إلى الدالة.

package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    changeCreature(&creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

ستحصل عند تنفيذ الشيفرة السابقة على الخرج التالي:

1) {Species:shark}
2) &{Species:jellyfish}
3) {Species:jellyfish}

قد تبدو الأمور واضحة الآن، فعندما مرّرنا المتغير creature إلى الدالة changeCreature، كان التمرير بالمرجع، وبالتالي أي تغيير يطرأ على المتغير creature (وهو تغيير قيمة الحقل Species إلى "jellyfish") داخل هذه الدالة، سيكون مُطبّقًا على المتغير الأصلي نفسه الموجود ضمن الدالة main لأننا نُعدّل على نفس الموقع في الذاكرة، وبالتالي ستكون قيمة الخرج لتعليمات الطباعة 2 و 3 مُتطابقة.

قد لا يكون لدينا في بعض الأحيان قيمة مُعرّفة للمؤشر، وهذا قد يحدث لأسباب كثيرة منها ما هو متوقع ومنها لا، وبالتالي قد يسبب لك حالات هلع panic في البرنامج. دعنا نلقي نظرةً على كيفية حدوث ذلك وكيفية التخطيط لتلك المشكلة المحتملة.

التأشير إلى اللاشيء Nil

القيمة الافتراضية لجميع المتغيرات في لغة جو هي الصفر، وهذا الكلام ينطبق أيضًا على المؤشرات. لدى التصريح عن مؤشر بنوعٍ ما ولكن دون أي قيمة مُسندة، ستكون القيمة الصفرية الافتراضية هي nil. الصفر هنا مفهوم متعلق بالنوع، أي أنه في حالة الأعداد الصحيحة هو العدد 0، وفي حالة السلاسل النصية هو السلسلة الفارغة ""، وأخيرًا في حالة المؤشرات هو القيمة nil إشارةً إلى الحالة الافتراضية لقيمة أي مؤشر.

سنُعدّل في البرنامج التالي على البرنامج السابق، بحيث نعرّف مؤشرًا متغيرًا creature من النوع Creature، لكن دون استنساخ للنسخة الحقيقية من Creature ودون إسناد عنوانها إلى المؤشر؛ أي أن قيمة المؤشر هي nil، ولن نستطيع الرجوع إلى أي من الحقول أو التوابع المُعرّفة في النوع Creature. لنرى ماذا سيحدث:

package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature *Creature

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

سيكون الخرج على النحو التالي:

1) <nil>
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86]

goroutine 1 [running]:
main.changeCreature(0x0)
        /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26
    main.main()
            /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98
        exit status 2

نلاحظ عند تشغيل البرنامج أن تعليمة الطباعة الأولى 1 نجحت وطبعت قيمة المتغير creature وهي <nil>، لكن عندما وصلنا إلى استدعاء الدالة changeCreature ومحاولة ضبط قيمة الحقل Species، ظهرت حالة هلع في البرنامج نظرًا لعدم إنشاء نسخة من هذا المتغيّر، وأدى هذا إلى محاولة الوصول إلى موقع ذاكري غير موجود أصلًا أو غير مُحدد.

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

نتحقق عادةً من قيمة المؤشر في أي دالة تستقبل مؤشرًا مثل وسيط لها كما يلي:

if someVariable == nil {
    // هنا يمكن أن نطبع أي رسالة تُشير إلى هذه الحالة أو أن نخرج من الدالة
}

يمكنك بذلك التحقق مما إذا كان الوسيط يحمل قيمةً صفريةً أم لا. عند تمرير قيمة صفرية (هنا نقصد nil) قد ترغب في الخروج من الدالة باستخدام تعليمة return أو إعادة رسالة خطأ تخبر المستخدم أن الوسيط الممرّر إلى الدالة أو التابع غير صالح.

تتحقق الشيفرة التالية من وجود قيمة صفرية للمؤشر:

package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature *Creature

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    if creature == nil {
        fmt.Println("creature is nil")
        return
    }

    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

أضفنا إلى الدالة changeCreature تعليمات لفحص قيمة الوسيط creature فيما إذا كانت صفرية أم لا؛ ففي حال كانت صفرية نطبع "creature is nil" ونخرج من الدالة من خلال تعليمة return، وإلا نتابع العمل في الدالة ونُعدّل قيمة الحقل Species. سنحصل الآن على المخرجات التالية:

1) <nil>
creature is nil
3) <nil>

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

إذا أنشأنا نسخةً من النوع Creature وأُسندت للمتغير creature، سيتغير الخرج بالتأكيد، لأنه أصبح يُشير إلى موقع ذاكري حقيقي:

package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature *Creature
    creature = &Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    if creature == nil {
        fmt.Println("creature is nil")
        return
    }

    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

سنحصل على الناتج المتوقع التالي:

1) &{Species:shark}
2) &{Species:jellyfish}
3) &{Species:jellyfish}

عندما تتعامل مع المؤشرات، هناك احتمال أن يتعرّض البرنامج لحالة هلع، لذلك يجب عليك التحقق لمعرفة ما إذا كانت قيمة المؤشر صفرية قبل محاولة الوصول إلى أي من الحقول أو التوابع المعرّفة ضمن نوع البيانات الذي يشير إليه.

دعنا نلقي نظرةً على كيفية استخدام المؤشرات مع التوابع.

مستقبلات مؤشرات التوابع

المُستقبل receiver في لغة جو هو الوسيط الذي يُعرّف عند التصريح عن التابع. ألقِ نظرةً على الشيفرة التالية:

type Creature struct {
    Species string
}

func (c Creature) String() string {
    return c.Species
}

المُستقبل في هذا التابع هو c Creature، وهو يُشير إلى أن نسخة المتغير c من النوع Creature وأنك ستستخدمه ليُشير إلى نسخة متغير من هذا النوع.

يختلف أيضًا سلوك التوابع كما هو الحال في الدوال التي يختلف فيها سلوك الدالة تبعًا لطريقة تمرير الوسيط (بالمرجع أو بالقيمة). ينبع الاختلاف الأساسي من أنه إذا صرّحت عن دالة مع مُستقبل "قيمة"، فلن تتمكن من إجراء تغييرات على نسخة هذا النوع التي عُرّف التابع عليه. عمومًا، ستكون هناك أوقات تحتاج فيها أن يكون تابعك قادرًا على تحديث نسخة المتغير الذي تستخدمه، ولإجراء هكذا تحديثات ينبغي عليك جعل المستقبل "مؤشرًا".

دعنا نضيف التابع Reset للنوع Creature، الذي يسند سلسلةً نصيةً فارغة إلى الحقل Species.

package main

import "fmt"

type Creature struct {
    Species string
}

func (c Creature) Reset() {
    c.Species = ""
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    creature.Reset()
    fmt.Printf("2) %+v\n", creature)
}

إذا شغّلت البرنامج ستحصل على الخرج:

1) {Species:shark}
2) {Species:shark}

لاحظ أنه على الرغم من ضبطنا قيمة الحقل Species على السلسلة الفارغة في التابع Reset، إلا أننا عندما طبعنا المتغير creature في الدالة main حصلنا على "shark". السبب في عدم انتقال التغيير هو استخدامنا مُستقبل قيمة في تعريف التابع Reset، وبالتالي سيكون لهذا التابع إمكانية التعديل فقط على نسخة المتغير creature وليس المتغير الأصلي. بالتالي، إذا أردنا تحديث هذه القيمة؛ أي التعديل على النسخة الأصلية للمتغير، فيجب علينا تعريف مُستقبل مؤشر.

package main

import "fmt"

type Creature struct {
    Species string
}

func (c *Creature) Reset() {
    c.Species = ""
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    creature.Reset()
    fmt.Printf("2) %+v\n", creature)
}

لاحظ أننا أضفنا المعامل "*" أمام النوع Creature عندما صرّحنا عن التابع Reset، وهذا يعني أن الوسيط الذي نُمرره أصبح مؤشرًا، وبالتالي أصبحت كل التعديلات التي نُجريها من خلاله مُطبّقةً على المتغير الأصلي.

1) {Species:shark}
2) {Species:}

لاحظ أن التابع Reset عدّل قيمة الحقل Species كما توقعنا، وهذا مُماثل لفكرة التمرير بالمرجع أو القيمة في الدوال.

خاتمة

تؤثر طريقة تمرير الوسطاء (بالمرجع أو القيمة) إلى التوابع أو الدوال على آلية الوصول إلى المتغير المُمرّر؛ ففي حالة التمرير بالمرجع يكون التعديل مباشرةً على المتغير الأصلي، أما في الحالة الثانية فيكون على نسخة من المتغير. الآن بعد أن تعرفت على المؤشرات، أصبح بإمكانك التعرف على استخدامها مع الواجهات أيضًا.

ترجمة -وبتصرف- للمقال Understanding Pointers in Go لصاحبه Gopher Guides.


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...