Menu

Enum en Golang : construire des enums avec const et iota

Go n'a pas de mot-clé enum. Cette page montre le remplacement idiomatique : un type nommé plus un bloc const avec iota, et comment ajouter String(), la validation, le parsing, des flags binaires et le support JSON.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Go n'a pas de mot-clé enum. Vous construisez un enum à partir de deux éléments : un type nommé, et un bloc const de valeurs de ce type, numérotées avec iota.

Sunday vaut 0, et chaque ligne suivante vaut un de plus. C'est le type nommé Weekday qui en fait un enum plutôt qu'une liste de nombres : isWeekend dit dans sa signature ce qu'elle attend, et on peut ajouter des méthodes au type. La sortie affiche 1 5 6 parce que rien n'indique encore à Go comment afficher un Weekday sous forme de texte. Cela vient plus bas.

Comment fonctionne iota

iota est un compteur que le compilateur fournit dans un bloc const. Deux règles expliquent toutes les astuces construites dessus :

  1. iota vaut l'indice de la ligne courante dans le bloc, à partir de 0, et repart à 0 dans chaque nouveau bloc const.
  2. Une ligne sans = expression répète l'expression et le type de la ligne précédente, évalués avec le nouveau iota.

Donc Monday ci-dessus est un raccourci pour Monday Weekday = iota, avec iota qui vaut maintenant 1. Comme l'expression est répétée, elle peut être n'importe quelle expression constante, pas seulement iota :

iota compte les lignes, pas les noms : deux constantes sur la même ligne partagent une seule valeur de iota, comme le montrent X et Y.

Commencer à 1, et pourquoi ne pas le faire

Une variable d'un type enum que personne n'a définie contient 0, sa valeur zéro. Si 0 est une vraie valeur comme Sunday, vous ne pouvez pas distinguer « l'utilisateur a choisi dimanche » de « le champ n'a jamais été rempli ». Trois corrections courantes :

L'option 1 est la plus courante dans le code de production, et les enums Go générés par protobuf la suivent (..._UNSPECIFIED = 0). La valeur zéro signifie alors quelque chose d'honnête.

Sauter des valeurs

L'identifiant blanc _ consomme une valeur de iota sans créer de nom. Servez-vous-en pour laisser des trous, par exemple pour correspondre à des numéros définis par un protocole ou pour retirer une valeur sans renuméroter le reste :

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
)

Quand les numéros sont fixés par une spécification externe, comme ces opcodes WebSocket, les écrire explicitement (OpClose Opcode = 8) est souvent plus clair que de compter des blancs. iota est fait pour des valeurs dont les numéros exacts vous importent peu.

Ne réordonnez jamais une liste iota et n'y insérez rien si ses numéros sont stockés dans une base de données, un fichier, ou envoyés sur le réseau. Ajouter une ligne au milieu décale toutes les valeurs suivantes. Ajoutez les nouvelles valeurs à la fin, ou attribuez les numéros explicitement.

Ajouter une méthode String

Donnez au type une méthode String() string et fmt l'utilise pour %v, %s et Println :

Deux détails de cette méthode comptent :

  • La vérification des bornes. Sans elle, Weekday(9).String() provoque un panic d'indice hors limites, et cela finira par arriver, car rien n'empêche un appelant de créer Weekday(9).
  • Le int(d) dans Sprintf. Formater d lui-même avec %d ne pose pas de problème, mais le formater avec %v appellerait de nouveau String() et récurserait jusqu'au débordement de pile.

%d affiche toujours le nombre, donc vous avez les deux formes : Wednesday is day 3.

Générer String avec stringer

Pour les longues listes, l'outil stringer écrit la méthode à votre place :

//go:generate go run golang.org/x/tools/cmd/stringer@latest -type=Weekday
go generate ./...

Il crée weekday_string.go avec une implémentation compacte de String(), plus une vérification à la compilation qui casse le build si les constantes changent sans régénération. Le flag -linecomment utilise un commentaire de fin de ligne comme nom, pratique pour des noms avec des espaces.

Valider les valeurs

Un enum Go n'est pas fermé. Toute valeur du type sous-jacent s'y convertit, et les constantes non typées s'y convertissent implicitement :

var d Weekday = 42     // compiles
d = Weekday(userInput) // compiles

Vérifiez donc les valeurs qui viennent de l'extérieur de votre code (JSON, bases de données, flags, autres packages) :

La sentinelle non exportée colorCount à la fin du bloc garde IsValid correct quand vous ajoutez de nouvelles couleurs, puisqu'elle se trouve toujours juste après la dernière vraie valeur.

Un switch sur un enum

Les enums sont généralement consommés par un switch. Go ne vérifie pas qu'un switch couvre toutes les valeurs, donc ajoutez un default qui signale la surprise :

