Retour au blog

Le mot-clé const en Go : constantes non typées, iota et enums

Comment fonctionne le mot-clé const en Go : déclarations groupées, ce qui peut être une constante, constantes typées ou non typées, iota, enums avec méthode String et stringer, et const ou var.

Le mot-clé const en Go : constantes non typées, iota et enums

Le mot-clé const déclare un nom pour une valeur que le compilateur connaît au moment du build, comme un timeout, un nom d'en-tête ou une taille maximale. Une constante Go ne peut contenir qu'un booléen, un nombre, une chaîne ou un rune, et elle ne peut jamais changer pendant l'exécution du programme. La plupart des constantes sont non typées : elles n'ont pas de type fixe tant que vous ne les utilisez pas, si bien que const maxBodyBytes = 1 << 20 fonctionne comme un int, un int64 ou un float64 sans conversion. Dans un bloc const, l'identifiant prédéclaré iota compte à partir de 0, et c'est ainsi que Go écrit les enums (spécification Go).

En bref

  • const name = value déclare une constante. Un bloc const ( ... ) en déclare plusieurs, et la plupart du code Go regroupe ainsi les constantes liées entre elles.
  • Les constantes peuvent être des booléens, des nombres, des chaînes ou des runes. Les slices, les maps, les structs et le résultat d'un appel de fonction comme time.Now() ne peuvent pas être des constantes.
  • Une constante non typée comme 5 ou "GET" prend le type dont son contexte a besoin. C'est pour cette raison que 5 * time.Second compile, alors que timeout * time.Second avec une variable int ne compile pas.
  • Les constantes non typées sont exactes. Le compilateur effectue leurs calculs en précision arbitraire et ne se plaint que lorsqu'une valeur ne tient pas dans le type qu'elle finit par prendre.
  • iota démarre à 0 dans chaque bloc const et augmente de un à chaque ligne. _ saute une valeur, iota + 1 démarre à 1 et 1 << iota construit des flags de bits.
  • Go n'a pas de mot-clé enum. Un enum, c'est un type nommé, un bloc const avec iota et généralement une méthode String(), que l'outil stringer peut générer.
  • Vous ne pouvez ni prendre l'adresse d'une constante ni lui affecter une valeur. Les valeurs calculées à l'exécution, et les tables de correspondance comme les maps, relèvent d'un var.

Comment déclarer une constante en Go ?

Écrivez const, un nom et une valeur. Pour plusieurs constantes liées, utilisez un bloc groupé entre parenthèses :

example.gogo
package main

import (
	"fmt"
	"net/http"
	"time"
)

const maxBodyBytes = 1 << 20 // 1 MiB

const (
	readTimeout  = 5 * time.Second
	writeTimeout = 10 * time.Second
	idleTimeout  = 2 * time.Minute
)

const (
	headerRequestID  = "X-Request-ID"
	headerRetryAfter = "Retry-After"
)

func main() {
	srv := &http.Server{
		Addr:         ":8080",
		ReadTimeout:  readTimeout,
		WriteTimeout: writeTimeout,
		IdleTimeout:  idleTimeout,
	}
	fmt.Println(srv.ReadTimeout, srv.WriteTimeout, srv.IdleTimeout)
}

Ce programme affiche 5s 10s 2m0s. Pour des valeurs simples comme celles-ci, le regroupement ne sert qu'aux lecteurs. Chaque nom du bloc est une constante indépendante, et le bloc réunit les timeouts du serveur à un seul endroit, où un relecteur peut les comparer.

Les constantes suivent les mêmes règles de portée et de visibilité que les variables. Une constante déclarée au niveau du package est visible dans tous les fichiers du package, et elle est exportée si son nom commence par une majuscule, comme http.StatusNotFound ou time.RFC3339. Le mot-clé package en Go explique comment les noms exportés fonctionnent d'un package à l'autre. Une constante déclarée dans une fonction n'existe que dans cette fonction. Le style Go utilise le mixed caps pour les constantes comme pour tout autre nom : l'orthographe idiomatique est donc maxBodyBytes ou MaxBodyBytes, pas MAX_BODY_BYTES.

Un bloc const obéit à une règle qu'un bloc var n'a pas. Quand une ligne comporte un nom mais ni type ni valeur, elle reprend le type et l'expression de la ligne précédente. Seule, cette règle ne fait que donner la même valeur à plusieurs noms, ce qui est rarement le but recherché. Combinée à iota, elle permet à une seule expression de numéroter toute une liste de constantes.

Quelles valeurs peuvent être des constantes en Go ?

Uniquement les booléens, les nombres, les chaînes et les runes. Les types numériques sont les entiers, les nombres à virgule flottante et les nombres complexes, et un rune est un entier qui représente un point de code Unicode. Tout le reste échoue à la compilation, y compris une valeur fixe en pratique mais qui doit être calculée à l'exécution :

example.gogo
package main

import "time"

const startedAt = time.Now()
example.texttext
./main.go:5:19: time.Now() (value of struct type time.Time) is not constant

Les valeurs composites sont rejetées de la même façon. const allowedMethods = []string{"GET", "POST"} échoue avec []string{…} (value of type []string) is not constant. Les slices, les maps, les structs, les pointeurs, les channels et les fonctions ne peuvent pas être des constantes, même quand chaque élément est un littéral.

Les types nommés construits sur un nombre, une chaîne ou un booléen sont autorisés. time.Duration est défini comme un int64, donc const pollInterval = 500 * time.Millisecond est une constante de type time.Duration de valeur 500ms. Il en va de même pour vos propres types, comme type LogLevel int, qui sert de base aux enums.

Quelques fonctions intégrées produisent aussi des constantes quand leur argument le permet. len d'un tableau est une constante, car la longueur d'un tableau fait partie de son type. len("Retry-After") sur une constante chaîne est aussi une constante. unsafe.Sizeof, unsafe.Alignof et unsafe.Offsetof sont des constantes dès que le type de l'argument a une taille fixe : const headerSize = unsafe.Sizeof(int64(0)) compile donc et vaut 8. len d'un slice n'est pas une constante, puisque la longueur d'un slice n'est connue qu'à l'exécution.

Quelle est la différence entre constantes typées et non typées en Go ?

Une constante typée indique un type dans sa déclaration. Une constante non typée n'en indique pas, et elle prend le type dont le code environnant a besoin :

example.gogo
const maxRetries int32 = 3   // typed: always an int32
const maxBodyBytes = 1 << 20 // untyped: becomes whatever type it is used as

La constante typée suit les règles d'affectation habituelles, elle ne peut donc pas aller dans un int sans conversion :

example.gogo
var attempts int = maxRetries
example.texttext
./main.go:6:21: cannot use maxRetries (constant 3 of type int32) as int value in variable declaration

La constante non typée s'insère aussi bien dans var limit int64 = maxBodyBytes que dans var limit int = maxBodyBytes et float64(maxBodyBytes). La bibliothèque standard s'appuie sur cette souplesse et laisse la plupart de ses constantes numériques non typées, comme math.MaxInt64 et math.Pi.

Une constante non typée a tout de même un type par défaut. Il s'applique quand rien d'autre ne détermine le type, comme dans x := 1.5 ou quand la constante est passée à fmt.Println :

Constante non typéeType par défaut
true, falsebool
42, 1 << 20int
1.5, 1e9float64
'a'rune (un alias de int32)
2icomplex128
"GET"string

Pourquoi 5 * time.Second compile mais pas timeout * time.Second ?

Parce que 5 est une constante non typée et que timeout est une variable. Go ne convertit jamais de lui-même d'un type numérique à un autre, mais une constante non typée peut prendre n'importe quel type numérique dans lequel sa valeur tient. Dans 5 * time.Second, le 5 devient un time.Duration, et le résultat est un time.Duration de cinq secondes.

Une variable a déjà un type, donc la même multiplication échoue quand le nombre provient de la configuration :

example.gogo
package main

import (
	"net/http"
	"os"
	"strconv"
	"time"
)

func main() {
	timeout, _ := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS"))
	client := &http.Client{Timeout: timeout * time.Second}
	_ = client
}
example.texttext
./main.go:12:34: invalid operation: timeout * time.Second (mismatched types int and "time".Duration)

timeout est un int, et time.Second est un time.Duration. La solution est une conversion explicite, time.Duration(timeout) * time.Second. Une constante non typée de même valeur fonctionne sans conversion : const retryDelay = 3 suivi de retryDelay * time.Second donne 3s.

Quelle est la précision des constantes non typées ?

Les constantes numériques non typées sont exactes. La spécification impose aux compilateurs de représenter les constantes entières sur au moins 256 bits et de conserver les constantes à virgule flottante avec une mantisse d'au moins 256 bits, et le compilateur Go standard va jusqu'à 512 bits. math.Pi est écrit avec plus de chiffres qu'un float64 ne peut en contenir, et il n'est arrondi qu'au moment où il atterrit dans une variable float64 ou float32.

Cette précision vous permet d'écrire des valeurs intermédiaires trop grandes pour n'importe quel type Go, tant que le résultat final tient. const maxOffset = 1 << 100 compile, et maxOffset >> 98 est la constante 4. L'erreur n'arrive que lorsque la valeur doit devenir un type concret :

example.gogo
package main

import "fmt"

const maxOffset = 1 << 100

func main() {
	fmt.Println(maxOffset)
}
example.texttext
./main.go:8:14: cannot use maxOffset (untyped int constant 1267650600228229401496703205376) as int value in argument to fmt.Println (overflows)

Comme le compilateur connaît la valeur de chaque constante, ces vérifications de plage ont lieu au build au lieu de provoquer un dépassement silencieux à l'exécution. Quelques exemples supplémentaires :

CodeErreur du compilateur
var uploadLimit int32 = 1 << 40cannot use 1 << 40 (untyped int constant 1099511627776) as int32 value in variable declaration (overflows)
const maxConns uint8 = 300cannot use 300 (untyped int constant) as uint8 value in constant declaration (overflows)
var workers int = 2.5cannot use 2.5 (untyped float constant) as int value in variable declaration (truncated)

La vérification ne porte que sur la valeur. var workers int = 2.0 compile, car 2.0 est une constante flottante non typée dont la valeur est un nombre entier.

Qu'est-ce que iota en Go ?

iota est un identifiant prédéclaré qui n'a de sens qu'à l'intérieur d'une déclaration const. Il vaut 0 pour la première spécification de constante d'un bloc const et augmente de un à chaque spécification suivante. Une spécification est une ligne qui déclare des noms, donc les lignes vides et les commentaires ne comptent pas. Combinés à la répétition implicite de l'expression précédente, un type et un seul iota suffisent pour numéroter toute une liste :

example.gogo
type LogLevel int

const (
	LevelDebug LogLevel = iota // 0
	LevelInfo                  // 1
	LevelWarn                  // 2
	LevelError                 // 3
)

LevelInfo, LevelWarn et LevelError n'ont ni type ni valeur propres : chacune reprend donc LogLevel = iota avec la valeur suivante de iota. Toutes les constantes de la liste sont de type LogLevel.

Comment iota compte :

  1. iota repart à 0 au début de chaque bloc const. Deux blocs distincts commencent tous deux à 0.
  2. Il compte les spécifications, pas les noms. Deux constantes déclarées sur la même ligne voient le même iota.
  3. Une ligne qui utilise _ compte quand même, donc l'identifiant vide saute une valeur.
  4. iota est une constante entière non typée, il fonctionne donc dans n'importe quelle expression constante, comme iota + 1 ou 1 << iota.

Comment faire démarrer iota à 1 ou sauter une valeur ?

Ajoutez-lui un, ou jetez la première valeur avec _. Démarrer à 1 laisse la valeur zéro libre pour signifier « non défini » :

example.gogo
type Priority int

const (
	PriorityLow    Priority = iota + 1 // 1
	PriorityNormal                     // 2
	PriorityHigh                       // 3
)

Sauter une valeur avec _ convient quand la première valeur n'a pas de sens. Les unités de taille ci-dessous sautent iota == 0, puisque 1 << 0 vaudrait 1 octet :

example.gogo
type ByteSize int64

const (
	_           = iota // skip 0
	KB ByteSize = 1 << (10 * iota)
	MB
	GB
)

KB vaut 1 << 10, soit 1024, MB vaut 1 << 20 et GB vaut 1 << 30. Chaque ligne reprend ByteSize = 1 << (10 * iota) avec un iota plus grand.

Comment définir des flags de bits avec iota ?

Décalez 1 vers la gauche de iota positions. Chaque constante obtient alors son propre bit, et vous les combinez avec | :

example.gogo
package main

import "fmt"

type Permission uint8

const (
	PermRead   Permission = 1 << iota // 1
	PermWrite                         // 2
	PermDelete                        // 4
)

func main() {
	perm := PermRead | PermWrite
	fmt.Println(perm&PermWrite != 0)  // true
	fmt.Println(perm&PermDelete != 0) // false
}

La bibliothèque standard utilise le même pattern. log.Ldate, log.Ltime et log.Lshortfile sont des flags 1 << iota que vous combinez dans log.SetFlags(log.LstdFlags | log.Lshortfile).

Existe-t-il des enums en Go ?

Pas en tant que fonctionnalité distincte du langage. Il n'existe pas de mot-clé enum. Go construit les enums à partir de trois éléments dont il dispose déjà : un type nommé, un bloc const avec iota et une méthode String() pour que les valeurs s'affichent sous forme de noms plutôt que de nombres :

example.gogo
package main

import "fmt"

type LogLevel int

const (
	LevelDebug LogLevel = iota
	LevelInfo
	LevelWarn
	LevelError
)

func (l LogLevel) String() string {
	switch l {
	case LevelDebug:
		return "debug"
	case LevelInfo:
		return "info"
	case LevelWarn:
		return "warn"
	case LevelError:
		return "error"
	default:
		return fmt.Sprintf("LogLevel(%d)", int(l))
	}
}

func main() {
	fmt.Println(LevelWarn, LogLevel(9))
	fmt.Printf("level=%v\n", LevelError)
}

Ce programme affiche warn LogLevel(9) puis level=error. fmt appelle String() quand il affiche une valeur avec Println, %v ou %s. Le cas default transforme une valeur hors de la liste, comme LogLevel(9), en quelque chose de lisible.

Comment générer des méthodes String avec stringer ?

Écrire ce switch à la main devient vite fastidieux, et il se désynchronise quand quelqu'un ajoute une constante en oubliant la méthode. L'outil stringer de l'équipe Go la génère pour vous. Ajoutez une directive go:generate à côté du type et lancez go generate :

example.gogo
package main

import "fmt"

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

type OrderStatus int

const (
	OrderStatusUnknown OrderStatus = iota
	OrderStatusPending
	OrderStatusPaid
	OrderStatusShipped
	OrderStatusRefunded
)

func main() {
	fmt.Println(OrderStatusPaid, OrderStatus(42))
}

go generate écrit orderstatus_string.go avec une méthode String(), et le programme affiche Paid OrderStatus(42). -trimprefix retire le préfixe commun des noms. Le fichier généré contient aussi une petite vérification à la compilation. Quand la valeur d'une constante existante change et que le fichier n'a pas été régénéré, le build échoue avec une erreur comme invalid argument: index 4 out of bounds [0:1]. Une constante ajoutée en fin de liste n'est pas détectée, et elle s'affiche comme OrderStatus(5) tant que vous ne relancez pas go generate. C'est pourquoi de nombreuses équipes le lancent en CI et font échouer le build quand les fichiers générés changent.

Pourquoi la valeur zéro d'un enum doit-elle être Unknown ?

Parce que toute variable Go démarre à sa valeur zéro, et pour un enum basé sur int, c'est 0. Une struct Order décodée depuis du JSON sans champ status, ou déclarée avec var o Order, a le statut 0. Si 0 correspond à OrderStatusPending, une valeur manquante ressemble à une vraie commande en attente. Placer un OrderStatusUnknown explicite en premier, ou démarrer à iota + 1, fait que la valeur zéro signifie « non défini », et le code peut alors la rejeter.

Go vérifie-t-il qu'un switch couvre toutes les valeurs d'un enum ?

Non. Un type enum reste un type entier ordinaire, le compilateur ne sait donc pas quelles valeurs sont valides. var level LogLevel = 9 compile, puisque 9 est une constante non typée qui tient dans un int. Un switch qui ne traite qu'une partie des valeurs compile lui aussi sans avertissement.

Les équipes qui veulent cette vérification ajoutent le linter exhaustive, inclus dans golangci-lint. Il signale un switch sur un type enum qui oublie l'une des constantes du type. Un cas default, comme celui de la méthode String() ci-dessus, traite à l'exécution les valeurs hors de la liste.

Quand utiliser const ou var en Go ?

Utilisez const pour les valeurs connues au moment où vous écrivez le code et qui ne changent jamais : timeouts, limites, noms d'en-têtes, valeurs d'enum. Utilisez var pour tout ce qui est calculé à l'exécution, lu depuis la configuration ou construit à partir d'un type composite.

Vous ne pouvez pas prendre l'adresse d'une constante, puisqu'une constante n'est pas stockée dans une variable :

example.gogo
p := &maxBodyBytes
example.texttext
./main.go:6:8: invalid operation: cannot take address of maxBodyBytes (untyped int constant 1048576)

Le cas se présente avec les API qui attendent des pointeurs vers des champs optionnels. Depuis Go 1.26, new accepte une expression, donc new(maxBodyBytes) renvoie un *int qui pointe vers une copie de la valeur. Sur les versions plus anciennes, copiez d'abord la constante dans une variable, puis prenez son adresse.

Vous ne pouvez pas non plus affecter une valeur à une constante. maxBodyBytes = 2 << 20 échoue avec cannot assign to maxBodyBytes (neither addressable nor a map index expression).

Les constantes inutilisées compilent. Une variable locale inutilisée est une erreur, declared and not used: retries, mais une constante inutilisée, même dans une fonction, est acceptée. Des linters comme staticcheck signalent tout de même les constantes non exportées inutilisées au niveau du package.

Les tables de correspondance sont des variables. Une map qui associe un statut à un libellé ne peut pas être une constante : déclarez-la avec var au niveau du package, ou renvoyez-la depuis une fonction si vous ne voulez pas que d'autre code la modifie :

example.gogo
var orderStatusLabels = map[OrderStatus]string{
	OrderStatusPending:  "Awaiting payment",
	OrderStatusPaid:     "Paid",
	OrderStatusShipped:  "On the way",
	OrderStatusRefunded: "Refunded",
}

Un var de niveau package peut être modifié par n'importe quel code du package. C'est la contrepartie de sa capacité à contenir n'importe quel type. Quand les valeurs sont fixes et simples, une constante vous apporte des vérifications à la compilation et aucun risque de modification à l'exécution. Le mot-clé var en Go traite en détail des variables, des valeurs zéro et de :=.

Où se situe LevelUpGo

LevelUpGo enseigne Go au travers d'exercices qui exécutent du vrai code Go dans le navigateur. Go Basics présente const en même temps que les variables et les types. Go Language Deep Dives comporte une leçon sur iota et les patterns de constantes, dans laquelle vous construisez un enum, des flags de bits et des unités de taille en octets. Le Training Ground propose de courts exercices indépendants pour pratiquer en dehors d'un cours. Pour les 24 autres mots réservés, consultez Les mots-clés Go : les 25 expliqués.

Questions fréquentes

const est-il un mot-clé en Go ?

Oui. const fait partie des 25 mots-clés réservés de Go, vous ne pouvez donc pas l'utiliser comme nom de variable, de fonction ou de type. Il introduit une déclaration de constante, soit un simple const name = value, soit un bloc groupé const ( ... ).

Un slice ou une map peut-il être une constante en Go ?

Non. Les constantes ne peuvent être que des booléens, des nombres, des chaînes et des runes, ou des types nommés construits sur eux. const allowedMethods = []string{"GET", "POST"} échoue avec is not constant. Utilisez un var de niveau package pour les slices et les maps, ou une fonction qui renvoie une copie neuve quand les appelants ne doivent pas modifier la version partagée.

Qu'est-ce que iota en Go ?

iota est un identifiant prédéclaré qui numérote les lignes d'un bloc const. Il vaut 0 sur la première ligne, augmente de un à chaque ligne suivante et repart à zéro dans chaque nouveau bloc. Avec un type nommé et la répétition implicite, il donne à chaque constante d'une liste sa propre valeur, et c'est ainsi que Go écrit les enums et les flags de bits.

Quelle est la différence entre constantes typées et non typées en Go ?

Une constante typée, comme const maxRetries int32 = 3, a un type fixe et suit les règles d'affectation habituelles. Une constante non typée, comme const maxRetries = 3, n'a pas de type tant qu'elle n'est pas utilisée, elle peut donc devenir un int, un int64, un float64 ou un time.Duration. Les constantes non typées conservent aussi des valeurs exactes, en précision arbitraire, jusqu'à leur conversion.

Existe-t-il des enums en Go ?

Go n'a pas de mot-clé enum, mais il a des enums en pratique. Vous déclarez un type nommé comme type OrderStatus int, listez ses valeurs dans un bloc const avec iota et ajoutez une méthode String(), souvent générée par stringer. Le compilateur ne restreint pas le type aux valeurs listées : validez donc les entrées et utilisez le linter exhaustive si vous avez besoin que chaque switch couvre chaque valeur.

Sources

Écrivez du Go comme un ingénieur senior

Des leçons interactives dans votre navigateur. Les premières sont gratuites.

Essayer une leçon gratuiteOu créer un compte gratuit