Go هم مانند بسیاری از زبانهای دیگر، «اشارهگر» دارد. اگر تازه با اشارهگرها آشنا شدهاید، ممکن است کمی مرموز به نظر برسند، اما وقتی به آنها عادت کنید، کاملاً ساده میشوند. اشارهگرها بخش مهمی از Go هستند، پس وقت بگذارید و آنها را واقعاً درک کنید.
پیش از پرداختن به جزئیات، خوب است کاربرد اشارهگرها را بفهمید. اشارهگرها روشی برای به اشتراک گذاشتن حافظه با بخشهای دیگر برنامهی ما هستند و به دو دلیل عمده مفیدند:
فرض کنید یک متغیر صحیح معمولی به اسم a داریم:
var a int
وقتی متغیری تعریف میکنیم، Go باید جایی در حافظه برای ذخیرهی مقدار آن پیدا کند. این کار تا حد زیادی از دید ما پنهان است؛ وقتی بخواهیم مقدار ذخیرهشده در آن بخش از حافظه را واکشی کنیم، کافی است با اسم متغیر به آن ارجاع دهیم.
برای مثال، وقتی a + 2 را مینویسیم، در واقع مقدار ذخیرهشده در حافظهی مرتبط با متغیر a را واکشی میکنیم و ۲ به آن اضافه میکنیم.
به همین ترتیب، وقتی بخواهیم مقدار موجود در بخش حافظهی a را تغییر دهیم، میتوانیم با اسم متغیر یک انتساب انجام دهیم:
a = 3
بخش حافظهای که با a مرتبط است، اکنون مقدار 3 را ذخیره میکند.
متغیرها به ما اجازه میدهند به مقادیر موجود در حافظه ارجاع دهیم، اما گاهی دانستن نشانی حافظهای که متغیر به آن اشاره میکند مفید است. اشارهگرها نشانیهای حافظهی این مقادیر را نگه میدارند. متغیری با نوع اشارهگر را با گذاشتن یک ستاره پیش از نوع پایه تعریف میکنید:
var p *int // 'p' contains the memory address of an integer
اینجا متغیری به اسم p از نوع «اشارهگر به int» (*int) تعریف میکنیم.
یعنی p نشانی حافظهی یک عدد صحیح را نگه میدارد.
«مقدار صفر» اشارهگرها nil است، چون اشارهگر nil هیچ نشانی حافظهای در خود ندارد.
برای یافتن نشانی حافظهی مقدار یک متغیر، میتوانیم از عملگر & استفاده کنیم.
برای مثال، اگر بخواهیم نشانی حافظهی متغیر a را پیدا کنیم و در اشارهگر p ذخیره کنیم، میتوانیم این کار را انجام دهیم:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
وقتی اشارهگری داریم، ممکن است بخواهیم مقدار ذخیرهشده در نشانی حافظهای را که اشارهگر نمایندگی میکند بدانیم.
این کار را میتوانیم با عملگر * انجام دهیم:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
عملیات *p مقداری را که در نشانی حافظهی ذخیرهشده در p قرار دارد واکشی میکند.
به این عملیات اغلب «مرجعزدایی» میگویند.
میتوانیم از عملگر مرجعزدایی استفاده کنیم تا مقدار تازهای به نشانی حافظهای که اشارهگر به آن ارجاع میدهد اختصاص دهیم:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
انتساب به *pa مقداری را که در نشانی حافظهای که pa نگه میدارد تغییر میدهد.
چون pa نشانی حافظهی a را در خود دارد، با انتساب به *pa در واقع مقدار a را تغییر میدهیم!
اما یک نکتهی احتیاط: پیش از مرجعزدایی، همیشه بررسی کنید که اشارهگر nil نباشد.
مرجعزدایی از یک اشارهگر nil باعث میشود برنامه در زمان اجرا از کار بیفتد!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
structهاتا اینجا فقط اشارهگرها به مقادیر اولیه را دیدهایم.
میتوانیم برای structها هم اشارهگر بسازیم:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
میتوانستیم یک Person تازه هم بسازیم و بلافاصله اشارهگری به آن ذخیره کنیم:
var p *Person
p = &Person{Name: "Peter", Age: 22}
وقتی اشارهگری به یک struct داریم، لازم نیست پیش از دسترسی به یکی از فیلدها اشارهگر را مرجعزدایی کنیم:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
اسلایسها و مپها نوعهای ویژهای هستند، چون در پیادهسازی خود از قبل اشارهگر دارند. یعنی در بیشتر موارد لازم نیست برای این نوعها اشارهگر بسازیم تا نشانی حافظهی مقادیرشان را به اشتراک بگذاریم. تصور کنید تابعی داریم که مقدار یک کلید را در یک مپ یک واحد افزایش میدهد:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
اگر یک مپ بسازیم و این تابع را فراخوانی کنیم، تغییراتی که تابع در مپ ایجاد کرده پس از پایان تابع باقی میمانند. این رفتار مشابه چیزی است که با استفاده از اشارهگر به دست میآوریم، اما توجه کنید که در این مثال از هیچ ارجاعدهی یا مرجعزدایی و هیچ بخشی از نحوهی نگارش اشارهگر استفاده نمیکنیم:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
همین موضوع وقتی یک عنصر موجود در یک اسلایس را تغییر میدهیم هم صدق میکند.
اما کارهایی که یک اسلایس تازه برمیگردانند، مانند append، حالت ویژهای دارند و ممکن است اسلایس بیرون از تابع را تغییر ندهند.
این به نحوهی کار داخلی اسلایسها مربوط است، اما در این تمرین به جزئیات آن نمیپردازیم، چون موضوعی پیشرفتهتر است.
اگر واقعاً کنجکاو هستید، میتوانید در وبلاگ Go: سازوکارهای append بیشتر بخوانید.
یک مدرسهی محلی نزدیک شما یک انجمن دانشآموزی بسیار فعال دارد. انجمن دانشآموزی توسط یک رئیس اداره میشود و هر ۲ سال یکبار، انتخابات برای انتخاب رئیس جدید برگزار میشود.
در انتخابات امسال، تصمیم گرفته شد که به یک سیستم دیجیتال جدید برای شمارش آرا نیاز است. مدرسه برای ساختن این سیستم جدید به کمک شما نیاز دارد.
یکی از اولین مواردی که سیستم رأیگیری جدید به آن نیاز دارد، یک شمارندهی آرا است. این شمارنده راهی است برای پیگیری آرای یک نامزد مشخص.
تابعی به اسم NewVoteCounter بسازید که تعداد آرای اولیهی یک نامزد را میپذیرد و یک اشارهگر به یک int برمیگرداند که با تعداد آرای اولیهی دادهشده مقداردهی شده است.
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
حالا راهی برای ساختن شمارندههای جدید دارید! اما اکنون متوجه میشوید که سیستم جدید به راهی برای گرفتن تعداد آرا از یک شمارنده هم نیاز خواهد داشت.
تابعی به اسم VoteCount بسازید که یک شمارنده (*int) را به عنوان آرگومان میگیرد و تعداد آرا در شمارنده را برمیگرداند.
اگر شمارنده nil باشد، باید فرض کنید که شمارنده هیچ رأیی ندارد:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
بالاخره وقت شمارش آرا رسیده است! حالا به راهی برای افزایش آرا در یک شمارنده نیاز دارید.
تابعی به اسم IncrementVoteCount بسازید که یک شمارنده (*int) و تعدادی رأی را به عنوان آرگومان میگیرد و شمارنده را به اندازهی آن تعداد آرا افزایش میدهد.
میتوانید فرض کنید اشارهگری که پاس داده میشود هرگز nil نخواهد بود.
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
حالا که همهی آرا شمرده شدهاند، وقت آن است که اعلام نتیجه برای کل مدرسه آماده شود. برای این کار، متوجه میشوید که فقط داشتن شمارنده برای آرا کافی نیست. باید راهی وجود داشته باشد که تعداد آرا را به یک نامزد مشخص مرتبط کند.
تابعی به اسم NewElectionResult بسازید که اسم یک نامزد و تعداد آرای او را دریافت میکند و یک نتیجهی انتخاباتی جدید برمیگرداند.
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
ساختار نتیجهی انتخابات از قبل برایتان ساخته شده است و به این صورت تعریف میشود:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
وقت آن است که رئیس جدید را به مدرسه اعلام کنید!
رئیس در تابلوهای پیام دیجیتال کوچکی که مدرسه دارد اعلام خواهد شد.
پیام باید اسم رئیس جدید و تعداد آرای او را با قالب زیر نشان دهد: <candidate_name> (<votes>).
این نمونهای از چنین پیامی است: "Peter (51)".
تابعی به اسم DisplayResult بسازید که یک *ElectionResult را به عنوان آرگومان دریافت میکند و رشتهای با پیام قابل نمایش برمیگرداند.
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
برای اطمینان از درستی نتایج نهایی، آرا دوباره شمرده شدند. در بازشماری مشخص شد که تعداد آرای برخی از نامزدها یک واحد اختلاف داشت.
تابعی به اسم DecrementVotesOfCandidate بسازید که نتایج نهایی و اسم نامزدی را دریافت میکند که باید تعداد آرای او را کاهش دهید.
نتایج نهایی به شکل یک map[string]int داده میشوند که کلیدها اسم نامزدها و مقادیر کل آرای آنها هستند.
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9