Mohammad Amin Ahmadi
Mohammad Amin Ahmadi
خواندن ۵ دقیقه·۱ سال پیش

چرا در گولنگ nil == nil نیست؟

گولنگ واقعا خفنه. برای همین می‌خوام توی این نوشته راجع به جزئیات دیتا تایپ هاش بگم.

قبل از جواب دادن به سوال اصلی‌مون، بیاید ببینیم nil چیه؟ چه موقع به nil می‌رسیم؟
اولین حدسی که به ذهنمون میاد ممکنه این باشه که خب این nil همون null در زبان‌های مشابه هست؛ تا حدودی درسته، اما یه سری تفاوت‌ها وجود داره.

برای مثال، نمی‌تونیم توی Java بیایم non-static متدهای یه null object رو صدا بزنیم؛ اما توی گولنگ می‌شه.

var user *User user.Something()

کد بالا کار می‌کنه! بخاطر اینکه متد بالا شبیه به فانکشن زیر می‌شه.

func Something(user *User) { }

پس nil دقیقا چیه؟

نیل یا nil در لاتین به معنی هیچ است. nil در گولنگ Zero value خیلی از تایپ ها رو مشخص می‌کنه.

در گولنگ Pointerها مثل زبان C به جایی از حافظه اشاره می‌کنند؛ با این تفاوت که Pointer Arithmetic بخاطر امنیت حافظه نداریم و البته که Garbage Collector بخش Heap رو واسه‌مون مدیریت می‌کنه.

بنابراین nil pointer هم در واقع مقدار پیش فرض (Zero value) برای Pointer هست.

چه موقع nil == nil نیست؟

اگه به پیاده‌سازی interface در گولنگ نگاهی بندازیم، می‌بینیم که ساختار iface دو pointer داره.

type iface struct { tab *itab data unsafe.Pointer }

پوینتر اول (tab)، type اینترفیس و type دیتایی که بهش اشاره می‌کنه رو نگه می‌داره.
پوینتر دوم (data)، آدرس دیتا رو نگه می‌داره.

برای مثال، در کد زیر نوع اینترفیس مشخص شده اما نوع دیتا و اشاره‌گر به اون دیتا مشخص نشده.

var str fmt.Stringer fmt.Println(s == nil) // true

نکته مهم در رابطه با nil بودن اینترفیس این هست که اگه فقط نوع اینترفیس مشخص شده باشه، اون اینترفیس برابر با nil هست؛ ولی اگه نوع دیتا هم مشخص شده باشه، اون موقع اینترفیس nil نیست!

برای مثال، با اینکه در کد زیر ما یک ساختار nil رو داریم به اینترفیس می‌دیم اما چون نوع دیتا رو با این کار داریم مشخص می‌کنیم، دیگه اون اینترفیس nil نیست! و می‌تونیم حتی متدها رو صدا بزنیم!

var usr *User var str fmt.Stringer str = usr fmt.Println(str == nil) // false

برگشت دادن اینترفیس کار اشتباهیه؛ اما بخاطر موارد بالا باید برای اینترفیس error این استثنا رو داشته باشیم.
یعنی اگه Custom Error داریم، بجای برگشت دادن Concrete struct، اینترفیس error رو برگشت بدیم.

برای مثال، ما یه Custom Error داریم.

type CustomError struct { } func (c *CustomError) Error() string { return &quotAn error occurred&quot }

و یه فانکشن WrapError که داره فانکشن اصلی‌مون رو صدا می‌زنه.

func WrapError() error { return DoSomething() } func DoSomething() *CustomError { return nil }

و در نهایت WrapError صدا زده می‌شه.
توی فانکشن اصلی‌مون یعنی DoSomething، داریم nil برگشت می‌دیم و یعنی خطایی رخ نداده، اما وقتی nil بودن خطا رو داریم بررسی می‌کنیم، err != nil هست!

func main() { err := WrapError() if err != nil { // true fmt.Printf(&quotDoSomething: %s\n&quot, err) // DoSomething: An error occurred } }

توی مثال بالا داریم یه Concrete struct رو برگشت می‌دیم و توی یه فانکشن دیگه داریم errorای که به دستمون رسیده رو برگشت می‌دیم و در نهایت err رو برای nil بودن بررسی می‌کنیم.
چون اینجا نوع دیتا بخاطر برگشت دادن Concrete struct مشخص شده، پس دیگه err برابر nil نیست!

توی گولنگ nil اشتباه چند میلیون دلاری نیست!

توی گولنگ nil می‌تونه اشتباه چند میلیون دلاری نباشه. درسته که برای zero value تایپ های function ،interface ،pointer ،slice ،map و channel مقدار nil رو داریم، اما یه سری از اینا کار رو خراب نمی‌کنن یا حتی می‌تونن بدرد بخور هم باشن!

برای مثال می‌تونیم به slice ای که nil هست append کنیم! چون وقتی append انجام می‌دیم تازه allocation اتفاق می‌افته.

var buff []byte fmt.Println(buff == nil, buff, cap(buff), len(buff)) // true [] 0 0 buff = append(buff, 0) fmt.Println(append(buff, 0), cap(buff), len(buff)) // [0] 8 1

برای map هم اونقدرا اوضاع بد نیست. می‌تونیم به شکل read-only از map ای که nil هست استفاده کنیم.

var config map[string]interface{} fmt.Println(config == nil, config[&quotverify_certificates&quot])

بعضی جاها nil کردن channel می‌تونه ما رو نجات بده!

توی خیلی از Concurrency pattern ها میایم از select برای خواندن از دو یا چند کانال استفاده می‌کنیم. مشکل از جایی شروع می‌شه که یکی از کانال ها بسته شده و از یکی دیگه همچنان داریم مقدار می‌خونیم. توی این حالت هر دفعه مقدار بسته شدن کانال اول رو می‌گیریم و ممکنه کارمون رو خراب کنه! اما با nil کردن کانالی که بسته شده خیلی راحت می‌تونیم اون case رو نادیده بگیریم.

برای مثال ما سه کانال in2 ،in1 و out رو داریم. می‌خوایم هر چی از دو کانال in گرفتیم رو توی کانال out بریزیم. برای اینکار فانکشن funnel رو به شکل زیر داریم.

func funnel(out chan<- int, in1, in2 <-chan int) { for { select { case n, ok := <-in1: if !ok { in1 = nil continue } out <- n case n, ok := <-in2: if !ok { in2 = nil continue } out <- n } } }

اگه فقط به continue بسنده کنیم، ممکنه بلاک درون if !ok بیشتر از چند بار اجرا بشه و کلا کار رو خراب کنه.
ولی چرا با nil کردن کانال، کد درست کار می‌کنه؟ در کل وقتی می‌خوایم از یه کانال nil، بخونیم یا داخلش بنویسیم، برای همیشه بلاک می‌شیم. به همین دلیل select میاد case ای که برای همیشه بلاک شده رو نادیده می‌گیره.




از شما ممنونم که تا اینجا وقت گذاشتید. امیدوارم مفید بوده باشه.
هر سوالی داشتید توی کامنت ها بپرسید و اگه فکر می‌کنید جایی دارم اشتباه می‌گم لطفا بهم بگید تا این نوشته رو کامل کنیم.

گولنگ
شاید از این پست‌ها خوشتان بیاید