Retour au blog

Le mot-clé if en Go : conditions, instructions d'initialisation et retours anticipés

Le mot-clé if en Go : accolades obligatoires, conditions bool, place du else, instruction courte, masquage, retours anticipés et absence de ternaire.

Le mot-clé if en Go : conditions, instructions d'initialisation et retours anticipés

Le mot-clé if exécute un bloc de code quand une condition booléenne est vraie. La version de Go obéit à trois règles qui surprennent les développeurs venus de C, JavaScript ou Python. La condition s'écrit sans parenthèses, les accolades sont toujours obligatoires, et la condition doit être un vrai bool, car Go ne connaît pas les valeurs truthy ou falsy. Un if peut aussi commencer par une instruction courte, et c'est pourquoi if err := f(); err != nil apparaît dans presque tous les fichiers Go (Go spec).

En bref

  • if status >= 500 { ... } n'a pas de parenthèses autour de la condition, et les accolades sont obligatoires, même pour une seule ligne.
  • La condition doit être de type bool. if retries { ... } échoue avec non-boolean condition in if statement.
  • else doit se trouver sur la même ligne que l'accolade fermante }. Placé seul sur sa ligne, il provoque une erreur de syntaxe.
  • if v, err := parse(s); err != nil { ... } déclare des variables qui existent dans le if et dans toutes ses branches else, et nulle part ailleurs ensuite.
  • Un := dans un bloc if peut masquer une variable externe. Le code compile et utilise silencieusement l'ancienne valeur.
  • Le Go idiomatique retourne tôt en cas d'erreur et garde le chemin nominal sans indentation. Un else placé après un return est généralement supprimé.
  • Go n'a pas d'opérateur ternaire. Utilisez une affectation suivie d'un if, cmp.Or pour les valeurs par défaut, ou min et max pour borner une valeur.
  • && et || court-circuitent l'évaluation, donc req.User != nil && req.User.IsAdmin ne déréférence jamais un pointeur nil.
  • Une longue chaîne if/else if se lit généralement mieux sous la forme d'un switch sans expression.

Comment écrire une instruction if en Go ?

Écrivez if, une expression booléenne et un bloc entre accolades. Voici une vérification de nouvelle tentative pour un client HTTP :

example.gogo
package main

import "fmt"

func shouldRetry(status, attempt, maxAttempts int) bool {
	if attempt >= maxAttempts {
		return false
	}
	return status == 429 || status >= 500
}

func main() {
	fmt.Println(shouldRetry(503, 1, 3))
	fmt.Println(shouldRetry(503, 3, 3))
	fmt.Println(shouldRetry(404, 1, 3))
}
example.texttext
true
false
false

Il n'y a pas de parenthèses autour de attempt >= maxAttempts. Vous pouvez en mettre et le code compile toujours, mais gofmt les retire au prochain enregistrement du fichier. Ce sont les accolades que Go exige. if attempt >= maxAttempts return false sur une seule ligne ne passe pas l'analyse syntaxique, pas plus qu'une condition suivie d'une instruction à la ligne suivante :

example.gogo
if status >= 500
	fmt.Println("server error")
example.texttext
./main.go:8:19: syntax error: unexpected newline, expected { after if clause

Les accolades obligatoires écartent un bug courant en C. Vous ajoutez une seconde ligne sous un if sans accolades, elle semble faire partie de la condition, et elle s'exécute à chaque fois. Le bug TLS « goto fail » d'Apple en 2014 venait exactement de cette erreur. En Go, chaque corps de if a des limites explicites, et gofmt les indente de la même façon dans toutes les bases de code.

Go a-t-il des valeurs truthy et falsy ?

Non. La condition doit être une expression de type bool. Un int, une string, un pointeur, une slice ou une erreur n'est jamais converti en true ou false à votre place :

example.gogo
retries := 3
if retries {
	fmt.Println("retrying")
}
example.texttext
./main.go:7:5: non-boolean condition in if statement

Vous écrivez la comparaison que vous voulez faire : retries > 0, name != "", user != nil, len(items) > 0 ou err != nil. Cela coûte quelques caractères et rend la vérification explicite. En JavaScript, if (count) saute le bloc quand count vaut 0, ce qui est souvent un bug quand zéro est une valeur valide. En Go, le lecteur voit toujours quelle condition est testée.

Comment fonctionnent else et else if en Go ?

else s'exécute quand la condition est fausse, et else if teste une autre condition. Ici, un logger de requêtes choisit un niveau de log selon le code de statut :

example.gogo
func logRequest(ctx context.Context, logger *slog.Logger, path string, status int) {
	var level slog.Level
	if status >= 500 {
		level = slog.LevelError
	} else if status >= 400 {
		level = slog.LevelWarn
	} else {
		level = slog.LevelInfo
	}
	logger.Log(ctx, level, "request", "path", path, "status", status)
}
example.texttext
level=INFO msg=request path=/api/orders status=200
level=WARN msg=request path=/api/orders/99 status=404
level=ERROR msg=request path=/api/checkout status=502

Les branches sont évaluées de haut en bas, et la première qui est vraie s'exécute. Un 502 correspond d'abord à status >= 500, il n'atteint donc jamais le test status >= 400. Le mot-clé else en Go détaille else et les chaînes else if.

Pourquoi else doit-il être sur la même ligne que l'accolade fermante ?

À cause de l'insertion automatique des points-virgules. La grammaire de Go utilise des points-virgules pour terminer les instructions, et le lexer en ajoute un à la fin de toute ligne qui se termine par }, un identifiant, un littéral ou quelques autres tokens (Go spec). Une } seule sur sa ligne termine donc l'instruction if. Le else de la ligne suivante commence alors une nouvelle instruction, et aucune instruction ne peut commencer par else :

example.gogo
if status >= 500 {
	fmt.Println("server error")
}
else {
	fmt.Println("ok")
}
example.texttext
./main.go:10:2: syntax error: unexpected keyword else, expected }

La correction consiste à écrire } else { sur une seule ligne. La même règle explique pourquoi l'accolade ouvrante d'un if, d'un for ou d'une func ne peut pas être placée seule sur sa ligne. Du coup, les projets Go ne se disputent pas sur le style des accolades. Le compilateur n'accepte qu'un seul placement, et gofmt s'occupe du reste de la mise en forme.

Qu'est-ce qu'un if avec instruction courte ?

Un if peut commencer par une instruction courte, séparée de la condition par un point-virgule. L'instruction s'exécute en premier, et les variables qu'elle déclare ont pour portée le if. L'usage le plus courant est la gestion des erreurs :

example.gogo
func loadConfig(path string) (Config, error) {
	var cfg Config
	data, err := os.ReadFile(path)
	if err != nil {
		return cfg, fmt.Errorf("read config: %w", err)
	}
	if err := json.Unmarshal(data, &cfg); err != nil {
		return cfg, fmt.Errorf("parse %s: %w", path, err)
	}
	return cfg, nil
}

json.Unmarshal ne renvoie qu'une erreur, donc l'appel et la vérification tiennent sur une seule ligne, et ce err cesse d'exister après l'accolade fermante. os.ReadFile renvoie des données dont vous avez besoin ensuite, il a donc sa propre ligne. C'est la répartition habituelle en Go. Placez l'instruction dans le if quand ses résultats ne servent qu'à la vérification, et à l'extérieur quand le reste de la fonction les utilise.

La même forme fonctionne avec toute expression qui renvoie une valeur et un booléen, ce que Go appelle l'idiome comma-ok. Une lecture dans une map indique si la clé était présente :

example.gogo
roles := map[string]string{"u_42": "admin", "u_7": "billing"}

if role, ok := roles["u_42"]; ok {
	fmt.Println("u_42 is", role)
}
if _, ok := roles["u_99"]; !ok {
	fmt.Println("u_99 has no role")
}
example.texttext
u_42 is admin
u_99 has no role

Une assertion de type vérifie un comportement optionnel sans risquer de panic. Un handler de streaming ne vide le tampon que si le http.ResponseWriter le permet :

example.gogo
if f, ok := w.(http.Flusher); ok {
	f.Flush()
}

Go 1.26 a ajouté errors.AsType, une version générique de errors.As qui renvoie l'erreur trouvée et un booléen. Elle s'intègre donc au même schéma. Un service peut se rabattre sur des valeurs par défaut quand le fichier de configuration est absent, tout en échouant sur un fichier corrompu :

example.gogo
for _, path := range []string{"missing.json", "bad.json"} {
	_, err := loadConfig(path)
	if pathErr, ok := errors.AsType[*fs.PathError](err); ok {
		fmt.Println("no config file at", pathErr.Path, "so using defaults")
	} else if err != nil {
		fmt.Println("fatal:", err)
	}
}
example.texttext
no config file at missing.json so using defaults
fatal: parse bad.json: invalid character '}' looking for beginning of object key string

Avec l'ancien errors.As, vous déclarez var pathErr *fs.PathError avant le if et passez &pathErr, si bien que la variable survit à la vérification.

Quelle est la portée d'une variable déclarée dans un if ?

La variable existe dans la condition, dans le bloc if et dans chaque bloc else if et else qui lui est rattaché. Elle n'existe plus une fois l'instruction terminée (Go spec) :

example.gogo
raw := "abc"
if n, err := strconv.Atoi(raw); err != nil {
	fmt.Println("bad MAX_CONNS:", err)
} else {
	fmt.Println("max conns", n)
}
fmt.Println(n)

La branche else peut utiliser n, mais la dernière ligne échoue avec undefined: n. C'est délibéré. La variable ne peut pas fuir dans du code qui ne devrait pas en dépendre, et le nom redevient libre pour le if suivant. C'est ainsi qu'une fonction Go peut contenir dix vérifications if err := ...; err != nil sans dix noms d'erreur différents.

Comment un := dans un if provoque-t-il des bugs de masquage ?

Chaque bloc ouvre une nouvelle portée, et := déclare toujours dans la portée courante. Quand vous affectez une variable externe depuis un bloc if avec :=, vous obtenez à la place une nouvelle variable du même nom :

example.gogo
func requestTimeout() time.Duration {
	timeout := 5 * time.Second
	if raw := os.Getenv("HTTP_TIMEOUT"); raw != "" {
		timeout, err := time.ParseDuration(raw)
		if err != nil {
			fmt.Println("ignoring HTTP_TIMEOUT:", err)
			return 5 * time.Second
		}
		fmt.Println("HTTP_TIMEOUT set to", timeout)
	}
	return timeout
}
example.texttext
HTTP_TIMEOUT set to 30s
using 5s

err est nouveau dans le bloc, donc := est autorisé, et il déclare discrètement un second timeout au passage. La valeur analysée va dans la variable interne et disparaît à l'accolade fermante. Le compilateur accepte ce code, et les vérifications par défaut de go vet ne le signalent pas. La correction consiste à déclarer err avec var err error et à utiliser =, pour que ce soit le timeout externe qui reçoive la valeur. Le mot-clé var en Go détaille le masquage, y compris l'analyseur shadow qui détecte ce cas.

Pourquoi le Go idiomatique évite-t-il else ?

Parce que la plupart des branches else en Go suivent un if qui a déjà retourné. Effective Go le formule ainsi : quand une instruction if ne se poursuit pas dans l'instruction suivante, parce que son corps se termine par break, continue, goto ou return, le else inutile est omis (Effective Go). Le résultat est un code où les erreurs sont traitées et renvoyées au fur et à mesure, et où le chemin de succès descend tout droit le long du bord gauche de la fonction.

Voici un handler de remboursement écrit avec un if imbriqué pour chaque vérification :

example.gogo
func handleRefund(w http.ResponseWriter, r *http.Request) {
	user, ok := userFromContext(r.Context())
	if ok {
		if user.CanRefund {
			var req RefundRequest
			if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
				if req.AmountCents > 0 {
					fmt.Fprintf(w, "refund of %d cents queued for %s", req.AmountCents, req.OrderID)
				} else {
					http.Error(w, "amount_cents must be positive", http.StatusBadRequest)
				}
			} else {
				http.Error(w, "invalid JSON body", http.StatusBadRequest)
			}
		} else {
			http.Error(w, "forbidden", http.StatusForbidden)
		}
	} else {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
	}
}

Ça fonctionne, mais le remboursement proprement dit se trouve cinq niveaux plus bas, et chaque message d'erreur est éloigné de la condition qui l'a provoqué. Pour comprendre pourquoi une requête reçoit un 401, il faut associer le dernier else au premier if. Inverser chaque condition et retourner tôt donne le même comportement :

example.gogo
func handleRefund(w http.ResponseWriter, r *http.Request) {
	user, ok := userFromContext(r.Context())
	if !ok {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	if !user.CanRefund {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}

	var req RefundRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
		return
	}
	if req.AmountCents <= 0 {
		http.Error(w, "amount_cents must be positive", http.StatusBadRequest)
		return
	}

	fmt.Fprintf(w, "refund of %d cents queued for %s", req.AmountCents, req.OrderID)
}

En faisant passer les deux versions par httptest avec les mêmes cinq requêtes, on obtient des réponses identiques :

example.texttext
401 unauthorized
403 forbidden
400 invalid JSON body
400 amount_cents must be positive
200 refund of 4999 cents queued for ord_1001

Dans la version à plat, chaque vérification est une clause de garde, une condition associée directement à sa réponse. Ajouter une nouvelle règle, comme un plafond de remboursement, revient à ajouter un bloc if de plus au-dessus de la dernière ligne, au lieu de tout envelopper dans un niveau supplémentaire.

Que signifie « drop this else and outdent its block » ?

C'est le message de la règle indent-error-flow de revive, le linter qui a remplacé golint et qui tourne dans golangci-lint. Elle se déclenche sur un bloc if qui se termine par return et qui est suivi d'un else :

example.gogo
func parsePort(raw string) (int, error) {
	if port, err := strconv.Atoi(raw); err != nil {
		return 0, fmt.Errorf("invalid port %q: %w", raw, err)
	} else if port < 1 || port > 65535 {
		return 0, errors.New("port out of range")
	} else {
		return port, nil
	}
}
example.texttext
main.go:14:9: if block ends with a return statement, so drop this else and outdent its block (move short variable declaration to its own line if necessary)

L'indication entre parenthèses concerne la portée du if. port est déclaré dans l'instruction if, donc supprimer le else placerait return port, nil hors de sa portée. La correction consiste à déplacer port, err := strconv.Atoi(raw) sur sa propre ligne au-dessus du if. Chaque vérification devient alors une simple clause de garde qui retourne.

La règle superfluous-else de revive couvre le même schéma après break, continue, goto, panic et os.Exit, et la règle optionnelle early-return suggère d'inverser un if/else dont la branche else se termine par return. Rien de tout cela ne signifie que else est une erreur. L'exemple logRequest plus haut l'utilise correctement, car chaque branche affecte une valeur et l'exécution continue après l'instruction. Le mot-clé else en Go explique quand else est le bon choix et comment refactoriser les cas où il ne l'est pas.

Go a-t-il un opérateur ternaire ?

Non. La FAQ de Go répond directement à la question. Les concepteurs avaient vu ?: « trop souvent utilisé pour créer des expressions d'une complexité impénétrable », et ils ont décidé qu'« un langage n'a besoin que d'une seule construction de contrôle de flux conditionnel » (Go FAQ). Le remplaçant proposé par la FAQ est un if qui affecte une variable. En pratique, la forme la plus courte définit d'abord la valeur par défaut, puis la remplace :

example.gogo
timeout := 5 * time.Second
if cfg.Debug {
	timeout = 5 * time.Minute
}

Deux usages courants du ternaire disposent désormais de fonctions dédiées. cmp.Or, ajouté dans Go 1.22, renvoie le premier argument qui n'est pas la valeur zéro, ce qui couvre la plupart des cas du type « utilise ceci, sinon cela ». Les built-ins min et max de Go 1.21 servent à borner une valeur :

example.gogo
port := cmp.Or(os.Getenv("PORT"), "8080")
fmt.Println("listening on :" + port)

requested := 500
pageSize := max(1, min(requested, 100))
fmt.Println("page size", pageSize)
example.texttext
listening on :8080
page size 100

cmp.Or accepte un nombre quelconque d'arguments, donc cmp.Or(flagAddr, os.Getenv("ADDR"), ":8080") vérifie un flag, puis l'environnement, puis une valeur par défaut.

Pourquoi ne pas écrire une fonction ternaire générique ?

Vous pouvez écrire func ternary[T any](cond bool, a, b T) T, et certaines bases de code le font. Elle cache un piège que la forme if n'a pas. Go évalue tous les arguments avant d'appeler une fonction, donc les deux branches s'exécutent toujours :

example.gogo
var u *User
name := ternary(u != nil, u.Name, "anonymous")
example.texttext
panic: runtime error: invalid memory address or nil pointer dereference

Un véritable opérateur ?: ignorerait u.Name quand u est nil. La fonction lit u.Name d'abord et plante. Il en va de même pour les appels coûteux, qui s'exécutent que leur résultat soit utilisé ou non. Un if n'évalue que la branche qu'il emprunte.

Comment && et || court-circuitent-ils un if ?

&& n'évalue son opérande de droite que si celui de gauche vaut true, et || n'évalue son opérande de droite que si celui de gauche vaut false (Go spec). L'ordre des conditions peut donc décider si le code panique ou non. Une vérification de nil doit précéder l'accès au champ :

example.gogo
if req.User != nil && req.User.IsAdmin {
	fmt.Println("admin panel")
} else {
	fmt.Println("access denied")
}

Avec un req.User nil, ce code affiche access denied. Inversez les deux conditions, comme dans req.User.IsAdmin && req.User != nil, et la même requête panique avec invalid memory address or nil pointer dereference, car le champ est lu avant que la vérification ne s'exécute.

|| fonctionne de la même façon pour rejeter tôt. Cette fonction utilitaire analyse un en-tête Authorization et s'arrête à la première condition qui échoue :

example.gogo
func bearerToken(header string) (string, bool) {
	scheme, token, found := strings.Cut(header, " ")
	if !found || !strings.EqualFold(scheme, "Bearer") || token == "" {
		return "", false
	}
	return token, true
}

Pour "Bearer eyJhbGciOi", elle renvoie le token et true. Pour "Basic dXNlcjpwYXNz" et pour un "Bearer" seul, elle renvoie false. Placez les vérifications peu coûteuses en premier et les lentes, comme une requête en base de données, en dernier, pour que l'appel lent ne s'exécute que lorsqu'il peut changer le résultat. && est prioritaire sur ||, donc a || b && c signifie a || (b && c). Ajoutez des parenthèses quand vous les mélangez, car gofmt les conserve.

Quand utiliser switch plutôt que if/else if ?

Quand vous comparez une seule valeur à plusieurs cas, ou quand une chaîne compte plus de deux ou trois branches else if. Un switch sans expression teste chaque case comme un booléen, de haut en bas, exactement comme une chaîne if/else if :

example.gogo
switch {
case status >= 500:
	level = slog.LevelError
case status >= 400:
	level = slog.LevelWarn
default:
	level = slog.LevelInfo
}

Les conditions s'alignent sur une seule colonne, et il n'y a pas de break, car un switch Go s'arrête après le premier cas correspondant. Un switch accepte aussi une instruction courte, comme dans switch ext := filepath.Ext(name); ext { ... }, avec les mêmes règles de portée que if. Gardez if pour une ou deux conditions, et surtout pour les vérifications d'erreur, où if err != nil est ce que tout lecteur Go s'attend à voir. La section switch du guide des mots-clés Go couvre les switches sur expression, les type switches et fallthrough.

La place de LevelUpGo

LevelUpGo enseigne Go à travers des exercices qui exécutent du vrai code Go dans le navigateur. Go Basics aborde tôt if, else et les opérateurs logiques, avec des exercices où vous écrivez vous-même les conditions. Simplification propose des leçons pour aplatir du code imbriqué avec des retours anticipés, simplifier la logique booléenne et remplacer les longues chaînes de if par un switch. Le Training Ground propose de courts exercices autonomes pour s'entraîner 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

Go a-t-il un opérateur ternaire ?

Non. Go n'a pas d'opérateur ?:, et la FAQ de Go explique que les concepteurs l'ont écarté parce qu'il servait souvent à construire des expressions difficiles à lire. Utilisez une variable avec une valeur par défaut et un if qui la remplace. Pour les valeurs de repli, cmp.Or(a, b) renvoie le premier argument qui n'est pas la valeur zéro, et min et max servent à borner une valeur.

Pourquoi Go exige-t-il des accolades sur if ?

Les accolades rendent explicite l'étendue de chaque corps de if, si bien qu'ajouter une ligne ne peut pas modifier par accident les instructions que contrôle la condition. Elles permettent aussi à Go de se passer des parenthèses autour de la condition, puisque le { marque la fin de celle-ci. gofmt formate ensuite chaque if de la même manière.

Go a-t-il des valeurs truthy et falsy ?

Non. La condition d'un if doit être de type bool. Les nombres, les strings, les pointeurs, les slices et les erreurs ne sont jamais convertis automatiquement, et if count { ... } échoue avec non-boolean condition in if statement. Écrivez la comparaison explicitement, par exemple count > 0, s != "" ou err != nil.

Une instruction if peut-elle déclarer une variable en Go ?

Oui. Un if peut commencer par une instruction courte, comme dans if err := save(order); err != nil. Les variables déclarées à cet endroit sont visibles dans la condition, le bloc if et les éventuels blocs else if ou else, et elles disparaissent une fois l'instruction terminée.

Pourquoi else doit-il être sur la même ligne que l'accolade fermante en Go ?

Le lexer de Go insère un point-virgule à la fin d'une ligne qui se termine par }. Si else commence la ligne suivante, l'instruction if est déjà terminée, et le compilateur signale syntax error: unexpected keyword else, expected }. Écrire } else { sur une seule ligne évite ce point-virgule inséré.

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