تصرّح الكلمة المفتاحية const عن اسم لقيمة يعرفها المترجم وقت البناء، مثل مهلة زمنية (timeout) أو اسم ترويسة أو حد للحجم. لا يمكن للثابت في Go أن يحمل إلا قيمة منطقية أو عددًا أو سلسلة نصية أو rune، ولا يمكن أن يتغير أبدًا أثناء تشغيل البرنامج. ومعظم الثوابت غير محددة النوع (untyped)، أي ليس لها نوع ثابت إلى أن تستخدمها، لذا يعمل const maxBodyBytes = 1 << 20 بوصفه int أو int64 أو float64 دون أي تحويل. وداخل كتلة const، يعدّ المعرّف المُصرَّح عنه مسبقًا iota تصاعديًا بدءًا من 0، وهكذا تكتب Go التعدادات (مواصفات Go).
الخلاصة
- الصيغة
const name = valueتصرّح عن ثابت واحد. والكتلةconst ( ... )تصرّح عن عدة ثوابت، ومعظم كود Go يجمع الثوابت المترابطة بهذه الطريقة. - يمكن أن تكون الثوابت قيمًا منطقية أو أعدادًا أو سلاسل نصية أو runes. أما الـ slices والـ maps والـ structs ونتيجة استدعاء دالة مثل
time.Now()فلا يمكن أن تكون ثوابت. - الثابت غير المحدد النوع مثل
5أو"GET"يأخذ النوع الذي يحتاجه سياقه. ولهذا السبب يُترجم5 * time.Second، بينما لا يُترجمtimeout * time.Secondحين يكونtimeoutمتغيرًا من نوعint. - الثوابت غير المحددة النوع دقيقة تمامًا. يُجري المترجم عملياتها الحسابية بدقة غير محدودة، ولا يعترض إلا حين لا تتسع القيمة للنوع الذي تنتهي إليه.
- تبدأ
iotaمن 0 في كل كتلةconstوتزيد بمقدار واحد في كل سطر. يتخطى_قيمة، وiota + 1تبدأ من 1، و1 << iotaتبني رايات البت (bit flags). - لا توجد في Go كلمة مفتاحية
enum. التعداد هو نوع مُسمّى، وكتلةconstمعiota، وعادةً دالةString()يمكن أن تولّدها أداةstringer. - لا يمكنك أخذ عنوان ثابت ولا الإسناد إليه. والقيم المحسوبة وقت التشغيل، وجداول البحث مثل الـ maps، مكانها
var.
كيف تصرّح عن ثابت في Go؟
اكتب const ثم اسمًا ثم قيمة. ولعدة ثوابت مترابطة، استخدم كتلة مجمّعة بين قوسين:
example.gogopackage main import ( "fmt" "net/http" "time" ) const maxBodyBytes = 1 << 20 // 1 MiB const ( readTimeout = 5 * time.Second writeTimeout = 10 * time.Second idleTimeout = 2 * time.Minute ) const ( headerRequestID = "X-Request-ID" headerRetryAfter = "Retry-After" ) func main() { srv := &http.Server{ Addr: ":8080", ReadTimeout: readTimeout, WriteTimeout: writeTimeout, IdleTimeout: idleTimeout, } fmt.Println(srv.ReadTimeout, srv.WriteTimeout, srv.IdleTimeout) }
يطبع هذا البرنامج 5s 10s 2m0s. وفي القيم البسيطة مثل هذه، يكون التجميع من أجل القارئ فقط. فكل اسم في الكتلة ثابت مستقل، والكتلة تُبقي المهل الزمنية للخادم في مكان واحد يستطيع فيه المراجع مقارنتها.
تتبع الثوابت قواعد النطاق والظهور نفسها التي تتبعها المتغيرات. فالثابت المُصرَّح عنه على مستوى الحزمة مرئي في كل ملفات الحزمة، ويكون مُصدَّرًا إذا بدأ اسمه بحرف كبير، كما في http.StatusNotFound أو time.RFC3339. ويشرح مقال الكلمة المفتاحية package في Go كيف تعمل الأسماء المُصدَّرة بين الحزم. أما الثابت المُصرَّح عنه داخل دالة فلا يوجد إلا في تلك الدالة. ويستخدم أسلوب Go الأحرف المختلطة (mixed caps) للثوابت كأي اسم آخر، لذا تكون الكتابة الاصطلاحية maxBodyBytes أو MaxBodyBytes، لا MAX_BODY_BYTES.
لكتلة const قاعدة واحدة لا توجد في كتلة var. حين يحتوي سطر على اسم دون نوع ودون قيمة، فإنه يكرر نوع السطر السابق وتعبيره. هذه القاعدة وحدها لا تفعل أكثر من إعطاء عدة أسماء القيمة نفسها، وهذا نادرًا ما تريده. أما مع iota فتتيح لتعبير واحد أن يرقّم قائمة كاملة من الثوابت.
ما القيم التي يمكن أن تكون ثوابت في Go؟
القيم المنطقية والأعداد والسلاسل النصية والـ runes فقط. أنواع الأعداد هي الأعداد الصحيحة والأعداد العشرية (floating-point) والأعداد المركبة، والـ rune عدد صحيح يمثّل نقطة ترميز (code point) في Unicode. وأي شيء آخر يفشل في الترجمة، بما في ذلك قيمة ثابتة عمليًا لكن يجب حسابها وقت التشغيل:
example.gogopackage main import "time" const startedAt = time.Now()
example.texttext./main.go:5:19: time.Now() (value of struct type time.Time) is not constant
والقيم المركّبة تُرفض بالطريقة نفسها. فالسطر const allowedMethods = []string{"GET", "POST"} يفشل بالخطأ []string{…} (value of type []string) is not constant. لا يمكن أن تكون الـ slices والـ maps والـ structs والمؤشرات والـ channels والدوال ثوابت، حتى لو كان كل عنصر فيها قيمة حرفية (literal).
أما الأنواع المُسمّاة المبنية على عدد أو سلسلة نصية أو قيمة منطقية فمسموحة. فالنوع time.Duration معرّف على أنه int64، لذا يكون const pollInterval = 500 * time.Millisecond ثابتًا من النوع time.Duration قيمته 500ms. والأمر نفسه ينطبق على أنواعك الخاصة، مثل type LogLevel int، وهذا ما تُبنى عليه التعدادات.
وبعض الدوال المدمجة تنتج ثوابت أيضًا حين يسمح معاملها بذلك. فـ len لمصفوفة ثابت، لأن طول المصفوفة جزء من نوعها. والأمر نفسه مع len("Retry-After")، لأن معاملها ثابت نصي. أما unsafe.Sizeof و unsafe.Alignof و unsafe.Offsetof فتنتج ثوابت متى كان نوع المعامل ذا حجم ثابت، لذا يُترجم const headerSize = unsafe.Sizeof(int64(0)) وقيمته 8. لكن len لـ slice ليس ثابتًا، لأن طول الـ slice لا يُعرف إلا وقت التشغيل.
ما الفرق بين الثوابت المحددة النوع وغير المحددة النوع في Go؟
الثابت المحدد النوع (typed) له نوع في تصريحه. أما الثابت غير المحدد النوع فليس له نوع، ويأخذ أي نوع يحتاجه الكود المحيط به:
example.gogoconst maxRetries int32 = 3 // typed: always an int32 const maxBodyBytes = 1 << 20 // untyped: becomes whatever type it is used as
يتبع الثابت المحدد النوع قواعد الإسناد العادية، لذا لا يمكن وضعه في int دون تحويل:
example.gogovar attempts int = maxRetries
example.texttext./main.go:6:21: cannot use maxRetries (constant 3 of type int32) as int value in variable declaration
أما الثابت غير المحدد النوع فيتسع في var limit int64 = maxBodyBytes و var limit int = maxBodyBytes و float64(maxBodyBytes) على حد سواء. وتعتمد المكتبة القياسية على هذه المرونة، فتترك معظم ثوابتها العددية دون نوع، مثل math.MaxInt64 و math.Pi.
ومع ذلك، للثابت غير المحدد النوع نوع افتراضي. ويُستخدم هذا النوع حين لا يحدد شيء آخر النوع، كما في x := 1.5 أو حين يُمرَّر الثابت إلى fmt.Println:
| الثابت غير المحدد النوع | النوع الافتراضي |
|---|---|
true, false | bool |
42, 1 << 20 | int |
1.5, 1e9 | float64 |
'a' | rune (اسم بديل لـ int32) |
2i | complex128 |
"GET" | string |
لماذا يُترجم 5 * time.Second بينما يفشل timeout * time.Second؟
لأن 5 ثابت غير محدد النوع، و timeout متغير. لا تحوّل Go أبدًا بين الأنواع العددية من تلقاء نفسها، لكن الثابت غير المحدد النوع يمكن أن يأخذ أي نوع عددي تتسع له قيمته. ففي 5 * time.Second يصبح 5 من النوع time.Duration، وتكون النتيجة time.Duration مدتها خمس ثوانٍ.
أما المتغير فله نوع مسبقًا، لذا تفشل عملية الضرب نفسها حين يأتي العدد من الإعدادات:
example.gogopackage main import ( "net/http" "os" "strconv" "time" ) func main() { timeout, _ := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS")) client := &http.Client{Timeout: timeout * time.Second} _ = client }
example.texttext./main.go:12:34: invalid operation: timeout * time.Second (mismatched types int and "time".Duration)
المتغير timeout من النوع int، و time.Second من النوع time.Duration. والحل تحويل صريح: time.Duration(timeout) * time.Second. أما الثابت غير المحدد النوع الذي يحمل القيمة نفسها فيعمل دون تحويل، لذا يعطي const retryDelay = 3 متبوعًا بـ retryDelay * time.Second القيمة 3s.
ما مدى دقة الثوابت غير المحددة النوع؟
الثوابت العددية غير المحددة النوع دقيقة تمامًا. تشترط المواصفات أن تمثّل المترجمات الثوابت الصحيحة بما لا يقل عن 256 بت، وأن تحافظ على دقة الثوابت العشرية بجزء عشري (mantissa) لا يقل عن 256 بت، ويسمح مترجم Go القياسي بما يصل إلى 512 بت. والثابت math.Pi مكتوب بأرقام أكثر مما يستطيع float64 حمله، ولا يُقرَّب إلا حين يوضع في متغير من النوع float64 أو float32.
تتيح لك هذه الدقة كتابة قيم وسيطة أكبر من أي نوع في Go، ما دامت النتيجة النهائية تتسع. فالسطر const maxOffset = 1 << 100 يُترجم، و maxOffset >> 98 هو الثابت 4. ولا يظهر الخطأ إلا حين يجب أن تصبح القيمة نوعًا محددًا:
example.gogopackage main import "fmt" const maxOffset = 1 << 100 func main() { fmt.Println(maxOffset) }
example.texttext./main.go:8:14: cannot use maxOffset (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows)
ولأن المترجم يعرف قيمة كل ثابت، تحدث فحوص النطاق هذه وقت البناء، بدلًا من أن تلتف القيمة بصمت وقت التشغيل. وهذه أمثلة أخرى:
| الكود | خطأ المترجم |
|---|---|
var uploadLimit int32 = 1 << 40 | cannot use 1 << 40 (untyped int constant 1099511627776) as int32 value in variable declaration (overflows) |
const maxConns uint8 = 300 | cannot use 300 (untyped int constant) as uint8 value in constant declaration (overflows) |
var workers int = 2.5 | cannot use 2.5 (untyped float constant) as int value in variable declaration (truncated) |
لا ينظر الفحص إلا إلى القيمة. فالسطر var workers int = 2.0 يُترجم، لأن 2.0 ثابت عشري غير محدد النوع قيمته عدد صحيح.
ما iota في Go؟
iota معرّف مُصرَّح عنه مسبقًا، ولا معنى له إلا داخل تصريح const. قيمته 0 في أول مواصفة ثابت (constant spec) في كتلة const، وتزيد بمقدار واحد مع كل مواصفة تليها. والمواصفة سطر واحد يصرّح عن أسماء، لذا لا تُحسب الأسطر الفارغة ولا التعليقات. ومع التكرار الضمني للتعبير السابق، يكفي نوع واحد و iota واحدة لترقيم قائمة كاملة:
example.gogotype LogLevel int const ( LevelDebug LogLevel = iota // 0 LevelInfo // 1 LevelWarn // 2 LevelError // 3 )
ليس لـ LevelInfo و LevelWarn و LevelError نوع أو قيمة خاصة بها، لذا يكرر كل منها LogLevel = iota مع القيمة التالية لـ iota. وكل ثابت في القائمة من النوع LogLevel.
كيف تعدّ iota:
- تعود
iotaإلى 0 في بداية كل كتلةconst. والكتلتان المنفصلتان تبدأ كل منهما من 0. - هي تعدّ المواصفات، لا الأسماء. والثابتان المُصرَّح عنهما في السطر نفسه يريان قيمة
iotaنفسها. - السطر الذي يستخدم
_يُحسب أيضًا، لذا يتخطى المعرّف الفارغ قيمة. iotaثابت صحيح غير محدد النوع، لذا تعمل في أي تعبير ثابت، مثلiota + 1أو1 << iota.
كيف تبدأ iota من 1 أو تتخطى قيمة؟
أضف إليها واحدًا، أو تخلّص من القيمة الأولى باستخدام _. البدء من 1 يُبقي القيمة الصفرية حرّة لتعني «غير مضبوطة»:
example.gogotype Priority int const ( PriorityLow Priority = iota + 1 // 1 PriorityNormal // 2 PriorityHigh // 3 )
والتخطي باستخدام _ يناسب الحالات التي لا معنى فيها للقيمة الأولى. وحدات الحجم أدناه تتخطى iota == 0، لأن 1 << 0 سيكون بايتًا واحدًا:
example.gogotype ByteSize int64 const ( _ = iota // skip 0 KB ByteSize = 1 << (10 * iota) MB GB )
قيمة KB هي 1 << 10، أي 1024، و MB هي 1 << 20، و GB هي 1 << 30. وكل سطر يكرر ByteSize = 1 << (10 * iota) مع قيمة أكبر لـ iota.
كيف تعرّف رايات البت (bit flags) باستخدام iota؟
أزِح 1 إلى اليسار بمقدار iota. عندها يحصل كل ثابت على بت خاص به، وتجمع بينها باستخدام |:
example.gogopackage main import "fmt" type Permission uint8 const ( PermRead Permission = 1 << iota // 1 PermWrite // 2 PermDelete // 4 ) func main() { perm := PermRead | PermWrite fmt.Println(perm&PermWrite != 0) // true fmt.Println(perm&PermDelete != 0) // false }
وتستخدم المكتبة القياسية النمط نفسه. فالثوابت log.Ldate و log.Ltime و log.Lshortfile رايات من نوع 1 << iota تجمع بينها في log.SetFlags(log.LstdFlags | log.Lshortfile).
هل في Go تعدادات (enums)؟
ليس بوصفها ميزة مستقلة في اللغة. فلا توجد كلمة مفتاحية enum. تبني Go التعدادات من ثلاثة أجزاء موجودة فيها أصلًا: نوع مُسمّى، وكتلة const مع iota، ودالة String() كي تُطبع القيم أسماءً بدلًا من أعداد:
example.gogopackage main import "fmt" type LogLevel int const ( LevelDebug LogLevel = iota LevelInfo LevelWarn LevelError ) func (l LogLevel) String() string { switch l { case LevelDebug: return "debug" case LevelInfo: return "info" case LevelWarn: return "warn" case LevelError: return "error" default: return fmt.Sprintf("LogLevel(%d)", int(l)) } } func main() { fmt.Println(LevelWarn, LogLevel(9)) fmt.Printf("level=%v\n", LevelError) }
يطبع هذا البرنامج warn LogLevel(9) ثم level=error. تستدعي fmt الدالة String() حين تطبع قيمة باستخدام Println أو %v أو %s. وتحوّل الحالة default أي قيمة خارج القائمة، مثل LogLevel(9)، إلى شيء مقروء.
كيف تولّد دوال String باستخدام stringer؟
كتابة تلك الـ switch يدويًا مملة، وتتخلف عن الثوابت حين يضيف أحدهم ثابتًا وينسى تحديث الدالة. أداة stringer من فريق Go تولّدها تلقائيًا. أضف توجيه go:generate بجوار النوع وشغّل go generate:
example.gogopackage main import "fmt" //go:generate go run golang.org/x/tools/cmd/stringer@latest -type=OrderStatus -trimprefix=OrderStatus type OrderStatus int const ( OrderStatusUnknown OrderStatus = iota OrderStatusPending OrderStatusPaid OrderStatusShipped OrderStatusRefunded ) func main() { fmt.Println(OrderStatusPaid, OrderStatus(42)) }
يكتب go generate الملف orderstatus_string.go الذي يحتوي على دالة String()، ويطبع البرنامج Paid OrderStatus(42). ويحذف الخيار -trimprefix البادئة المشتركة من الأسماء. ويحتوي الملف المولَّد أيضًا على فحص صغير وقت الترجمة. فحين تتغير قيمة ثابت موجود دون إعادة توليد الملف، يفشل البناء بخطأ مثل invalid argument: index 4 out of bounds [0:1]. أما الثابت المضاف في نهاية القائمة فلا يكتشفه هذا الفحص، ويُطبع على شكل OrderStatus(5) إلى أن تشغّل go generate من جديد. لذلك تشغّله فرق كثيرة في CI وتُفشل البناء حين تتغير الملفات المولَّدة.
لماذا ينبغي أن تكون القيمة الصفرية للتعداد Unknown؟
لأن كل متغير في Go يبدأ بقيمته الصفرية، وهي 0 في التعداد المبني على int. فالـ struct من النوع Order الذي فُكّ ترميزه من JSON دون حقل status، أو المُصرَّح عنه بـ var o Order، تكون حالته 0. وإذا كانت 0 هي OrderStatusPending، فستبدو القيمة المفقودة كأنها طلب حقيقي قيد الانتظار. ووضع ثابت صريح OrderStatusUnknown في البداية، أو البدء من iota + 1، يجعل القيمة الصفرية تعني «غير مضبوطة»، فيستطيع الكود رفضها.
هل تتحقق Go من أن switch يغطي كل قيم التعداد؟
لا. نوع التعداد ما زال نوعًا صحيحًا عاديًا، لذا لا يعرف المترجم القيم الصالحة. فالسطر var level LogLevel = 9 يُترجم، لأن 9 ثابت غير محدد النوع يتسع في int. والـ switch التي لا تعالج إلا بعض القيم تُترجم أيضًا دون أي تحذير.
والفرق التي تريد هذا الفحص تضيف أداة الفحص exhaustive، وهي مضمّنة في golangci-lint. تبلّغ هذه الأداة عن أي switch على نوع تعداد يفوتها أحد ثوابت النوع. أما الحالة default، مثل تلك الموجودة في دالة String() أعلاه، فتعالج القيم الواقعة خارج القائمة وقت التشغيل.
متى تستخدم const ومتى تستخدم var في Go؟
استخدم const للقيم المعروفة حين تكتب الكود والتي لا تتغير أبدًا: المهل الزمنية، والحدود، وأسماء الترويسات، وقيم التعدادات. واستخدم var لأي قيمة تُحسب وقت التشغيل، أو تُقرأ من الإعدادات، أو تُبنى من نوع مركّب.
لا يمكنك أخذ عنوان ثابت، لأن الثابت لا يُخزَّن في متغير:
example.gogop := &maxBodyBytes
example.texttext./main.go:6:8: invalid operation: cannot take address of maxBodyBytes (untyped int constant 1048576)
تظهر هذه المشكلة مع واجهات API التي تأخذ مؤشرات إلى حقول اختيارية. ومنذ Go 1.26 تقبل new تعبيرًا، لذا تعيد new(maxBodyBytes) قيمة من النوع *int تشير إلى نسخة من القيمة. وفي الإصدارات الأقدم، انسخ الثابت إلى متغير أولًا ثم خذ عنوانه.
ولا يمكنك أيضًا الإسناد إلى ثابت. فالسطر maxBodyBytes = 2 << 20 يفشل بالخطأ cannot assign to maxBodyBytes (neither addressable nor a map index expression).
الثوابت غير المستخدمة تُترجم. فالمتغير المحلي غير المستخدم خطأ، declared and not used: retries، أما الثابت غير المستخدم فمقبول حتى داخل دالة. ومع ذلك تبلّغ أدوات الفحص مثل staticcheck عن الثوابت غير المُصدَّرة وغير المستخدمة على مستوى الحزمة.
جداول البحث متغيرات. فالـ map التي تربط الحالة بتسمية لا يمكن أن تكون ثابتًا، لذا صرّح عنها بـ var على مستوى الحزمة، أو أعِدها من دالة حين لا تريد أن يعدّلها كود آخر:
example.gogovar orderStatusLabels = map[OrderStatus]string{ OrderStatusPending: "Awaiting payment", OrderStatusPaid: "Paid", OrderStatusShipped: "On the way", OrderStatusRefunded: "Refunded", }
يمكن لأي كود في الحزمة أن يغيّر var على مستوى الحزمة، وهذا هو الثمن مقابل قدرتها على حمل أي نوع. وحين تكون القيم ثابتة وبسيطة، يمنحك الثابت فحوصًا وقت الترجمة ولا خطر من تغيّره وقت التشغيل. ويشرح مقال الكلمة المفتاحية var في Go المتغيرات والقيم الصفرية و:= بالتفصيل.
أين يأتي دور LevelUpGo
يعلّم LevelUpGo لغة Go عبر تمارين تشغّل كود Go حقيقيًا في المتصفح. تقدّم دورة Go Basics الكلمة const إلى جانب المتغيرات والأنواع. وفي دورة Go Language Deep Dives درس عن iota وأنماط const تبني فيه تعدادًا ورايات بت ووحدات لحجم البايتات. ويقدّم Training Ground تمارين قصيرة مستقلة للتدرّب خارج أي دورة. وللتعرّف على الكلمات المحجوزة الـ 24 الأخرى، راجع الكلمات المفتاحية في Go: شرح جميع الكلمات الـ 25.
الأسئلة الشائعة
هل const كلمة مفتاحية في Go؟
نعم. const واحدة من الكلمات المفتاحية المحجوزة الـ 25 في Go، لذا لا يمكنك استخدامها اسمًا لمتغير أو دالة أو نوع. وهي تبدأ تصريحًا عن ثابت، إما ثابتًا واحدًا const name = value أو كتلة مجمّعة const ( ... ).
هل يمكن أن تكون slice أو map ثابتًا في Go؟
لا. لا يمكن أن تكون الثوابت إلا قيمًا منطقية أو أعدادًا أو سلاسل نصية أو runes، أو أنواعًا مُسمّاة مبنية عليها. فالسطر const allowedMethods = []string{"GET", "POST"} يفشل بالخطأ is not constant. استخدم var على مستوى الحزمة للـ slices والـ maps، أو دالة تعيد نسخة جديدة حين يجب ألا يعدّل المستدعون النسخة المشتركة.
ما iota في Go؟
iota معرّف مُصرَّح عنه مسبقًا يرقّم أسطر كتلة const. قيمته 0 في السطر الأول، وتزيد بمقدار واحد في كل سطر يليه، وتعود إلى البداية في كل كتلة جديدة. ومع نوع مُسمّى والتكرار الضمني، تعطي كل ثابت في القائمة قيمته الخاصة، وهكذا تكتب Go التعدادات ورايات البت.
ما الفرق بين الثوابت المحددة النوع وغير المحددة النوع في Go؟
الثابت المحدد النوع، مثل const maxRetries int32 = 3، له نوع ثابت ويتبع قواعد الإسناد العادية. أما الثابت غير المحدد النوع، مثل const maxRetries = 3، فليس له نوع إلى أن يُستخدم، لذا يمكن أن يصبح int أو int64 أو float64 أو time.Duration. وتحتفظ الثوابت غير المحددة النوع أيضًا بقيم دقيقة وبدقة غير محدودة إلى أن تُحوَّل.
هل في Go تعدادات (enums)؟
لا توجد في Go كلمة مفتاحية enum، لكن فيها تعدادات عمليًا. تصرّح عن نوع مُسمّى مثل type OrderStatus int، وتسرد قيمه في كتلة const مع iota، وتضيف دالة String() تولّدها غالبًا أداة stringer. ولا يقصر المترجم النوع على القيم المسرودة، لذا تحقّق من صحة المدخلات واستخدم أداة الفحص exhaustive إذا كنت تحتاج إلى أن تغطي كل switch جميع القيم.
المصادر
- The Go Programming Language Specification, Constants: https://go.dev/ref/spec#Constants
- The Go Programming Language Specification, Constant declarations: https://go.dev/ref/spec#Constant_declarations
- The Go Programming Language Specification, Iota: https://go.dev/ref/spec#Iota
- The Go Programming Language Specification, Representability: https://go.dev/ref/spec#Representability
- The Go Blog, Constants (Rob Pike): https://go.dev/blog/constants
- Effective Go, Constants: https://go.dev/doc/effective_go#constants
- stringer: https://pkg.go.dev/golang.org/x/tools/cmd/stringer
- Go Code Review Comments, Mixed caps: https://go.dev/wiki/CodeReviewComments#mixed-caps
- exhaustive linter: https://github.com/nishanths/exhaustive
