یک مهندس نرمافزار در دیوار.
پوینتر ها در گو | آشنایی و درک بهتر آنها
اگر قلق و پشت صحنه پوینترها رو ندونیم به احتمال زیاد توی پیاده سازی هامون دردسر های زیادی خواهیم داشت و خیلی جاها بدون اینکه متوجه باشیم چطور کدمون کار میکنه (یا بدتر با آزمون و خطای چند حالت و رسیدن به حالتی که کار میکنه!) کد میزنیم و بعدا میتونه دردسر داشته باشه برامون.
پوینتر ها در گولنگ چی هستن؟
قبل از همه یک تعریف انگلیسی در موردش بخونیم:
Pointers in Go programming language is a variable which is used to store the memory address of another variable.
این تعریف داره میگه پوینترها در گو متغیرهایی هستند که آدرس مموری یک متغیر دیگه رو توی خودشون نگه میدارند. این تعریف یک نکته مهم داره. پوینتر ها یک متغیر هستند. یعنی پوینتر ها چیز خیلی پیچیده ایی نیستند، یک متغیر هستند که جای مثلا استرینگ و اینتیجر توی خودشون آدرس نگه میدارند. خود پوینترها توی مموری مثل بقیه متغیرها یک آدرس دارن و میتونیم با de-reference کردن یک پوینتر به متغیری که به اون اشاره میکنه دسترسی داشته باشیم. بیاید یک مثال بزنیم، یک تایپ تغریف کردم که جلوتر باهاش کلی کار داریم:
type T struct {
Name string
}
برای مثال میتونیم به روش زیر از این تایپ یک پوینتر بسازیم:
p := &T{
Name: "Mohammad",
}
علامت & داره میگه که به من مقدار متغیری که ساختم رو برنگردون، در عوض آدرسش توی مموری رو برگردون. پس ما یک پوینتر داریم به اسم P که value اون آدرس یک متغیر توی مموری هست.
fmt.Printf("p is pointing to: %p \n", p) // 0xc00010a040
با استفاده از %p هم میتونیم یک آدرس رو نمایش بدیم. همونطوری که میبینید توی کد بالا آدرسی که p داره بهش اشاره میکنه رو مشاهده میکنیم. اما همونطوری که گفتم خود پوینترها هم یک نوع متغیر هستند و توی مموری یک آدرس دارن. چطور به اون ها دسترسی داشته باشیم؟
fmt.Printf("p address: %p \n", &p) // 0xc000128018
این کد خود آدرس p رو برمیگردونه. همونطوری که میبینید یک آدرس کاملا متفاوت از آدرس متغیر قبلی داریم. حالا اگر بخوایم از یک پوینتر یک کپی بسازیم چه اتفاقی میوفته؟
newP := p
fmt.Printf("newP is pointing to: %p \n", newP) // 0xc00010a040
fmt.Printf("newP address: %p \n", &newP) // 0xc000128028
همونطوری که توی کدبالا میبینید آدرسی که متغیر جدیدمون بهش اشاره میکنه با آدرس متغیر قبلی یکسانه. اما چون یک متغیر جدید ساختیم آدرس خود پوینتر با آدرس های قبلی فرق داره.
وقتی یک تابع پوینتر میگیره چنین اتفاقی میوفته. ما آدرس رو داریم میفرستیم و باید تغییراتی که میخوایم رو روی اون آدرس تغییر بدیم. یعنی روی value ورودی تابعمون نه روی آدرس متغیری که به عنوان ورودی تعریف کردیم.
چطور پوینتر بسازیم؟
با سه روش زیر شما یک متغیر پوینتر میسازید:
var p1 *T
p2 := new(T)
p3 := &T{}
روش دوم و سوم تقریبا یک کار هستند. داره میگه اول یک متغیر توی حافظه بساز، حالا آدرسش رو بریز توی پوینتری که ما گفتیم. اما روش اول صرفا میگه یک پوینتر بساز. بیاید ببینیم این پوینترها به چه آدرسی توی حافظه اشاره میکنند:
fmt.Printf("p1 address: %p\n", p1) // 0X0
fmt.Printf("p2 address: %p\n", p2) // 0xc000010200
fmt.Printf("p3 address: %p\n", p3) // 0xc000010210
نکته مهمی که باید توجه کنیم آدرسیه که p1 داره بهش اشاره میکنه. به p1 میگیم nil pointer، یعنی یک پوینتر که به متغیر خاصی توی حافظه اشاره نمیکنه و خالیه. چرا باید حواسمون بهش باشه؟
ما راحت میتونیم متغیر های p2 و p3 رو با استفاده از *p دی رفرنس کنیم و به مقداری که بهش اشاره میکنن دسترسی داشته باشیم. اما گفتیم p1 به هیچی اشاره نمیکنه، اگه dereference اش کنیم چه اتفاقی میوفته؟
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49a812]
پنیک میکنیم و کرش میکنیم! همیشه باید مراقب این nil pointer ها باشیم و قبل از دی رفرنس کردن یک پوینتر نیل بودنش رو چک کنیم.
کار با اسلایس و پوینترها
اسلایس چیه؟ یک پوینتر به یک آرایه هست که علاوه بر امکانات آرایه یک شعوری داره که باعث میشه اگر داشتیم از ظرفیت آرایه بیشتر میشدیم یک آرایه جدید میسازه و مقادیر قبلی رو اونجا کپی میکنه و پوینترش دیگه به آرایه جدید اشاره میکنه.
برای ساختن یک اسلایس از پوینترها میتونیم از این روش ها استفاده کنیم:
var a1 []*T
a2 := make([]*T, 10)
a3 := []*T{
{Name: ""},
}
خب خب رسیدیم جای جالب. همونطوری که گفتیم اسلایس در اصل یک پوینتر هست. پس اما پوینتر a1 به کجا اشاره میکنه؟ برعکس a2 و a3 که یک آرایه براشون ساخته شده به هیچ جا اشاره نمیکنه:
fmt.Printf("a1 address: %p\n", a1) // 0x0
fmt.Printf("a2 address: %p\n", a2) // 0xc000066050
fmt.Printf("a3 address: %p\n", a3) // 0xc00000e028
اما بازم باید مراقب باشیم! چرا؟ چون توی a2 ما گفتیم یک آرایه از پوینترها برامون بساز. برامون پوینتر رو میسازه ولی پوینترها nil هستد!
fmt.Println(a2) // [<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>]
fmt.Printf("a2[0] address: %p\n", a2[0]) // a2[0] address: 0x0
خب این چطور میتونه خطرناک باشه؟ برای مثال توی a3 ما گفتیم یک آرایه از پوینتر ها بساز ولی برای تک تک پوینتر ها داریم میگیم به کجا اشاره کن و nil نیستند.
func C4() {
a1 := []*T{
{Name: ""},
}
ChangeItem(a1[0])
fmt.Println("Name is", a1[0].Name) // Name is Mohammad
}
func ChangeItem(t *T) {
*t = T{Name: "Mohammad"}
}
اگر این مثال رو اجرا کنیم چون a1[0] ما پوینتری هست که به جایی توی حافظه اشاره میکه و ما داریم اون آدرس رو به تابع میدیم و آدرس توی t هست و با دی رفرنس کردن t به اون متغیر توی حافظه دسترسی داریم. پس میتونیم مقدارش رو تغییر بدیم.
func C5() {
a1 := make([]*T, 10)
ChangeItem(a1[0]) // panic !!
fmt.Println("Name is", a1[0].Name)
}
اما اگر این کد رو اجرا کنیم panic میکنیم. علت اینه ما داریم آدرس 0x0 رو به عنوان ورودی به تابع میدیم و آدرسی که t بهش اشاره میکنه 0x0 هست، پس ما نمیتونیم این پوینتر رو دی-رفرنس کنیم در نتیجه پنیک میخوریم.
a1 := make([]*T, 10)
a1[0] = &T{}
ChangeItem(a1[0])
fmt.Println("Name is", a1[0].Name) // Name is Mohammad
برای حل این مشکل ما میتونیم قبل صدا زدن تابع یک آدرس توی حافظه به پوینتر بدیم و انتظار داشته باشیم اون آدرس تغییر کنه.
البته با این تریک هم میشه یک پوینتر نیل رو مقدار دهی کنیم که من خیلی ازش خوشم نمیاد چون علاوه بر پیچیده کردن و سخت شدن کد امکان بوجود اومدن خطاهای دیگه ایی رو به کد بیس ما اضافه میکنه.
ما میتونیم جای اینکه پوینتر نیل رو بخوایم مقدار دهی کنیم میتونیم آدرس اون پوینتر رو به عنوان پوینترِ یک پوینتر به تابع بدیم و از طریق این پوینترِ پوینتر به خود پوینتر دسترسی داشته باشیم و بگیم که به کجا اشاره کنه.
func main() {
var p *T
ChangeName(&p)
fmt.Println(p.Name)
}
func ChangeName(t **T) {
*t = &T{
Name: "Hello",
}
}
مطلبی دیگر از این انتشارات
همروندی در زبان گو
مطلبی دیگر از این انتشارات
متدها در زبان GoLang
مطلبی دیگر از این انتشارات
آیا می دانستید زبان GO چیست؟