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 avecnon-boolean condition in if statement. elsedoit 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 leifet dans toutes ses brancheselse, et nulle part ailleurs ensuite.- Un
:=dans un blocifpeut 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
elseplacé après unreturnest généralement supprimé. - Go n'a pas d'opérateur ternaire. Utilisez une affectation suivie d'un
if,cmp.Orpour les valeurs par défaut, ouminetmaxpour borner une valeur. &&et||court-circuitent l'évaluation, doncreq.User != nil && req.User.IsAdminne déréférence jamais un pointeur nil.- Une longue chaîne
if/else ifse lit généralement mieux sous la forme d'unswitchsans 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.gogopackage 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.texttexttrue 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.gogoif 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.gogoretries := 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.gogofunc 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.texttextlevel=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.gogoif 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.gogofunc 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.gogoroles := 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.texttextu_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.gogoif 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.gogofor _, 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.texttextno 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.gogoraw := "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.gogofunc 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.texttextHTTP_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.gogofunc 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.gogofunc 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.texttext401 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.gogofunc 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.texttextmain.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.gogotimeout := 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.gogoport := 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.texttextlistening 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.gogovar u *User name := ternary(u != nil, u.Name, "anonymous")
example.texttextpanic: 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.gogoif 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.gogofunc 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.gogoswitch { 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
- The Go Programming Language Specification, If statements : https://go.dev/ref/spec#If_statements
- The Go Programming Language Specification, Semicolons : https://go.dev/ref/spec#Semicolons
- The Go Programming Language Specification, Blocks : https://go.dev/ref/spec#Blocks
- The Go Programming Language Specification, Declarations and scope : https://go.dev/ref/spec#Declarations_and_scope
- The Go Programming Language Specification, Logical operators : https://go.dev/ref/spec#Logical_operators
- Go FAQ, Why does Go not have the ?: operator? : https://go.dev/doc/faq#Does_Go_have_a_ternary_form
- Effective Go, If : https://go.dev/doc/effective_go#if
- Effective Go, Redeclaration and reassignment : https://go.dev/doc/effective_go#redeclaration
- Go 1.22 Release Notes, cmp : https://go.dev/doc/go1.22#minor_library_changes
- revive rule descriptions (indent-error-flow, superfluous-else, early-return) : https://github.com/revive-lint/revive/blob/master/RULES_DESCRIPTIONS.md