func (c Color) Hex() string {
	switch c {
	case Red:
		return "#ff0000"
	case Green:
		return "#00ff00"
	case Blue:
		return "#0000ff"
	default:
		return "#000000"
	}
}

Le linter tiers exhaustive (inclus dans golangci-lint) signale les switchs sur des types enum qui oublient un cas, ce qui vous donne l'essentiel de ce qu'apporte la vérification d'exhaustivité des enums dans d'autres langages.

Enums en flags binaires

Quand les valeurs se combinent, comme des permissions, utilisez un bit par valeur avec 1 << iota :

| combine des flags, & les teste, et &^ (l'opérateur ET NON de Go) les efface. Un type sous-jacent non signé est le bon choix ici : uint8 contient 8 flags, uint64 en contient 64.

Enums de chaînes

Quand la valeur est de toute façon stockée ou envoyée sous forme de texte, un type basé sur string évite la couche de conversion :

type Env string

const (
	EnvDev     Env = "dev"
	EnvStaging Env = "staging"
	EnvProd    Env = "prod"
)

Les valeurs s'affichent et se sérialisent lisiblement sans méthode String(), et une colonne de base de données contient "prod" au lieu d'un nombre qui dépend de l'ordre de déclaration. Les compromis : les comparaisons sont des comparaisons de chaînes, les flags binaires sont impossibles, et la validation reste à votre charge, puisque Env("banana") compile aussi.

Enums et JSON

Un enum entier se sérialise par défaut comme un nombre. Pour lire et écrire des noms à la place, implémentez encoding.TextMarshaler et encoding.TextUnmarshaler. encoding/json les utilise pour les valeurs et pour les clés de map :

MarshalText a un receveur valeur pour fonctionner à la fois sur Level et *Level ; UnmarshalText a besoin d'un receveur pointeur parce qu'il modifie la valeur. Ces deux mêmes méthodes font fonctionner le type avec TextVar du package flag et avec la plupart des bibliothèques de configuration.

Pièges

  • Conversion implicite des littéraux. Une fonction qui prend un Weekday accepte aussi la constante non typée 42. Seules les valeurs typées d'un autre type sont rejetées.
  • Oublier le type sur la première ligne. Dans const ( Red = iota; Green; Blue ), les trois sont des constantes entières non typées, pas des valeurs Color, donc les méthodes de Color ne s'appliquent pas à elles. Écrivez Red Color = iota pour que l'expression répétée porte le type.
  • Réordonner des enums stockés. Insérer une valeur au milieu d'un bloc iota change silencieusement les numéros déjà enregistrés ailleurs.
  • Récursion dans String. Dans String(), ne formatez jamais le receveur avec %v ou %s. Convertissez d'abord vers le type sous-jacent.

Questions fréquentes

Go a-t-il des enums ?

Pas en tant que fonctionnalité du langage. Il n'y a pas de mot-clé enum. Le remplacement idiomatique est un type nommé plus un bloc de constantes typées, généralement numérotées avec iota :

type Color int

const (
	Red Color = iota
	Green
	Blue
)

Le type vous donne des signatures lisibles et un endroit où accrocher des méthodes comme String(). Il n'empêche pas quelqu'un d'écrire Color(42), donc validez les valeurs qui viennent de l'extérieur.

Qu'est-ce que iota en Go ?

iota est un identifiant prédéclaré qui vaut l'indice de la ligne courante (spécification de constante) dans un bloc const, à partir de 0. Il repart à 0 dans chaque nouveau bloc const. Quand une ligne omet son expression, Go répète l'expression précédente avec le iota suivant, ce qui fait que Red = iota; Green; Blue produit 0, 1, 2.

Comment faire commencer iota à 1 ?

Écrivez First Kind = iota + 1 sur la première ligne, ou sautez zéro avec l'identifiant blanc : _ = iota puis First. Beaucoup de développeurs Go gardent plutôt 0 et le nomment Unknown ou Invalid, pour qu'une variable non initialisée (dont la valeur zéro est 0) ne soit manifestement pas un vrai choix.

Comment afficher un enum sous forme de chaîne en Go ?

Donnez au type une méthode String() string. fmt l'appelle pour %v, %s et Println, donc fmt.Println(Green) affiche Green au lieu de 1. Vous pouvez écrire la méthode à la main avec un switch ou un tableau, ou la générer avec go run golang.org/x/tools/cmd/stringer@latest -type=Color.

Comment convertir une chaîne en enum en Go ?

Écrivez une fonction de parsing qui recherche la chaîne, généralement dans une map[string]Color ou un switch, et renvoie une erreur pour une entrée inconnue : func ParseColor(s string) (Color, error). Implémenter UnmarshalText avec la même logique permet au JSON, aux flags et aux chargeurs de configuration de l'utiliser automatiquement.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER