Go nie ma słowa kluczowego enum. Enum buduje się z dwóch elementów: nazwanego typu i bloku const z wartościami tego typu, numerowanymi przez iota.
Sunday to 0, a każda kolejna linia to o jeden więcej. To nazwany typ Weekday sprawia, że jest to enum, a nie lista liczb: isWeekend mówi w swojej sygnaturze, czego oczekuje, a do typu można dodawać metody. Wynik to 1 5 6, bo nic jeszcze nie mówi Go, jak pokazać Weekday jako tekst. Do tego dojdziemy niżej.
Jak działa iota
iota to licznik, który kompilator udostępnia w bloku const. Dwie zasady wyjaśniają każdą opartą na nim sztuczkę:
iotarówna się indeksowi bieżącej linii w bloku, licząc od 0, i resetuje się do 0 w każdym nowym blokuconst.- Linia bez
= expressionpowtarza wyrażenie i typ z poprzedniej linii, obliczone z nową wartościąiota.
Zatem Monday powyżej to skrót od Monday Weekday = iota, gdzie iota wynosi już 1. Ponieważ wyrażenie jest powtarzane, może to być dowolne wyrażenie stałe, nie tylko iota:
iota liczy linie, nie nazwy: dwie stałe w tej samej linii dzielą jedną wartość iota, co pokazują X i Y.
Start od 1 i dlaczego czasem lepiej nie
Zmienna typu enum, której nikt nie ustawił, przechowuje 0, czyli swoją wartość zerową. Jeśli 0 to prawdziwa wartość, jak Sunday, nie odróżnisz sytuacji "użytkownik wybrał niedzielę" od "pole nigdy nie zostało wypełnione". Trzy typowe rozwiązania:
Opcja 1 jest najczęstsza w kodzie produkcyjnym i stosują ją enumy Go generowane z protobuf (..._UNSPECIFIED = 0). Wartość zerowa oznacza wtedy coś uczciwego.
Pomijanie wartości
Identyfikator pusty _ zużywa wartość iota bez tworzenia nazwy. Używaj go, żeby zostawić luki, na przykład aby dopasować się do numerów zdefiniowanych przez protokół albo wycofać wartość bez przenumerowania pozostałych:
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
)
Gdy numery narzuca zewnętrzna specyfikacja, jak w przypadku tych opcode'ów WebSocket, jawne ich wypisanie (OpClose Opcode = 8) jest często czytelniejsze niż liczenie pustych identyfikatorów. iota jest dla wartości, których dokładne numery nie mają znaczenia.
Nigdy nie zmieniaj kolejności ani nie wstawiaj niczego do listy iota, której numery są zapisywane w bazie danych, w pliku albo wysyłane przez sieć. Dodanie linii w środku przesuwa każdą kolejną wartość. Nowe wartości dopisuj na końcu albo przypisuj numery jawnie.
Dodawanie metody String
Dodaj do typu metodę String() string, a fmt użyje jej dla %v, %s i Println:
W tej metodzie ważne są dwa szczegóły:
- Sprawdzenie zakresu. Bez niego
Weekday(9).String()wywoła panic z indeksem poza zakresem, i prędzej czy później to się stanie, bo nic nie powstrzymuje wywołującego przed utworzeniemWeekday(9). int(d)wewnątrzSprintf. Formatowanie samegodprzez%djest w porządku, ale formatowanie przez%vwywołałoby ponownieString()i rekurencję aż do przepełnienia stosu.
%d nadal wypisuje liczbę, więc masz obie formy: Wednesday is day 3.
Generowanie String przez stringer
Przy długich listach narzędzie stringer napisze metodę za ciebie:
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...
Tworzy plik weekday_string.go ze zwięzłą implementacją String() oraz kontrolą w czasie kompilacji, która psuje build, jeśli stałe się zmienią bez ponownego wygenerowania. Flaga -linecomment używa komentarza na końcu linii jako nazwy, co przydaje się przy nazwach ze spacjami.
Walidacja wartości
Enum w Go nie jest zamknięty. Każda wartość typu bazowego da się na niego skonwertować, a stałe beztypowe konwertują się niejawnie:
var d Weekday = 42 // compiles
d = Weekday(userInput) // compiles
Dlatego sprawdzaj wartości, które przychodzą spoza twojego kodu (JSON, bazy danych, flagi, inne pakiety):
Nieeksportowany wartownik colorCount na końcu bloku sprawia, że IsValid pozostaje poprawne po dopisaniu nowych kolorów, bo zawsze znajduje się o jeden za ostatnią prawdziwą wartością.
Switch na enumie
Enumy zwykle obsługuje się przez switch. Go nie sprawdza, czy switch obejmuje każdą wartość, więc dodaj default, który zgłosi niespodziankę:
func (c Color) Hex() string {
switch c {
case Red:
return "#ff0000"
case Green:
return "#00ff00"
case Blue:
return "#0000ff"
default:
return "#000000"
}
}
Zewnętrzny linter exhaustive (dołączony do golangci-lint) zgłasza switche na typach enum, w których brakuje przypadku, co daje większość tego, co w innych językach zapewnia sprawdzanie kompletności enumów.
Enumy jako flagi bitowe
Gdy wartości się łączą, jak uprawnienia, użyj jednego bitu na wartość przez 1 << iota:
| łączy flagi, & je sprawdza, a &^ (operator AND NOT w Go) je czyści. Właściwym wyborem jest tu typ bazowy bez znaku: uint8 mieści 8 flag, uint64 mieści 64.
Enumy tekstowe
Gdy wartość i tak jest zapisywana lub wysyłana jako tekst, typ oparty na stringu pozwala uniknąć warstwy konwersji:
type Env string
const (
EnvDev Env = "dev"
EnvStaging Env = "staging"
EnvProd Env = "prod"
)
Wartości wypisują się i serializują czytelnie bez metody String(), a kolumna w bazie danych przechowuje "prod" zamiast liczby zależnej od kolejności deklaracji. Koszty: porównania to porównania stringów, flagi bitowe są niemożliwe, a walidacja nadal spada na ciebie, bo Env("banana") też się kompiluje.
Enumy i JSON
Enum całkowity domyślnie serializuje się jako liczba. Żeby zamiast tego czytać i zapisywać nazwy, zaimplementuj encoding.TextMarshaler i encoding.TextUnmarshaler. encoding/json używa ich dla wartości i dla kluczy map:
MarshalText ma odbiorcę wartościowego, więc działa zarówno na Level, jak i na *Level; UnmarshalText potrzebuje odbiorcy wskaźnikowego, bo zmienia wartość. Te same dwie metody sprawiają, że typ działa z TextVar z pakietu flag i z większością bibliotek konfiguracyjnych.
Pułapki
- Niejawna konwersja literałów. Funkcja przyjmująca
Weekdayprzyjmie też beztypową stałą42. Odrzucane są tylko typowane wartości innego typu. - Pominięcie typu w pierwszej linii. W
const ( Red = iota; Green; Blue )wszystkie trzy to beztypowe stałe całkowite, a nie wartościColor, więc metodyColorich nie dotyczą. NapiszRed Color = iota, żeby powtarzane wyrażenie niosło typ. - Zmiana kolejności zapisywanych enumów. Wstawienie wartości w środek bloku
iotapo cichu zmienia numery, które są już zapisane gdzie indziej. - Rekurencja w String. Wewnątrz
String()nigdy nie formatuj odbiorcy przez%vani%s. Najpierw skonwertuj go na typ bazowy.
Najczęściej zadawane pytania
Czy Go ma enumy?
Nie jako funkcję języka. Nie ma słowa kluczowego enum. Idiomatycznym zamiennikiem jest nazwany typ plus blok stałych typowanych, zwykle numerowanych przez iota:
type Color int
const (
Red Color = iota
Green
Blue
)
Typ daje czytelne sygnatury i miejsce na metody takie jak String(). Nie powstrzymuje nikogo przed napisaniem Color(42), więc sprawdzaj wartości, które przychodzą z zewnątrz.
Czym jest iota w Go?
iota to predeklarowany identyfikator równy indeksowi bieżącej linii (specyfikacji stałej) w bloku const, licząc od 0. Resetuje się do 0 w każdym nowym bloku const. Gdy linia pomija wyrażenie, Go powtarza poprzednie wyrażenie z kolejną wartością iota, i właśnie dlatego Red = iota; Green; Blue daje 0, 1, 2.
Jak sprawić, żeby iota zaczynała od 1?
Napisz First Kind = iota + 1 w pierwszej linii albo pomiń zero identyfikatorem pustym: _ = iota, a potem First. Wielu programistów Go zamiast tego zostawia 0 i nazywa je Unknown lub Invalid, żeby niezainicjalizowana zmienna (której wartość zerowa to 0) wyraźnie nie była prawdziwym wyborem.
Jak wypisać enum jako string w Go?
Dodaj do typu metodę String() string. fmt wywołuje ją dla %v, %s i Println, więc fmt.Println(Green) wypisuje Green zamiast 1. Metodę możesz napisać ręcznie ze switch lub tablicą albo wygenerować przez go run golang.org/x/tools/cmd/stringer@latest -type=Color.
Jak zamienić string na enum w Go?
Napisz funkcję parsującą, która wyszukuje string, zwykle w map[string]Color lub w switch, i zwraca błąd dla nieznanych danych: func ParseColor(s string) (Color, error). Implementacja UnmarshalText z tą samą logiką sprawia, że JSON, flagi i loadery konfiguracji używają jej automatycznie.