Menu

Golang Enum: Enums mit const und iota bauen

Go hat kein Schlüsselwort enum. Diese Seite zeigt den idiomatischen Ersatz, einen benannten Typ plus einen const-Block mit iota, und wie du String(), Validierung, Parsen, Bit-Flags und JSON-Unterstützung hinzufügst.

Diese Seite enthält ausführbare Editoren - bearbeiten, ausführen und Ausgabe sofort sehen.

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:

  1. iota entspricht dem Index der aktuellen Zeile im Block, beginnend bei 0, und wird in jedem neuen const-Block auf 0 zurückgesetzt.
  2. Eine Zeile ohne = expression wiederholt Ausdruck und Typ der vorherigen Zeile, ausgewertet mit dem neuen iota.

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) in Sprintf. d selbst mit %d zu formatieren ist in Ordnung, aber mit %v würde es erneut String() 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 Weekday nimmt, akzeptiert auch die untypisierte Konstante 42. 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, keine Color-Werte, also gelten Methoden von Color für sie nicht. Schreib Red 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 %v oder %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.

Coddy programming languages illustration

Lerne mit Coddy zu programmieren

LOS GEHT'S