Menu

Enum in Golang: come costruire enum con const e iota

Go non ha una parola chiave enum. Questa pagina mostra il sostituto idiomatico: un tipo con nome più un blocco const con iota, e come aggiungere String(), validazione, parsing, flag di bit e supporto JSON.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

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:

  1. iota vale l'indice della riga corrente nel blocco, a partire da 0, e torna a 0 in ogni nuovo blocco const.
  2. Una riga senza = expression ripete l'espressione e il tipo della riga precedente, valutati con il nuovo iota.

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 creare Weekday(9).
  • L'int(d) dentro Sprintf. Formattare d stesso con %d va bene, ma formattarlo con %v chiamerebbe di nuovo String() 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 Weekday accetta anche la costante non tipizzata 42. 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 valori Color, quindi i metodi di Color non si applicano a loro. Scrivi Red Color = iota così l'espressione ripetuta porta con sé il tipo.
  • Riordinare enum memorizzati. Inserire un valore nel mezzo di un blocco iota cambia silenziosamente i numeri già salvati altrove.
  • Ricorsione in String. Dentro String(), non formattare mai il receiver con %v o %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.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA