Go hat kein Schlüsselwort enum. Du baust ein Enum aus zwei Teilen: einem benannten Typ und einem const-Block mit Werten dieses Typs, durchnummeriert mit iota.
Sunday ist 0, und jede folgende Zeile ist eins mehr. Der benannte Typ Weekday macht daraus ein Enum statt einer Liste von Zahlen: isWeekend sagt in seiner Signatur, was es erwartet, und dem Typ lassen sich Methoden hinzufügen. Die Ausgabe ist 1 5 6, weil Go noch nicht weiß, wie ein Weekday als Text aussieht. Das kommt weiter unten.
Wie iota funktioniert
iota ist ein Zähler, den der Compiler innerhalb eines const-Blocks bereitstellt. Zwei Regeln erklären jeden Trick, der darauf aufbaut:
iotaentspricht dem Index der aktuellen Zeile im Block, beginnend bei 0, und wird in jedem neuenconst-Block auf 0 zurückgesetzt.- Eine Zeile ohne
= expressionwiederholt Ausdruck und Typ der vorherigen Zeile, ausgewertet mit dem neueniota.
Monday oben ist also die Kurzform für Monday Weekday = iota, wobei iota jetzt 1 ist. Weil der Ausdruck wiederholt wird, kann er jeder konstante Ausdruck sein, nicht nur iota:
iota zählt Zeilen, nicht Namen: Zwei Konstanten in derselben Zeile teilen sich einen iota-Wert, wie X und Y zeigen.
Bei 1 beginnen und warum du es vielleicht lässt
Eine Variable eines Enum-Typs, die niemand gesetzt hat, enthält 0, ihren Nullwert. Ist 0 ein echter Wert wie Sunday, kannst du „der Nutzer hat Sonntag gewählt“ nicht von „das Feld wurde nie gefüllt“ unterscheiden. Drei verbreitete Lösungen:
Option 1 ist in Produktionscode am häufigsten, und aus protobuf generierte Go-Enums folgen ihr (..._UNSPECIFIED = 0). Der Nullwert bedeutet dann etwas Ehrliches.
Werte überspringen
Der Blank Identifier _ verbraucht einen iota-Wert, ohne einen Namen zu erzeugen. Damit lässt du Lücken, etwa um Nummern aus einem Protokoll zu treffen oder einen Wert auszumustern, ohne den Rest neu zu nummerieren:
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
)
Wenn eine externe Spezifikation die Nummern festlegt, wie bei diesen WebSocket-Opcodes, ist es oft klarer, sie explizit hinzuschreiben (OpClose Opcode = 8), statt Blanks zu zählen. iota ist für Werte da, deren genaue Nummern dir egal sind.
Sortier eine iota-Liste nie um und füg nichts in der Mitte ein, wenn ihre Nummern gespeichert in einer Datenbank oder einer Datei stehen oder über das Netzwerk gesendet werden. Eine Zeile in der Mitte verschiebt jeden Wert danach. Häng neue Werte ans Ende an oder vergib die Nummern explizit.
Eine String-Methode hinzufügen
Gib dem Typ eine Methode String() string, und fmt nutzt sie für %v, %s und Println:
Zwei Details in dieser Methode zählen:
- Die Bereichsprüfung. Ohne sie löst
Weekday(9).String()eine Panic wegen eines Index außerhalb des Bereichs aus, und irgendwann passiert das, weil nichts einen Aufrufer davon abhält,Weekday(9)zu erzeugen. - Das
int(d)inSprintf.dselbst mit%dzu formatieren ist in Ordnung, aber mit%vwürde es erneutString()aufrufen und rekursiv laufen, bis der Stack überläuft.
%d gibt weiterhin die Zahl aus, also bekommst du beide Formen: Wednesday is day 3.
String mit stringer erzeugen
Bei langen Listen schreibt das Tool stringer die Methode für dich:
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...
Es erzeugt weekday_string.go mit einer kompakten Implementierung von String() plus einer Prüfung zur Compilezeit, die den Build bricht, wenn sich die Konstanten ändern, ohne dass neu generiert wurde. Das Flag -linecomment nimmt einen Kommentar am Zeilenende als Namen, praktisch für Namen mit Leerzeichen.
Werte validieren
Ein Go-Enum ist nicht geschlossen. Jeder Wert des zugrunde liegenden Typs lässt sich dorthin konvertieren, und untypisierte Konstanten werden implizit konvertiert:
var d Weekday = 42 // compiles
d = Weekday(userInput) // compiles
Prüf also Werte, die von außerhalb deines Codes kommen (JSON, Datenbanken, Flags, andere Pakete):
Der nicht exportierte Wächter colorCount am Ende des Blocks hält IsValid korrekt, wenn du neue Farben anhängst, da er immer genau hinter dem letzten echten Wert steht.
switch über ein Enum
Enums werden meist von einem switch verarbeitet. Go prüft nicht, ob ein switch jeden Wert abdeckt, also füg ein default hinzu, das die Überraschung meldet:
func (c Color) Hex() string {
switch c {
case Red:
return "#ff0000"
case Green:
return "#00ff00"
case Blue:
return "#0000ff"
default:
return "#000000"
}
}
Der Linter exhaustive eines Drittanbieters (enthalten in golangci-lint) meldet switches über Enum-Typen, denen ein Case fehlt. Damit bekommst du das meiste von dem, was eine Vollständigkeitsprüfung für Enums in anderen Sprachen bietet.
Enums als Bit-Flags
Wenn Werte sich kombinieren, wie bei Berechtigungen, nimm mit 1 << iota ein Bit pro Wert:
| kombiniert Flags, & testet sie, und &^ (der AND-NOT-Operator von Go) löscht sie. Ein vorzeichenloser zugrunde liegender Typ ist hier die richtige Wahl: uint8 fasst 8 Flags, uint64 fasst 64.
String-Enums
Wird der Wert ohnehin als Text gespeichert oder gesendet, spart ein Typ auf String-Basis die Konvertierungsschicht:
type Env string
const (
EnvDev Env = "dev"
EnvStaging Env = "staging"
EnvProd Env = "prod"
)
Werte lassen sich ohne Methode String() lesbar ausgeben und serialisieren, und eine Datenbankspalte enthält "prod" statt einer Zahl, die von der Deklarationsreihenfolge abhängt. Die Kompromisse: Vergleiche sind String-Vergleiche, Bit-Flags sind unmöglich, und die Validierung bleibt trotzdem an dir hängen, da auch Env("banana") kompiliert.
Enums und JSON
Ein Ganzzahl-Enum wird standardmäßig als Zahl serialisiert. Um stattdessen Namen zu lesen und zu schreiben, implementier encoding.TextMarshaler und encoding.TextUnmarshaler. encoding/json nutzt sie für Werte und für Map-Schlüssel:
MarshalText hat einen Value Receiver und funktioniert damit sowohl auf Level als auch auf *Level; UnmarshalText braucht einen Pointer Receiver, weil es den Wert ändert. Dieselben zwei Methoden lassen den Typ auch mit TextVar aus dem Paket flag und mit den meisten Konfigurationsbibliotheken zusammenarbeiten.
Stolperfallen
- Implizite Konvertierung von Literalen. Eine Funktion, die ein
Weekdaynimmt, akzeptiert auch die untypisierte Konstante42. Nur typisierte Werte eines anderen Typs werden abgelehnt. - Den Typ in der ersten Zeile vergessen. In
const ( Red = iota; Green; Blue )sind alle drei untypisierte Ganzzahlkonstanten, keineColor-Werte, also gelten Methoden vonColorfür sie nicht. SchreibRed Color = iota, damit der wiederholte Ausdruck den Typ mitträgt. - Gespeicherte Enums umsortieren. Einen Wert in der Mitte eines
iota-Blocks einzufügen ändert still die Nummern, die anderswo schon gespeichert sind. - Rekursion in String. Formatiere in
String()den Receiver nie mit%voder%s. Konvertiere vorher in den zugrunde liegenden Typ.
Häufig gestellte Fragen
Hat Go Enums?
Nicht als Sprachfeature. Ein Schlüsselwort enum gibt es nicht. Der idiomatische Ersatz ist ein benannter Typ plus ein Block typisierter Konstanten, meist mit iota durchnummeriert:
type Color int
const (
Red Color = iota
Green
Blue
)
Der Typ liefert dir lesbare Signaturen und einen Ort für Methoden wie String(). Er hält aber niemanden davon ab, Color(42) zu schreiben, also validiere Werte, die von außen kommen.
Was ist iota in Go?
iota ist ein vordeklarierter Bezeichner, der dem Index der aktuellen Zeile (Konstantenspezifikation) in einem const-Block entspricht, beginnend bei 0. In jedem neuen const-Block wird er auf 0 zurückgesetzt. Lässt eine Zeile ihren Ausdruck weg, wiederholt Go den vorherigen Ausdruck mit dem nächsten iota, und deshalb ergibt Red = iota; Green; Blue die Werte 0, 1, 2.
Wie lasse ich iota bei 1 beginnen?
Schreib First Kind = iota + 1 in die erste Zeile oder überspring die Null mit dem Blank Identifier: _ = iota, dann First. Viele Go-Programmierer behalten stattdessen die 0 und nennen sie Unknown oder Invalid, damit eine nicht initialisierte Variable (deren Nullwert 0 ist) klar keine echte Wahl ist.
Wie gebe ich in Go ein Enum als String aus?
Gib dem Typ eine Methode String() string. fmt ruft sie für %v, %s und Println auf, also gibt fmt.Println(Green) Green statt 1 aus. Du kannst die Methode von Hand mit einem switch oder einem Array schreiben oder sie mit go run golang.org/x/tools/cmd/stringer@latest -type=Color erzeugen.
Wie konvertiere ich in Go einen String in ein Enum?
Schreib eine Parse-Funktion, die den String nachschlägt, meist in einer map[string]Color oder einem switch, und bei unbekannter Eingabe einen Fehler zurückgibt: func ParseColor(s string) (Color, error). Implementierst du UnmarshalText mit derselben Logik, nutzen JSON, Flags und Konfigurations-Loader sie automatisch.