Go non ha una parola chiave enum. Costruisci un enum con due pezzi: un tipo con nome e un blocco const di valori di quel tipo, numerati con iota.
Sunday vale 0, e ogni riga successiva vale uno in più. È il tipo con nome Weekday che rende tutto questo un enum invece di una lista di numeri: isWeekend dice nella sua firma cosa si aspetta, e al tipo si possono aggiungere metodi. L'output stampa 1 5 6 perché niente dice ancora a Go come mostrare un Weekday come testo. Ci arriviamo più avanti.
Come funziona iota
iota è un contatore che il compilatore fornisce dentro un blocco const. Due regole spiegano ogni trucco basato su di esso:
iotavale l'indice della riga corrente nel blocco, a partire da 0, e torna a 0 in ogni nuovo bloccoconst.- Una riga senza
= expressionripete l'espressione e il tipo della riga precedente, valutati con il nuovoiota.
Quindi Monday qui sopra è l'abbreviazione di Monday Weekday = iota, con iota che ora vale 1. Poiché l'espressione viene ripetuta, può essere qualsiasi espressione costante, non solo iota:
iota conta le righe, non i nomi: due costanti sulla stessa riga condividono un unico valore di iota, come mostrano X e Y.
Partire da 1, e perché potresti non volerlo
Una variabile di tipo enum che nessuno ha impostato contiene 0, il suo valore zero. Se 0 è un valore reale come Sunday, non puoi distinguere "l'utente ha scelto domenica" da "il campo non è mai stato compilato". Tre soluzioni comuni:
L'opzione 1 è la più comune nel codice di produzione, e gli enum Go generati da protobuf la seguono (..._UNSPECIFIED = 0). Il valore zero ha così un significato onesto.
Saltare valori
L'identificatore vuoto _ consuma un valore di iota senza creare un nome. Usalo per lasciare dei buchi, per esempio per rispettare numeri definiti da un protocollo o per ritirare un valore senza rinumerare gli altri:
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
)
Quando i numeri sono fissati da una specifica esterna, come per questi opcode WebSocket, scriverli esplicitamente (OpClose Opcode = 8) è spesso più chiaro che contare i vuoti. iota serve per valori di cui non ti interessano i numeri esatti.
Non riordinare né inserire mai elementi in una lista con iota i cui numeri vengono memorizzati in un database o in un file, o inviati sulla rete. Aggiungere una riga nel mezzo sposta tutti i valori successivi. Aggiungi i nuovi valori in fondo, oppure assegna i numeri esplicitamente.
Aggiungere un metodo String
Dai al tipo un metodo String() string e fmt lo userà per %v, %s e Println:
In quel metodo contano due dettagli:
- Il controllo dei limiti. Senza di esso,
Weekday(9).String()va in panic con un indice fuori intervallo, e prima o poi succederà, perché niente impedisce a un chiamante di creareWeekday(9). - L'
int(d)dentroSprintf. Formattaredstesso con%dva bene, ma formattarlo con%vchiamerebbe di nuovoString()e andrebbe in ricorsione fino all'overflow dello stack.
%d stampa comunque il numero, quindi ottieni entrambe le forme: Wednesday is day 3.
Generare String con stringer
Per liste lunghe, lo strumento stringer scrive il metodo al posto tuo:
//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...
Crea weekday_string.go con un'implementazione compatta di String(), più un controllo in fase di compilazione che rompe la build se le costanti cambiano senza rigenerare il file. Il flag -linecomment usa un commento a fine riga come nome, comodo per nomi con spazi.
Validare i valori
Un enum Go non è chiuso. Qualsiasi valore del tipo sottostante si può convertire in esso, e le costanti non tipizzate si convertono implicitamente:
var d Weekday = 42 // compiles
d = Weekday(userInput) // compiles
Quindi controlla i valori che arrivano da fuori dal tuo codice (JSON, database, flag, altri package):
La sentinella non esportata colorCount alla fine del blocco mantiene corretto IsValid quando aggiungi nuovi colori, perché si trova sempre una posizione dopo l'ultimo valore reale.
Usare uno switch su un enum
Gli enum di solito vengono usati in uno switch. Go non controlla che uno switch copra ogni valore, quindi aggiungi un default che segnali l'imprevisto:
func (c Color) Hex() string {
switch c {
case Red:
return "#ff0000"
case Green:
return "#00ff00"
case Blue:
return "#0000ff"
default:
return "#000000"
}
}
Il linter di terze parti exhaustive (incluso in golangci-lint) segnala gli switch su tipi enum a cui manca un caso, il che ti dà gran parte di ciò che offre il controllo di esaustività degli enum in altri linguaggi.
Enum come flag di bit
Quando i valori si combinano, come i permessi, usa un bit per valore con 1 << iota:
| combina i flag, & li verifica e &^ (l'operatore AND NOT di Go) li azzera. Qui la scelta giusta è un tipo sottostante senza segno: uint8 contiene 8 flag, uint64 ne contiene 64.
Enum di stringhe
Quando il valore viene comunque memorizzato o inviato come testo, un tipo basato su stringhe evita lo strato di conversione:
type Env string
const (
EnvDev Env = "dev"
EnvStaging Env = "staging"
EnvProd Env = "prod"
)
I valori si stampano e si serializzano in modo leggibile senza un metodo String(), e una colonna del database contiene "prod" invece di un numero che dipende dall'ordine di dichiarazione. I compromessi: i confronti sono confronti tra stringhe, i flag di bit sono impossibili e la validazione resta a tuo carico, perché anche Env("banana") compila.
Enum e JSON
Un enum intero viene serializzato come numero per impostazione predefinita. Per leggere e scrivere invece i nomi, implementa encoding.TextMarshaler ed encoding.TextUnmarshaler. encoding/json li usa per i valori e per le chiavi delle mappe:
MarshalText ha un receiver per valore, quindi funziona sia su Level sia su *Level; UnmarshalText ha bisogno di un receiver puntatore perché modifica il valore. Gli stessi due metodi fanno funzionare il tipo con TextVar del package flag e con la maggior parte delle librerie di configurazione.
Insidie
- Conversione implicita dei letterali. Una funzione che accetta un
Weekdayaccetta anche la costante non tipizzata42. Vengono rifiutati solo i valori tipizzati di un altro tipo. - Dimenticare il tipo sulla prima riga. In
const ( Red = iota; Green; Blue )tutte e tre sono costanti intere non tipizzate, non valoriColor, quindi i metodi diColornon si applicano a loro. ScriviRed Color = iotacosì l'espressione ripetuta porta con sé il tipo. - Riordinare enum memorizzati. Inserire un valore nel mezzo di un blocco
iotacambia silenziosamente i numeri già salvati altrove. - Ricorsione in String. Dentro
String(), non formattare mai il receiver con%vo%s. Convertilo prima nel tipo sottostante.
Domande frequenti
Go ha gli enum?
Non come funzionalità del linguaggio. Non esiste una parola chiave enum. Il sostituto idiomatico è un tipo con nome più un blocco di costanti tipizzate, di solito numerate con iota:
type Color int
const (
Red Color = iota
Green
Blue
)
Il tipo ti dà firme leggibili e un posto a cui agganciare metodi come String(). Non impedisce a nessuno di scrivere Color(42), quindi valida i valori che arrivano dall'esterno.
Cos'è iota in Go?
iota è un identificatore predichiarato che vale l'indice della riga corrente (la specifica di costante) dentro un blocco const, a partire da 0. Torna a 0 in ogni nuovo blocco const. Quando una riga omette la sua espressione, Go ripete l'espressione precedente con il valore successivo di iota, ed è per questo che Red = iota; Green; Blue produce 0, 1, 2.
Come faccio a far partire iota da 1?
Scrivi First Kind = iota + 1 sulla prima riga, oppure salta lo zero con l'identificatore vuoto: _ = iota e poi First. Molti programmatori Go preferiscono invece tenere lo 0 e chiamarlo Unknown o Invalid, così una variabile non inizializzata (il cui valore zero è 0) non è chiaramente una scelta reale.
Come stampo un enum come stringa in Go?
Dai al tipo un metodo String() string. fmt lo chiama per %v, %s e Println, quindi fmt.Println(Green) stampa Green invece di 1. Puoi scrivere il metodo a mano con uno switch o un array, oppure generarlo con go run golang.org/x/tools/cmd/stringer@latest -type=Color.
Come converto una stringa in un enum in Go?
Scrivi una funzione di parsing che cerca la stringa, di solito in una map[string]Color o con uno switch, e restituisce un errore per un input sconosciuto: func ParseColor(s string) (Color, error). Implementare UnmarshalText con la stessa logica fa sì che JSON, flag e caricatori di configurazione la usino automaticamente.