ל-Go אין מילת מפתח enum. בונים enum משני חלקים: טיפוס בעל שם, ובלוק const של ערכים מאותו טיפוס, ממוספרים עם iota.
Sunday הוא 0, וכל שורה אחריו גדולה באחד. הטיפוס בעל השם Weekday הוא מה שהופך את זה ל-enum ולא לרשימת מספרים: 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, ולמה אולי לא כדאי
משתנה מטיפוס enum שאף אחד לא הציב בו ערך מחזיק 0, ערך האפס שלו. אם 0 הוא ערך אמיתי כמו Sunday, אי אפשר להבחין בין "המשתמש בחר ביום ראשון" ל"השדה אף פעם לא מולא". שלושה פתרונות נפוצים:
אפשרות 1 היא הנפוצה ביותר בקוד production, ו-enums של 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
)
כשהמספרים נקבעים על ידי מפרט חיצוני, כמו ב-opcodes האלה של WebSocket, כתיבה מפורשת שלהם (OpClose Opcode = 8) ברורה לעיתים קרובות יותר מספירת מזהים ריקים. iota מיועד לערכים שהמספרים המדויקים שלהם לא משנים לכם.
אף פעם אל תשנו סדר ואל תכניסו ערכים לרשימת iota שהמספרים שלה נשמרים במסד נתונים, בקובץ, או נשלחים ברשת. הוספת שורה באמצע מזיזה כל ערך שאחריה. הוסיפו ערכים חדשים בסוף, או הציבו מספרים במפורש.
הוספת מתודת String
תנו לטיפוס מתודת String() string ו-fmt תשתמש בה עבור %v, %s ו-Println:
שני פרטים במתודה הזאת חשובים:
- בדיקת הגבולות. בלעדיה,
Weekday(9).String()נכנסת ל-panic עם index out of range, וזה יקרה בסופו של דבר, כי שום דבר לא מונע ממי שקורא ליצור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(), ועוד בדיקה בזמן קומפילציה ששוברת את הבנייה אם הקבועים משתנים בלי לייצר מחדש. ה-flag -linecomment משתמש בהערה שבסוף השורה כשם, מה שנוח לשמות עם רווחים.
ולידציה של ערכים
enum ב-Go אינו סגור. כל ערך של טיפוס הבסיס ניתן להמרה אליו, וקבועים בלי טיפוס מומרים באופן מרומז:
var d Weekday = 42 // compiles
d = Weekday(userInput) // compiles
לכן בדקו ערכים שמגיעים מחוץ לקוד שלכם (JSON, מסדי נתונים, flags, חבילות אחרות):
ה-sentinel הלא מיוצא colorCount בסוף הבלוק שומר על IsValid נכונה כשמוסיפים צבעים חדשים, כי הוא תמיד נמצא מקום אחד אחרי הערך האמיתי האחרון.
switch על enum
enums נצרכים בדרך כלל על ידי 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"
}
}
ה-linter החיצוני exhaustive (שכלול ב-golangci-lint) מדווח על switches על טיפוסי enum שחסר בהם מקרה, וזה נותן לכם את רוב מה שבדיקת שלמות של enums נותנת בשפות אחרות.
enums של דגלי ביטים
כשערכים משתלבים, כמו הרשאות, השתמשו בביט אחד לכל ערך עם 1 << iota:
| משלב דגלים, & בודק אותם, ו-&^ (אופרטור ה-AND NOT של Go) מנקה אותם. טיפוס בסיס לא מסומן הוא הבחירה הנכונה כאן: uint8 מחזיק 8 דגלים, uint64 מחזיק 64.
enums של מחרוזות
כשהערך נשמר או נשלח כטקסט בכל מקרה, טיפוס שמבוסס על מחרוזת חוסך את שכבת ההמרה:
type Env string
const (
EnvDev Env = "dev"
EnvStaging Env = "staging"
EnvProd Env = "prod"
)
הערכים מודפסים ומוסדרים (serialized) בצורה קריאה בלי מתודת String(), ועמודה במסד הנתונים מחזיקה "prod" במקום מספר שתלוי בסדר ההצהרה. המחיר: השוואות הן השוואות מחרוזות, דגלי ביטים בלתי אפשריים, והוולידציה עדיין עליכם, כי גם Env("banana") מתקמפל.
Enums ו-JSON
enum של מספר שלם מוסדר כמספר כברירת מחדל. כדי לקרוא ולכתוב שמות במקום, ממשו את encoding.TextMarshaler ואת encoding.TextUnmarshaler. encoding/json משתמשת בהם עבור ערכים ועבור מפתחות של map:
ל-MarshalText יש receiver מסוג ערך, כך שהיא עובדת גם על Level וגם על *Level; UnmarshalText צריכה receiver מסוג מצביע כי היא משנה את הערך. אותן שתי מתודות גורמות לטיפוס לעבוד עם TextVar של חבילת flag ועם רוב ספריות הקונפיגורציה.
מלכודות
- המרה מרומזת של ליטרלים. פונקציה שמקבלת
Weekdayמקבלת גם את הקבוע בלי הטיפוס42. רק ערכים עם טיפוס אחר נדחים. - שכחת הטיפוס בשורה הראשונה. ב-
const ( Red = iota; Green; Blue )שלושתם קבועים שלמים בלי טיפוס, לא ערכיColor, ולכן מתודות שלColorלא חלות עליהם. כתבוRed Color = iotaכדי שהביטוי החוזר יישא את הטיפוס. - שינוי סדר של enums שנשמרים. הכנסת ערך באמצע בלוק
iotaמשנה בשקט את המספרים שכבר נשמרו במקום אחר. - רקורסיה ב-String. בתוך
String(), אף פעם אל תעצבו את ה-receiver עם%vאו%s. המירו קודם לטיפוס הבסיס.
שאלות נפוצות
האם יש enums ב-Go?
לא כתכונה של השפה. אין מילת מפתח 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 מעדיפים לשמור את ה-0 ולקרוא לו 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, ל-flags ולטועני קונפיגורציה להשתמש בה אוטומטית.