تصرّح الكلمة المفتاحية var عن متغير. تعطيه اسمًا ونوعًا، أو قيمة ابتدائية، أو كليهما، كما في var port int و var host = "localhost" و var ratio float64 = 0.25. والمتغير المُصرَّح عنه دون قيمة يبدأ بالقيمة الصفرية (zero value) لنوعه، وهي 0 للأعداد و "" للسلاسل النصية و false للقيم المنطقية و nil للمؤشرات والـ slices والـ maps والـ channels والدوال والواجهات. وداخل الدوال يستخدم معظم كود Go الصيغة المختصرة name := value بدلًا منها، لكن var هي الطريقة الوحيدة للتصريح عن متغير على مستوى الحزمة (مواصفات Go).
الخلاصة
- الصيغة
var name Typeتصرّح عن متغير بقيمته الصفرية. والصيغةvar name = valueتستنتج النوع من القيمة. والصيغةvar name Type = valueتفعل الأمرين معًا. أما الكتلةvar ( ... )فتجمع عدة تصريحات. - كل متغير في Go مُهيّأ. لا توجد ذاكرة غير مُهيّأة، والأنواع مثل
sync.Mutexوsync.WaitGroupوbytes.Bufferجاهزة للاستخدام بقيمتها الصفرية. :=تصرّح وتُسند في خطوة واحدة، لكن داخل الدوال فقط. أما على مستوى الحزمة فتحتاج إلىvar.- تحتاج
:=إلى اسم جديد واحد على الأقل في جانبها الأيسر. والأسماء الموجودة مسبقًا في النطاق نفسه يُعاد استخدامها، أما الأسماء القادمة من نطاق خارجي فتُظلَّل. - تظليل متغير خارجي بـ
:=داخل كتلةifخطأ شائع. يقبله المترجم، ويكتشفه المحلّلshadow. - المتغير المحلي غير المستخدم خطأ ترجمة،
declared and not used. أما متغيرات مستوى الحزمة غير المستخدمة فتُترجم. - تُهيّأ متغيرات مستوى الحزمة بحسب ترتيب الاعتماديات قبل تشغيل أي دالة
init(). وأخطاء الحارس والتعابير النمطية المُترجَمة أمثلة نموذجية على متغيرات مستوى الحزمة. - السطر
var _ http.Handler = (*Server)(nil)يجعل المترجم يتحقق من أن نوعًا ما يحقق واجهة.
كيف تصرّح عن متغير في Go؟
هناك ثلاث صيغ، وكتلة مجمّعة واحدة تضم أي مزيج منها:
example.gogopackage main import ( "fmt" "time" ) var ( serviceName = "billing-api" maxRetries int64 = 3 requestCount int ) func main() { var port int // type only: starts at 0 var host = "localhost" // value only: the type is inferred as string var ratio float64 = 0.25 // both var readTimeout, writeTimeout time.Duration fmt.Println(serviceName, maxRetries, requestCount, port, host, ratio, readTimeout, writeTimeout) }
يطبع هذا البرنامج billing-api 3 0 0 localhost 0.25 0s 0s.
حين تحدد نوعًا دون قيمة، يأخذ المتغير القيمة الصفرية لذلك النوع. وحين تحدد قيمة دون نوع، يأخذ المتغير نوع القيمة. وبالنسبة إلى ثابت غير محدد النوع مثل "localhost" أو 0.25 يكون ذلك هو النوع الافتراضي: string أو int أو float64 أو rune أو bool أو complex128. وحين تحدد الاثنين، تُحوَّل القيمة إلى النوع المُصرَّح عنه، وهكذا تحصل على int64 أو float32 من قيمة عددية حرفية عادية. فالمتغير var maxRetries int64 = 3 من النوع int64، أما var maxRetries = 3 فمن النوع int.
يمكن أن يسرد تصريح واحد عدة أسماء. فالسطر var readTimeout, writeTimeout time.Duration يصرّح عن متغيرين من النوع نفسه، قيمة كل منهما 0s. والكتلة المجمّعة var ( ... ) شائعة على مستوى الحزمة، حيث تُبقي الإعدادات المترابطة معًا. وعلى عكس كتلة const، لا تكرر هذه الكتلة تعبير السطر السابق، لذا يأخذ كل اسم دون قيمة القيمة الصفرية.
وكأي اسم آخر في Go، يكون المتغير المُصرَّح عنه على مستوى الحزمة مُصدَّرًا إذا بدأ اسمه بحرف كبير. فـ http.DefaultClient و os.Args متغيران مُصدَّران على مستوى الحزمة. ويشرح مقال الكلمة المفتاحية package في Go كيف تعمل الأسماء المُصدَّرة بين الحزم.
ما القيمة الصفرية في Go؟
القيمة الصفرية هي القيمة التي يحملها المتغير حين تصرّح عنه دون تهيئته. لا توجد في Go ذاكرة غير مُهيّأة، لذا لكل متغير قيمة محددة منذ لحظة وجوده:
| النوع | القيمة الصفرية |
|---|---|
bool | false |
int و float64 وبقية الأنواع العددية | 0 |
string | "" (السلسلة النصية الفارغة) |
| المؤشرات والـ slices والـ maps والـ channels والدوال والواجهات | nil |
| المصفوفات | كل عنصر مضبوط على قيمته الصفرية |
| الـ structs | كل حقل مضبوط على قيمته الصفرية |
تنطبق القاعدة نفسها على الذاكرة التي تأتي من new، وعلى حقول الـ struct المحذوفة من القيمة المركّبة الحرفية (composite literal)، وعلى عناصر الـ slice المُنشأة بـ make. فالـ struct المسمّى User الذي فُكّ ترميزه من JSON دون حقل email ينتهي بـ Email == ""، لا بقيمة عشوائية.
لماذا تفيد القيم الصفرية في Go؟
الأنواع المصممة جيدًا تعمل بقيمتها الصفرية دون دالة إنشاء (constructor)، والمكتبة القياسية تعتمد على ذلك. فالقيمة الصفرية لـ sync.Mutex هي mutex غير مقفل، والقيمة الصفرية لـ sync.WaitGroup مجموعة عدّادها صفر، والقيمة الصفرية لـ bytes.Buffer مخزن مؤقت فارغ جاهز للكتابة. والـ struct المبني من حقول كهذه جاهز للاستخدام بمجرد التصريح عنه:
example.gogopackage main import ( "bytes" "fmt" "sync" ) type RequestCounter struct { mu sync.Mutex total int } func (c *RequestCounter) Inc() { c.mu.Lock() defer c.mu.Unlock() c.total++ } func main() { var counter RequestCounter // ready to use, no constructor needed var wg sync.WaitGroup for range 10 { wg.Go(counter.Inc) } wg.Wait() var buf bytes.Buffer fmt.Fprintf(&buf, "handled %d requests", counter.total) fmt.Println(buf.String()) }
يطبع هذا البرنامج handled 10 requests. لم يحتج أي من المتغيرات الثلاثة إلى دالة New. ويوصي Effective Go بتصميم أنواعك الخاصة بالطريقة نفسها، بحيث يكفي var x T لتبدأ باستخدام T.
ما القيم الصفرية التي لا يمكنك استخدامها مباشرة؟
الـ maps، وبدرجة أقل المؤشرات والـ channels. فالـ slice ذات القيمة nil تعمل مع append و len و range، لذا لا مشكلة في var tags []string متبوعًا بـ tags = append(tags, "go"). أما الـ map ذات القيمة nil فيمكن القراءة منها لا الكتابة فيها، والكتابة فيها تسبب panic بالرسالة assignment to entry in nil map. وفك الإشارة (dereference) إلى مؤشر nil يسبب panic، والإرسال إلى channel قيمتها nil يعلق إلى الأبد. ويتناول مقال var مقابل make في Go هذه الحالات ومتى تلجأ إلى make بدلًا منها.
ما الفرق بين var و := في Go؟
:= هي صيغة التصريح المختصر عن المتغيرات (short variable declaration). فـ retries := 3 تعني الشيء نفسه الذي تعنيه var retries = 3: تصرّح عن متغير جديد وتستنتج نوعه من القيمة. وهي توفّر كتابة الكلمة المفتاحية والنوع، وهي الخيار المعتاد داخل الدوال، خاصةً للقيم التي تعيدها استدعاءات الدوال:
example.gogoresp, err := http.Get(healthURL)
الفرق في المكان المسموح فيه لكل منهما. فـ := لا تعمل إلا داخل جسم دالة. أما على مستوى الحزمة فيجب أن يبدأ كل تصريح بكلمة مفتاحية، لذا يفشل هذا الكود:
example.gogopackage main port := 8080 func main() {}
example.texttext./main.go:3:1: syntax error: non-declaration statement outside function body
أما var port = 8080 على مستوى الحزمة فيُترجم.
داخل الدوال تُترجم الصيغتان كلتاهما، ويعود الاختيار بينهما إلى بضعة أعراف:
- استخدم
:=حين تكون لديك قيمة تُسندها، مثلcfg := loadConfig()أوn, err := w.Write(body). - استخدم
varحين تريد القيمة الصفرية عن قصد، مثلvar buf bytes.Bufferأوvar wg sync.WaitGroupأوvar users []Userلـ slice تملؤها بـappend. فـvarتخبر القارئ أن القيمة الصفرية هي نقطة البداية. - استخدم
varمع نوع حين يكون النوع الافتراضي للقيمة الحرفية غير مناسب. فـvar limit int64 = 1 << 40أوvar threshold float32 = 0.8أوضح في القراءة منlimit := int64(1 << 40). - استخدم
varحين يُسند إلى المتغير لاحقًا في فروع مختلفة، كما فيvar status stringمتبوعًا بـif/elseتضبط قيمته.
هل يمكنك إعادة التصريح عن متغير بـ := في Go؟
جزئيًا. تحتاج := إلى متغير جديد واحد على الأقل في جانبها الأيسر، وأي اسم مُصرَّح عنه مسبقًا في النطاق نفسه يُسند إليه بدلًا من التصريح عنه من جديد. وهذا ما يسمح لدالة بإعادة استخدام err واحد عبر عدة استدعاءات:
example.gogopackage main import "os" func main() { f, err := os.Open("config.yaml") if err != nil { return } defer f.Close() info, err := f.Stat() // ok: info is new, err is reused _ = info f, err := os.Open("secrets.yaml") _ = f }
example.texttext./main.go:14:9: no new variables on left side of :=
يُترجم info, err := f.Stat() لأن info جديد. أما في آخر :=، فكل من f و err موجود مسبقًا، لذا لا يوجد ما يُصرَّح عنه. اكتب f, err = os.Open("secrets.yaml") باستخدام = العادية للإسناد إليهما. ولا يوجد استثناء كهذا في var. فكتابة var err error مرتين في النطاق نفسه تفشل بالخطأ err redeclared in this block.
ما تظليل المتغيرات (shadowing) في Go؟
يحدث التظليل حين يستخدم تصريح في نطاق داخلي الاسم نفسه الذي يحمله متغير في نطاق خارجي. يخفي المتغير الداخلي المتغير الخارجي إلى أن ينتهي النطاق الداخلي. وكل كتلة if و for و switch و { } تفتح نطاقًا جديدًا، و := تصرّح دائمًا في النطاق الحالي. واجتماع هاتين القاعدتين يجعل كتابة هذا الخطأ سهلة:
example.gogopackage main import ( "fmt" "strconv" ) func listenAddr(rawPort string) (string, error) { port := 8080 if rawPort != "" { port, err := strconv.Atoi(rawPort) if err != nil { return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err) } fmt.Println("using PORT", port) } return fmt.Sprintf(":%d", port), nil } func main() { addr, err := listenAddr("9090") fmt.Println(addr, err) }
يطبع هذا البرنامج using PORT 9090 ثم :8080 <nil>. داخل كتلة if، المتغير err جديد، لذا تُسمح :=، وهي تصرّح عن port جديد أيضًا. تذهب القيمة المُحلَّلة إلى port الداخلي، الذي يختفي عند القوس المُغلِق. وتعيد الدالة port الخارجي، الذي ما زالت قيمته 8080. ويشرح مقال الكلمة المفتاحية if في Go قواعد النطاق في عبارات if التي تجعل هذا ممكنًا.
الحل هو الإسناد إلى المتغير الخارجي بدلًا من التصريح عن متغير جديد. صرّح عن err أولًا ثم استخدم =:
example.gogoif rawPort != "" { var err error port, err = strconv.Atoi(rawPort) if err != nil { return "", fmt.Errorf("invalid PORT %q: %w", rawPort, err) } }
لا تبلّغ فحوص go vet الافتراضية عن التظليل، لأن كثيرًا من التظليل مقصود، مثل err داخل if متداخلة. لكن المحلّل shadow من فريق Go يبلّغ عنه:
example.texttextgo install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest go vet -vettool=$(which shadow) ./...
example.texttext./main.go:11:3: declaration of "port" shadows declaration at line 9
ويمكن لـ golangci-lint تشغيل الفحص نفسه عبر أداة الفحص govet مع تفعيل shadow.
لماذا تقول Go "declared and not used"؟
يرفض المترجم أي متغير محلي لا يُقرأ أبدًا. في الكود السابق، سطر الطباعة هو السبب الوحيد الذي يجعل port الداخلي يُترجم. فبدونه يبلّغ المترجم عن declared and not used: port ويشير إلى الخطأ. والإسناد إلى المتغير لا يُعدّ استخدامًا له:
example.gogopackage main import "net/http" func main() { resp, err := http.Get("https://example.com") if err != nil { return } retries := 0 retries = 3 }
example.texttext./main.go:6:2: declared and not used: resp ./main.go:10:2: declared and not used: retries
لا تشمل القاعدة إلا المتغيرات المحلية. فمتغير مستوى الحزمة غير المستخدم، ومعامل الدالة غير المستخدم، والنتيجة المُسمّاة غير المستخدمة، كلها تُترجم. وحين تحتاج إلى الاحتفاظ بقيمة لا تقرأها بعد، أسندها إلى المعرّف الفارغ بـ _ = resp، أو تجاهلها كليًا بـ _, err := http.Get(url).
كيف تعمل متغيرات مستوى الحزمة في Go؟
المتغير المُصرَّح عنه خارج أي دالة ينتمي إلى الحزمة. فهو مرئي في كل ملفات الحزمة، ويبقى موجودًا طوال عمر البرنامج، ويُهيّأ قبل أن تبدأ main. وتُهيّأ متغيرات مستوى الحزمة بحسب اعتمادياتها، لا بالترتيب الذي تظهر به في الكود المصدري:
example.gogopackage main import ( "errors" "fmt" ) var ( usersURL = apiBase + "/users" apiBase = apiHost + "/v2" apiHost = "https://api.internal" ) var ErrNotFound = errors.New("user not found") func init() { fmt.Println("init sees", usersURL) } func main() { err := fmt.Errorf("load user 42: %w", ErrNotFound) fmt.Println(usersURL, errors.Is(err, ErrNotFound)) }
يطبع هذا البرنامج init sees https://api.internal/v2/users ثم https://api.internal/v2/users true. المتغير usersURL مُصرَّح عنه أولًا، لكنه يعتمد على apiBase، الذي يعتمد بدوره على apiHost، لذا يهيّئها المترجم بالترتيب المعاكس. أما المتغيرات التي لا يعتمد بعضها على بعض فتُهيّأ بترتيب التصريح. والمتغيران اللذان يعتمد كل منهما على الآخر خطأ، initialization cycle for a.
بعد أن يحصل كل متغير على مستوى الحزمة على قيمته، تُشغَّل دوال init() بالترتيب الذي تظهر به في الكود المصدري، ملفًا بعد ملف. ويمكن أن تحتوي الحزمة على عدة دوال init()، حتى في ملف واحد. وتُهيّأ الحزم المستوردة بالكامل أولًا، لذا حين تُشغَّل init() الخاصة بك تكون كل حزمة تستوردها جاهزة. ويشرح مقال الكلمة المفتاحية import في Go كيف تتفاعل init() مع الاستيراد الفارغ (blank imports).
لماذا يُصرَّح عن أخطاء الحارس بـ var؟
لأن errors.New استدعاء دالة، ولا يمكن أن تكون نتيجتها ثابتًا. وخطأ الحارس (sentinel error) متغير على مستوى الحزمة يقارن به المستدعون باستخدام errors.Is:
example.gogovar ( ErrNotFound = errors.New("user not found") ErrEmailTaken = errors.New("email already registered") )
تقارن errors.Is قيم الأخطاء، وكل استدعاء لـ errors.New يعيد قيمة مختلفة، حتى مع النص نفسه. لهذا يجب إنشاء الخطأ مرة واحدة على مستوى الحزمة ومشاركته. ومن أمثلة المكتبة القياسية io.EOF و sql.ErrNoRows و http.ErrServerClosed.
والأمر نفسه ينطبق على القيم الأخرى المكلفة في بنائها والتي لا تتغير بعد بدء التشغيل. فالسطر var slugPattern = regexp.MustCompile("^[a-z0-9-]+$") يترجم التعبير النمطي مرة واحدة عند بدء البرنامج. كما يسبب panic عند بدء التشغيل إذا كان النمط غير صالح، بدلًا من الفشل عند أول طلب.
هل ينبغي أن تستخدم المتغيرات العامة في Go؟
للقيم الثابتة بعد بدء التشغيل، مثل أخطاء الحارس والأنماط المُترجَمة وجداول البحث، نعم. أما للحالة القابلة للتغيير (mutable state) فغالبًا لا. فالمتغير var db *sql.DB أو var cfg Config على مستوى الحزمة تتشاركه كل goroutine وكل اختبار في الحزمة. والكتابات المتزامنة تحتاج إلى mutex، والاختبارات لا يمكن تشغيلها بالتوازي بقيم مختلفة، ولا شيء في توقيع الدالة يُظهر أنها تعتمد على المتغير العام.
البديل الشائع هو وضع الاعتماديات في struct وتمريره:
example.gogotype Server struct { db *sql.DB logger *slog.Logger } func (s *Server) handleGetUser(w http.ResponseWriter, r *http.Request) { // uses s.db and s.logger instead of package-level variables }
عندها يستطيع كل اختبار بناء Server خاص به، بقاعدة بيانات و logger خاصين به.
ما معنى var _ = في Go؟
تصرّح هذه الصيغة عن متغير اسمه المعرّف الفارغ _. تُحسب القيمة ثم يُتخلَّص منها. والاستخدام الأشيع هو فحص وقت الترجمة للتأكد من أن نوعًا ما يحقق واجهة:
example.gogotype Server struct{} func (s *Server) ServeHTTp(w http.ResponseWriter, r *http.Request) {} var _ http.Handler = (*Server)(nil)
في اسم الدالة أعلاه خطأ مطبعي، والتصريح يحوّله إلى خطأ بناء:
example.texttext./main.go:9:22: cannot use (*Server)(nil) (value of type *Server) as http.Handler value in variable declaration: *Server does not implement http.Handler (missing method ServeHTTP) have ServeHTTp(http.ResponseWriter, *http.Request) want ServeHTTP(http.ResponseWriter, *http.Request)
دون هذا الفحص، لن يظهر الخطأ المطبعي إلا حيث يُمرَّر *Server بوصفه http.Handler، وقد يكون ذلك في حزمة أخرى أو لا يحدث أبدًا. و (*Server)(nil) مؤشر nil محوَّل إلى *Server، لذا لا يكلّف الفحص أي تخصيص للذاكرة. واستخدم Server{} بدلًا منه حين تستخدم الدوال مستقبِلات قيمة (value receivers).
ويعمل المعرّف الفارغ في التصريحات متعددة القيم أيضًا. فالسطر var _, offset = time.Now().Zone() يحتفظ بالنتيجة الثانية فقط.
كيف تصرّح عن عدة متغيرات دفعة واحدة في Go؟
اسرد الأسماء مفصولة بفواصل، وأعطِ قائمة مطابقة من القيم أو استدعاء دالة يعيد العدد نفسه من القيم:
example.gogopackage main import ( "fmt" "strings" ) var host, port = "localhost", 8080 func main() { var user, domain, found = strings.Cut("[email protected]", "@") fmt.Println(host, port, user, domain, found) }
يطبع هذا البرنامج localhost 8080 ops levelupgo.dev true. دون نوع، يأخذ كل متغير نوع قيمته، لذا يكون host من النوع string و port من النوع int. ومع نوع، كما في var minPort, maxPort int = 1024, 65535، تتشارك كلها هذا النوع.
والإسناد يعمل بالطريقة نفسها. فـ a, b = b, a يبدّل قيمتي متغيرين، لأن Go تحسب كل القيم في الجانب الأيمن قبل أن تُسند أيًا منها.
ومنذ Go 1.22، صارت متغيرات الحلقة المُصرَّح عنها بـ := في تعليمة for متغيرات جديدة في كل تكرار. فالـ closure أو الـ goroutine التي تلتقط job في for _, job := range jobs ترى مهمة ذلك التكرار، لا المهمة الأخيرة. ويعرض مقال الكلمة المفتاحية for في Go خلل الـ goroutines الذي أصلحه هذا التغيير وكيف تتحقق من السلوك الذي تحصل عليه الوحدة (module).
متى تستخدم var ومتى تستخدم const في Go؟
استخدم const للقيم المعروفة حين تكتب الكود والتي لا تتغير أبدًا، مثل المهل الزمنية والحدود وأسماء الترويسات. واستخدم var لكل ما عدا ذلك: القيم المحسوبة وقت التشغيل، والقيم المقروءة من الإعدادات، وأي slice أو map أو struct أو مؤشر، لأن هذه لا يمكن أن تكون ثوابت. ويشرح مقال الكلمة المفتاحية const في Go الثوابت غير المحددة النوع و iota والتعدادات.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تقدّم دورة Go Basics الكلمة var و := والقيم الصفرية في دروسها الأولى، إلى جانب الثوابت والأنواع. وفي دورة Go Language Deep Dives درس عن النطاق والتظليل تعثر فيه على متغيرات مُظلَّلة وتصلحها. ويقدّم Training Ground تمارين قصيرة مستقلة للتدرّب خارج أي دورة. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل var كلمة مفتاحية في Go؟
نعم. var واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك استخدامها اسمًا لمتغير أو دالة أو نوع. وهي تبدأ تصريحًا عن متغير، إما تصريحًا واحدًا var name Type = value أو كتلة مجمّعة var ( ... ).
ما الفرق بين var و := في Go؟
:= صيغة مختصرة لـ var تستنتج النوع دائمًا ولا تعمل إلا داخل الدوال. فكل من x := 5 و var x = 5 يصرّح عن int قيمته 5. و var مطلوبة على مستوى الحزمة، وهي الخيار الأوضح حين تريد القيمة الصفرية أو نوعًا غير النوع الافتراضي للقيمة الحرفية. ويمكن لـ := أيضًا أن تمزج أسماء جديدة وموجودة، ما دام اسم واحد على الأقل في الجانب الأيسر جديدًا.
ما القيمة الصفرية للمتغير في Go؟
هي القيمة التي يحملها المتغير حين يُصرَّح عنه دون قيمة ابتدائية: 0 للأعداد، و "" للسلاسل النصية، و false للقيم المنطقية، و nil للمؤشرات والـ slices والـ maps والـ channels والدوال والواجهات. وتحصل المصفوفات والـ structs على القيمة الصفرية في كل عنصر أو حقل. ولا توجد في Go متغيرات غير مُهيّأة.
هل يمكنك استخدام := خارج دالة في Go؟
لا. على مستوى الحزمة يجب أن يبدأ كل تصريح بكلمة مفتاحية مثل var أو const أو type أو func، و port := 8080 يفشل بالخطأ syntax error: non-declaration statement outside function body. اكتب var port = 8080 بدلًا منه.
لماذا تعطي Go الخطأ "declared and not used"؟
لأن المتغير المحلي غير المستخدم يشير غالبًا إلى خطأ، مثل متغير مُظلَّل أو فحص خطأ منسي، لذا يرفضه مترجم Go. والإسناد إلى المتغير لا يُعدّ استخدامًا له. اقرأ المتغير، أو احذفه، أو أسنده إلى المعرّف الفارغ بـ _ = x. أما متغيرات مستوى الحزمة ومعاملات الدوال غير المستخدمة فمسموحة.
المصادر
- The Go Programming Language Specification, Variable declarations: https://go.dev/ref/spec#Variable_declarations
- The Go Programming Language Specification, Short variable declarations: https://go.dev/ref/spec#Short_variable_declarations
- The Go Programming Language Specification, The zero value: https://go.dev/ref/spec#The_zero_value
- The Go Programming Language Specification, Package initialization: https://go.dev/ref/spec#Package_initialization
- Effective Go, Variables and redeclaration: https://go.dev/doc/effective_go#redeclaration
- Effective Go, Allocation with new: https://go.dev/doc/effective_go#allocation_new
- Go FAQ, Can I stop these complaints about my unused variable/import?: https://go.dev/doc/faq#unused_variables_and_imports
- Fixing For Loops in Go 1.22: https://go.dev/blog/loopvar-preview
- shadow analyzer: https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow
