Retour au blog

Le mot-clé else en Go : if/else, else if, retours anticipés et portée

Le fonctionnement du mot-clé else en Go : syntaxe if/else, pourquoi else doit partager la ligne de l'accolade fermante, chaînes else if, portée des variables du if, retours anticipés et absence d'opérateur ternaire.

Le mot-clé else en Go : if/else, else if, retours anticipés et portée

Le mot-clé else exécute un bloc quand la condition du if qui le précède est fausse. Écrivez else if pour tester une autre condition, et enchaînez-en autant que nécessaire. Le else doit se trouver sur la même ligne que l'accolade fermante du bloc if, sinon le code ne compile pas. Le Go idiomatique utilise else moins souvent que la plupart des langages. Quand le bloc if se termine par return, le code qui suit le if est déjà le cas else. Le code Go retourne donc tôt et laisse le chemin nominal sans indentation (Go spec).

En bref

  • if cond { ... } else { ... } exige des accolades autour des deux blocs et aucune parenthèse autour de la condition.
  • } else { doit tenir sur une seule ligne. Un retour à la ligne après } termine l'instruction if, et le compilateur signale syntax error: unexpected keyword else, expected }.
  • Les chaînes else if sont évaluées de haut en bas et la première condition vraie l'emporte. Une longue chaîne se lit mieux sous forme de switch sans expression.
  • Une variable déclarée dans l'instruction courte du if (if v, err := f(); err != nil) est visible dans chaque branche else if et else, et disparaît après la dernière }.
  • Quand le bloc if retourne, supprimez le else. Le linter revive signale la forme restante avec if block ends with a return statement, so drop this else and outdent its block.
  • Go n'a pas d'opérateur ?:. Utilisez if/else, ou cmp.Or quand il vous faut seulement une valeur par défaut à la place d'une valeur zéro.

Comment écrire un if/else en Go ?

Un if prend une condition booléenne et un bloc. Un bloc else placé après s'exécute quand la condition est fausse. Voici la décision que prend une boucle de nouvelles tentatives après l'échec d'une requête :

example.gogo
package main

import "fmt"

func main() {
	attempt := 3
	maxAttempts := 5

	if attempt < maxAttempts {
		fmt.Printf("attempt %d failed, retrying\n", attempt)
	} else {
		fmt.Printf("attempt %d failed, giving up\n", attempt)
	}
}
example.texttext
attempt 3 failed, retrying

Trois règles diffèrent de C, Java et JavaScript :

  1. Pas de parenthèses autour de la condition. if (attempt < maxAttempts) { compile, mais gofmt supprime les parenthèses.
  2. Les accolades sont obligatoires, même pour un corps d'une seule ligne. if code >= 500 fmt.Println("server error") échoue avec syntax error: unexpected name fmt, expected {.
  3. La condition doit être un bool. Go n'a pas de valeurs truthy, donc if len(items) et if user sont des erreurs de type. Écrivez if len(items) > 0 et if user != nil.

La même règle des accolades s'applique à else. Une instruction nue après else échoue avec syntax error: else must be followed by if or statement block. Seul un autre if ou un bloc { ... } peut le suivre. Le mot-clé if en Go traite les conditions et les instructions d'initialisation du point de vue du if.

Pourquoi else doit-il être sur la même ligne que } ?

La grammaire de Go utilise des points-virgules pour terminer les instructions, mais vous n'en tapez presque jamais. Le lexer en insère un en fin de ligne quand le dernier token est un identifiant, un littéral, l'un des mots-clés break, continue, fallthrough ou return, un opérateur comme ++, ou un ), ] ou } fermant (Go spec, Semicolons).

C'est à cause de cette règle que ce code échoue :

example.gogo
	if retries > 0 {
		fmt.Println("retrying")
	}
	else {
		fmt.Println("giving up")
	}
example.texttext
./main.go:10:2: syntax error: unexpected keyword else, expected }

La ligne qui se termine par } reçoit un point-virgule, ce qui termine l'instruction if. La ligne suivante commence alors une nouvelle instruction par else, et aucune instruction ne peut commencer par else. La correction consiste à écrire } else { sur une seule ligne, ce que produit aussi gofmt. C'est la même règle qui oblige Go à placer l'accolade ouvrante sur la même ligne que le if lui-même.

Les développeurs C et Java habitués au style Allman (accolade sur sa propre ligne) s'y heurtent dès leur première semaine. En contrepartie, toutes les bases de code Go formatent if/else de la même manière, si bien que le style n'est jamais un sujet de discussion en revue de code.

Comment fonctionne else if en Go ?

else if n'est pas un mot-clé à part. C'est un else suivi d'une autre instruction if, ce que la grammaire autorise directement : "else" ( IfStmt | Block ). Enchaînez-en autant que nécessaire. Go les évalue de haut en bas et exécute la première branche dont la condition est vraie.

Cette boucle répartit des latences de requêtes dans des buckets de métriques :

example.gogo
package main

import (
	"fmt"
	"time"
)

func main() {
	latencies := []time.Duration{
		40 * time.Millisecond,
		250 * time.Millisecond,
		3 * time.Second,
		90 * time.Millisecond,
	}

	var fast, slow, timedOut int
	for _, d := range latencies {
		if d < 100*time.Millisecond {
			fast++
		} else if d < time.Second {
			slow++
		} else {
			timedOut++
		}
	}
	fmt.Printf("fast=%d slow=%d timed_out=%d\n", fast, slow, timedOut)
}
example.texttext
fast=2 slow=1 timed_out=1

L'ordre compte. Inversez les deux premières conditions pour que d < time.Second passe en premier, et la sortie devient fast=0 slow=3 timed_out=1. Toute requête sous 100ms est aussi sous une seconde, donc la vérification la plus large la capte d'abord et la branche fast ne s'exécute jamais. Placez la plage la plus étroite en premier.

Au-delà de trois branches, un switch sans expression se lit généralement mieux. Chaque case est une condition, les cases sont évalués dans le même ordre de haut en bas, et default remplace le else final :

example.gogo
	for _, d := range latencies {
		switch {
		case d < 100*time.Millisecond:
			fast++
		case d < time.Second:
			slow++
		default:
			timedOut++
		}
	}

La sortie est identique. Consultez la section switch du guide des mots-clés Go pour voir en quoi le switch de Go diffère de celui de C. switch et select utilisent default pour le cas « rien d'autre ne correspond », si bien que else n'apparaît jamais qu'après un if.

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

Un if peut commencer par une instruction courte, en général une déclaration :=. Les variables qu'elle déclare ont pour portée toute l'instruction if, ce qui inclut chaque branche else if et else. La spec place chaque if dans son propre bloc implicite, et les branches else se trouvent à l'intérieur (Go spec, Blocks).

C'est là que else est difficile à remplacer. La vérification d'une valeur PORT lue depuis l'environnement a besoin du nombre converti dans chaque branche :

example.gogo
package main

import (
	"fmt"
	"strconv"
)

func main() {
	for _, raw := range []string{"8080", "80", "http"} {
		if port, err := strconv.Atoi(raw); err != nil {
			fmt.Printf("PORT=%q is not a number: %v\n", raw, err)
		} else if port < 1024 {
			fmt.Printf("PORT=%d is privileged (below 1024)\n", port)
		} else {
			fmt.Printf("PORT=%d ok\n", port)
		}
	}
}
example.texttext
PORT=8080 ok
PORT=80 is privileged (below 1024)
PORT="http" is not a number: strconv.Atoi: parsing "http": invalid syntax

port et err sont disponibles dans les trois branches. Après l'accolade fermante, ils n'existent plus. Ajoutez fmt.Println("listening on", port) après le if et le build échoue avec undefined: port. Si vous avez besoin de la valeur ensuite, déclarez-la sur sa propre ligne avant le if.

Le piège du masquage de variable

Comme l'instruction courte déclare de nouvelles variables, elle peut masquer une variable extérieure du même nom. Ce code compile et passe go vet :

example.gogo
	var cfg Config
	if cfg, err := loadConfig(data); err != nil {
		log.Fatal(err)
	} else {
		fmt.Printf("loaded config, addr=%q\n", cfg.Addr)
	}
	fmt.Printf("starting server, addr=%q\n", cfg.Addr)
example.texttext
loaded config, addr=":8443"
starting server, addr=""

Le cfg à l'intérieur du if est une nouvelle variable. Le cfg extérieur reste à sa valeur zéro, et le serveur démarre avec une adresse vide. Écrivez cfg, err := loadConfig(data) sur sa propre ligne et vérifiez err avec un if simple. L'article sur le mot-clé var en Go traite du masquage (shadowing) et de l'analyseur shadow qui le détecte.

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

La plupart des fonctions Go sont une suite d'étapes qui peuvent chacune échouer. Si chaque étape s'imbrique dans le else de la précédente, le chemin de succès dérive vers la droite et la gestion d'erreur finit loin de la vérification qui l'a déclenchée. Voici un handler de création de commande écrit de cette façon :

example.gogo
func handleCreateOrder(w http.ResponseWriter, r *http.Request) {
	var req CreateOrderRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
		if req.Quantity > 0 {
			if id, err := saveOrder(req); err == nil {
				w.WriteHeader(http.StatusCreated)
				json.NewEncoder(w).Encode(map[string]string{"id": id})
			} else {
				http.Error(w, "could not save order", http.StatusInternalServerError)
			}
		} else {
			http.Error(w, "quantity must be positive", http.StatusBadRequest)
		}
	} else {
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
	}
}

Pour savoir ce qui se passe avec un JSON invalide, il faut lire jusqu'au bas de la fonction. Inversez chaque condition pour que le cas d'erreur passe en premier et retourne :

example.gogo
func handleCreateOrder(w http.ResponseWriter, r *http.Request) {
	var req CreateOrderRequest
	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
		http.Error(w, "invalid JSON body", http.StatusBadRequest)
		return
	}
	if req.Quantity <= 0 {
		http.Error(w, "quantity must be positive", http.StatusBadRequest)
		return
	}

	id, err := saveOrder(req)
	if err != nil {
		http.Error(w, "could not save order", http.StatusInternalServerError)
		return
	}
	w.WriteHeader(http.StatusCreated)
	json.NewEncoder(w).Encode(map[string]string{"id": id})
}

Un test httptest des deux versions sur quatre corps de requête (JSON cassé, quantité nulle, SKU en rupture de stock et commande valide) donne des codes de statut et des corps identiques. La seconde version ne contient aucun else. Chaque vérification se trouve à côté de sa réponse d'erreur, et les deux dernières lignes forment le chemin de succès, collé à la marge gauche. Une quatrième étape de validation ajoute un bloc if de plus, sans niveau d'imbrication supplémentaire.

L'équipe Go décrit ce style dans Effective Go : « Comme les cas d'erreur ont tendance à se terminer par des instructions return, le code obtenu n'a pas besoin d'instructions else. » La page Go Code Review Comments l'appelle Indent Error Flow : « Essayez de garder le chemin normal du code à une indentation minimale, et indentez la gestion d'erreur en la traitant en premier. »

Ce que disent les linters à propos de else

Le linter revive (le successeur maintenu de golint) active par défaut deux règles sur else. Exécuté sur ce code :

example.gogo
func readTimeout() (int, error) {
	secs, err := strconv.Atoi(os.Getenv("TIMEOUT_SECONDS"))
	if err != nil {
		return 0, fmt.Errorf("parse TIMEOUT_SECONDS: %w", err)
	} else {
		return secs, nil
	}
}

func countValid(lines []string) int {
	n := 0
	for _, line := range lines {
		if line == "" {
			continue
		} else {
			n++
		}
	}
	return n
}

il signale :

example.texttext
lint.go:13:9: if block ends with a return statement, so drop this else and outdent its block
lint.go:23:10: if block ends with a continue statement, so drop this else and outdent its block

Le premier message vient de indent-error-flow, qui se déclenche quand le bloc if se termine par return. Le second vient de superfluous-else, qui couvre continue, break et goto ainsi que les appels qui ne retournent jamais, comme panic, os.Exit et log.Fatal. revive propose aussi une règle optionnelle early-return pour la forme inversée, où c'est le bloc else qui retourne. golangci-lint expose les mêmes règles via son linter revive.

Aucune des règles par défaut ne signale le handler imbriqué ci-dessus, car aucun de ses blocs ne retourne. Le linter ne détecte que le cas mécanique, et aplatir une pyramide de vérifications de succès est un refactoring que vous devez faire vous-même.

Quand else est-il le bon choix en Go ?

Supprimer else est une règle pour les blocs qui sortent. Quand les deux branches font un vrai travail et qu'aucune ne quitte la fonction, else est la façon la plus claire de dire « l'une des deux ». Cela arrive dans trois situations.

La première, ce sont deux issues qui continuent toutes les deux. Une recherche dans un cache compte un hit ou un miss puis poursuit, et comme aucune branche ne retourne, il n'y a rien à désindenter :

example.gogo
	if entry, ok := cache[key]; ok {
		hits++
		resp = entry
	} else {
		misses++
		resp = fetchFromOrigin(key)
		cache[key] = resp
	}

La deuxième consiste à choisir entre deux valeurs. Pour une valeur peu coûteuse, définissez la valeur par défaut puis remplacez-la :

example.gogo
	level := slog.LevelInfo
	if debug {
		level = slog.LevelDebug
	}

Quand la construction de la valeur par défaut a un coût ou un effet de bord, comme l'ouverture d'une connexion, utilisez if/else pour qu'une seule branche s'exécute :

example.gogo
	var store Store
	if redisURL != "" {
		store = RedisStore{url: redisURL}
	} else {
		store = MemoryStore{}
	}

La forme « valeur par défaut puis remplacement » donnerait var store Store = MemoryStore{} suivi d'un remplacement. Cela ne fonctionne ici que parce que MemoryStore{} ne coûte rien. Si la valeur par défaut ouvrait un fichier ou un pool de connexions, le remplacement jetterait une ressource que vous n'avez jamais utilisée.

La troisième concerne une variable issue de l'instruction courte. Quand chaque branche en a besoin, comme dans l'exemple PORT, le else limite sa portée à la vérification. La déclarer avant le if fonctionne aussi, mais elle reste alors visible dans tout le reste de la fonction.

Existe-t-il un opérateur ternaire en Go ?

Non. Go n'a pas de cond ? a : b. La Go FAQ explique pourquoi : « Si ?: est absent de Go, c'est que les concepteurs du langage avaient vu cette opération trop souvent utilisée pour créer des expressions d'une complexité impénétrable. La forme if-else, bien que plus longue, est incontestablement plus claire. Un langage n'a besoin que d'une seule construction de contrôle conditionnelle. »

Le remplacement est if/else ou la forme « valeur par défaut puis remplacement » de la section précédente. Pour le cas courant « utiliser cette valeur sauf si elle est vide », Go 1.22 a ajouté cmp.Or, qui renvoie le premier de ses arguments qui n'est pas la valeur zéro :

example.gogo
package main

import (
	"cmp"
	"fmt"
	"os"
)

func main() {
	addr := cmp.Or(os.Getenv("LISTEN_ADDR"), ":8080")
	fmt.Println("listening on", addr)
}
example.texttext
listening on :8080

cmp.Or est une fonction, pas un opérateur, donc Go évalue chaque argument avant l'appel. cmp.Or(cachedToken, fetchToken()) appelle fetchToken à chaque fois, même quand cachedToken est défini. Un helper générique ternary(cond, a, b) a le même problème, ce qui explique en partie pourquoi les bases de code Go en définissent rarement. Quand une branche a un coût, écrivez le if.

La place de LevelUpGo

LevelUpGo enseigne Go à travers des exercices qui exécutent du vrai code Go dans le navigateur. Go Basics couvre if, else if, switch et le retour anticipé sur erreur, pour apprendre le langage depuis zéro. Simplification propose des leçons sur l'aplatissement du code imbriqué, la simplification de la logique booléenne et le remplacement des chaînes else if par un switch. Pour les 24 autres mots réservés, consultez Les mots-clés Go : les 25 expliqués.

Questions fréquentes

À quoi sert else en Go ?

else exécute un bloc quand la condition du if qui le précède est fausse. Il peut être suivi d'un bloc (else { ... }) ou d'un autre if (else if cond { ... }). C'est l'un des 25 mots-clés réservés de Go, et il ne peut apparaître qu'après un bloc if.

Pourquoi Go affiche-t-il « syntax error: unexpected keyword else » ?

Le else se trouve sur une nouvelle ligne après l'accolade } fermante du bloc if. Go insère un point-virgule après une } en fin de ligne, ce qui termine l'instruction if. Le else de la ligne suivante n'a alors plus rien à quoi se rattacher. Écrivez } else { sur une seule ligne, ou lancez gofmt.

Go a-t-il elif ou elseif ?

Non. Go l'écrit en deux mots, else if, c'est-à-dire un else suivi d'une nouvelle instruction if. Pour les longues chaînes, un switch sans expression après le mot-clé se lit plus proprement.

Faut-il utiliser else après return en Go ?

Non. Si le bloc if se termine par return, le code qui suit le if ne s'exécute que lorsque la condition était fausse, donc le else n'ajoute que de l'indentation. revive le signale avec sa règle indent-error-flow, et Go Code Review Comments recommande de traiter l'erreur en premier et de garder le chemin normal sans indentation.

Peut-on utiliser une variable du if dans le bloc else ?

Oui. Une variable déclarée dans l'instruction courte, comme dans if n, err := strconv.Atoi(s); err != nil, est visible dans le bloc if et dans chaque branche else if et else. Elle sort de la portée après la dernière accolade fermante.

Comment écrire un if/else sur une seule ligne en Go ?

Pas dans du code formaté. Un if/else sur une ligne compile, mais gofmt le répartit sur plusieurs lignes, et Go n'a pas d'opérateur ternaire. Utilisez un if/else classique, affectez une valeur par défaut puis remplacez-la dans un if, ou utilisez cmp.Or quand il vous faut une valeur de repli à la place d'une valeur zéro.

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