لا تملك Go كلمة enum. تبني التعداد من قطعتين: نوع مسمّى، وكتلة const من قيم ذلك النوع مرقّمة بـ iota.
Sunday قيمته 0، وكل سطر بعده يزيد واحدًا. النوع المسمّى Weekday هو ما يجعل هذا تعدادًا لا قائمة أرقام: تقول isWeekend في توقيعها ما تتوقّعه، ويمكن إضافة توابع إلى النوع. تطبع المخرجات 1 5 6 لأن لا شيء يخبر Go بعد كيف تعرض Weekday كنص. سيأتي ذلك لاحقًا.
كيف يعمل iota
iota عدّاد يوفّره المترجم داخل كتلة const. قاعدتان تشرحان كل حيلة مبنية عليه:
- يساوي
iotaفهرس السطر الحالي في الكتلة، بدءًا من 0، ويعود إلى 0 في كل كتلةconstجديدة. - السطر الذي ليس فيه
= expressionيكرّر تعبير السطر السابق ونوعه، مقيَّمًا بقيمةiotaالجديدة.
إذن Monday في الأعلى اختصار لـ Monday Weekday = iota، وiota الآن 1. ولأن التعبير يتكرّر، يمكن أن يكون أي تعبير ثابت، لا iota وحده:
يعدّ iota الأسطر لا الأسماء: ثابتان في السطر نفسه يتشاركان قيمة iota واحدة، كما يُظهر X وY.
البدء من 1، ولماذا قد لا تفعل
متغيّر من نوع تعداد لم يضبطه أحد يحمل 0، قيمته الصفرية. إذا كان 0 قيمة حقيقية مثل Sunday، فلا يمكنك التمييز بين "اختار المستخدم Sunday" و"لم يُملأ الحقل أبدًا". ثلاثة حلول شائعة:
الخيار 1 هو الأشيع في شيفرات الإنتاج، وتتبعه تعدادات Go المولَّدة من protobuf (..._UNSPECIFIED = 0). فتصبح القيمة الصفرية ذات معنى صادق.
تخطّي القيم
يستهلك المعرّف الفارغ _ قيمة iota دون إنشاء اسم. استخدمه لترك فجوات، مثلًا لمطابقة أرقام يحدّدها بروتوكول أو لإيقاف قيمة دون إعادة ترقيم البقية:
type Opcode byte
const (
OpContinue Opcode = iota // 0
OpText // 1
OpBinary // 2
_ // 3, reserved
_ // 4, reserved
_ // 5, reserved
_ // 6, reserved
_ // 7, reserved
OpClose // 8
OpPing // 9
OpPong // 10
)
عندما تحدّد مواصفة خارجية الأرقام، كما في رموز عمليات WebSocket هذه، فكتابتها صراحة (OpClose Opcode = 8) أوضح غالبًا من عدّ الفراغات. iota للقيم التي لا تهمّك أرقامها الدقيقة.
لا تُعِد ترتيب قائمة iota ولا تُدرج فيها إذا كانت أرقامها مخزّنة في قاعدة بيانات أو ملف أو مرسلة عبر الشبكة. إضافة سطر في المنتصف تزيح كل القيم بعده. أضف القيم الجديدة في النهاية، أو أسند الأرقام صراحة.
إضافة التابع String
أعطِ النوع تابع String() string فتستخدمه fmt مع %v و%s وPrintln:
تفصيلتان في هذا التابع مهمّتان:
- فحص الحدود. من دونه تسبّب
Weekday(9).String()panic بسبب فهرس خارج النطاق، وسيحدث ذلك عاجلًا أو آجلًا، لأن لا شيء يمنع المستدعي من إنشاءWeekday(9). - الـ
int(d)داخلSprintf. تنسيقdنفسه بـ%dلا بأس به، لكن تنسيقه بـ%vسيستدعيString()مجددًا ويدخل في عودية حتى يفيض المكدّس.
ما زال %d يطبع الرقم، فتحصل على الشكلين: Wednesday is day 3.
توليد String بأداة stringer
للقوائم الطويلة تكتب أداة stringer التابع نيابة عنك:
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...
تنشئ الملف weekday_string.go بتطبيق مختصر لـ String()، إضافة إلى فحص وقت الترجمة يكسر البناء إذا تغيّرت الثوابت دون إعادة التوليد. الخيار -linecomment يستخدم التعليق في آخر السطر اسمًا، وهذا مفيد للأسماء التي فيها مسافات.
التحقّق من القيم
التعداد في Go ليس مغلقًا. أي قيمة من النوع الأساسي تتحوّل إليه، والثوابت عديمة النوع تتحوّل ضمنيًا:
var d Weekday = 42 // compiles
d = Weekday(userInput) // compiles
لذا افحص القيم القادمة من خارج شيفرتك (JSON، قواعد البيانات، الخيارات، الحزم الأخرى):
الثابت الحارس غير المُصدَّر colorCount في نهاية الكتلة يُبقي IsValid صحيحة عندما تضيف ألوانًا جديدة، لأنه يقع دائمًا بعد آخر قيمة حقيقية بواحد.
switch على تعداد
تُستهلك التعدادات عادة عبر switch. لا تتحقّق Go من أن switch يغطّي كل قيمة، لذا أضف default يبلغ عن المفاجأة:
func (c Color) Hex() string {
switch c {
case Red:
return "#ff0000"
case Green:
return "#00ff00"
case Blue:
return "#0000ff"
default:
return "#000000"
}
}
أداة الفحص الخارجية exhaustive (المضمّنة في golangci-lint) تبلغ عن عبارات switch على أنواع التعداد التي تفوّت حالة، وهذا يعطيك معظم ما يقدّمه فحص شمول التعدادات في لغات أخرى.
تعدادات أعلام البتات
عندما تتجمّع القيم، مثل الصلاحيات، استخدم بتًا واحدًا لكل قيمة بـ 1 << iota:
يجمع | الأعلام، ويفحصها &، ويمسحها &^ (معامل AND NOT في Go). النوع الأساسي عديم الإشارة هو الخيار الصحيح هنا: uint8 يتّسع لـ 8 أعلام، وuint64 لـ 64.
تعدادات نصية
عندما تُخزَّن القيمة أو تُرسل كنص في كل الأحوال، يتجنّب النوع المبني على النص طبقة التحويل:
type Env string
const (
EnvDev Env = "dev"
EnvStaging Env = "staging"
EnvProd Env = "prod"
)
تُطبع القيم وتُسلسَل بشكل مقروء دون تابع String()، ويحمل عمود قاعدة البيانات "prod" بدل رقم يعتمد على ترتيب التعريف. المقابل: المقارنات مقارنات نصوص، وأعلام البتات مستحيلة، والتحقّق ما زال عليك، لأن Env("banana") تُترجم أيضًا.
التعدادات وJSON
يُسلسَل التعداد الصحيح كرقم افتراضيًا. لقراءة الأسماء وكتابتها بدلًا من ذلك، طبّق encoding.TextMarshaler وencoding.TextUnmarshaler. تستخدمهما encoding/json للقيم ولمفاتيح الخرائط:
لـ MarshalText مستقبِل قيمة فتعمل على Level و*Level كليهما؛ أما UnmarshalText فتحتاج مستقبِل مؤشر لأنها تغيّر القيمة. التابعان نفساهما يجعلان النوع يعمل مع TextVar في الحزمة flag ومع معظم مكتبات الإعدادات.
مزالق
- التحويل الضمني للحرفيات. الدالة التي تأخذ
Weekdayتقبل أيضًا الثابت عديم النوع42. القيم ذات النوع من نوع آخر وحدها تُرفض. - نسيان النوع في السطر الأول. في
const ( Red = iota; Green; Blue )الثلاثة ثوابت صحيحة عديمة النوع، لا قيمColor، فلا تنطبق عليها توابعColor. اكتبRed Color = iotaليحمل التعبير المكرّر النوع. - إعادة ترتيب التعدادات المخزّنة. إدراج قيمة في منتصف كتلة
iotaيغيّر بصمت الأرقام المحفوظة في مكان آخر. - العودية في String. داخل
String()لا تنسّق المستقبِل بـ%vأو%sأبدًا. حوّله إلى النوع الأساسي أولًا.
الأسئلة الشائعة
هل تملك Go تعدادات enum؟
ليس كميزة في اللغة. لا توجد كلمة enum. البديل المعتاد نوع مسمّى مع كتلة من الثوابت ذات النوع، تُرقَّم عادة بـ iota:
type Color int
const (
Red Color = iota
Green
Blue
)
يمنحك النوع توقيعات مقروءة ومكانًا تعلّق عليه توابع مثل String(). لكنه لا يمنع أحدًا من كتابة Color(42)، لذا تحقّق من القيم القادمة من الخارج.
ما هو iota في Go؟
iota معرّف مُعرَّف مسبقًا يساوي فهرس السطر الحالي (مواصفة الثابت) داخل كتلة const، بدءًا من 0. ويعود إلى 0 في كل كتلة const جديدة. عندما يحذف سطر تعبيره، تكرّر Go التعبير السابق مع قيمة iota التالية، وهذا ما يجعل Red = iota; Green; Blue تنتج 0 و1 و2.
كيف أجعل iota تبدأ من 1؟
اكتب First Kind = iota + 1 في السطر الأول، أو تخطَّ الصفر بالمعرّف الفارغ: _ = iota ثم First. لكن كثيرًا من مبرمجي Go يُبقون الصفر ويسمّونه Unknown أو Invalid، فيكون المتغيّر غير المهيّأ (وقيمته الصفرية 0) واضحًا أنه ليس اختيارًا حقيقيًا.
كيف أطبع قيمة enum كنص في Go؟
أعطِ النوع تابع String() string. تستدعيه fmt مع %v و%s وPrintln، فتطبع fmt.Println(Green) الكلمة Green بدل 1. يمكنك كتابة التابع يدويًا بـ switch أو مصفوفة، أو توليده بـ go run golang.org/x/tools/cmd/stringer@latest -type=Color.
كيف أحوّل نصًا إلى enum في Go؟
اكتب دالة تحليل تبحث عن النص، عادة في map[string]Color أو switch، وتعيد خطأ للمدخلات المجهولة: func ParseColor(s string) (Color, error). وتطبيق UnmarshalText بالمنطق نفسه يجعل JSON والخيارات ومحمّلات الإعدادات تستخدمه تلقائيًا.