for est le seul mot-clé de boucle en Go. Il existe sous trois formes : la boucle à trois clauses for i := 0; i < n; i++, la boucle à condition seule for cond, qui est la boucle while de Go, et la boucle infinie for { ... }. Ajoutez range et le même mot-clé itère sur les slices, les tableaux, les maps, les chaînes, les channels, les entiers et les fonctions itératrices. Go n'a pas de mot-clé while, do, foreach ou loop (Go spec, For statements).
En bref
for init; cond; post { ... }est la boucle avec compteur. Pas de parenthèses autour des clauses, et les accolades sont obligatoires.for cond { ... }est une boucle while, etfor { ... }boucle jusqu'à unbreak, unreturnou unpanic.for i, v := range xitère sur une slice, un tableau, une map, une chaîne, un channel, un entier (Go 1.22) ou une fonction itératrice (Go 1.23). L'ordre d'une map est aléatoire.- Depuis Go 1.22, chaque itération reçoit une nouvelle variable de boucle si
go.moddéclarego 1.22ou une version ultérieure. Les goroutines lancées dans une boucle ne voient plus toutes la dernière valeur. - La valeur de range est une copie. Affectez
items[i]pour modifier la slice. - Un
breaksans label à l'intérieur d'unselectou d'unswitchne quitte que cette instruction. Utilisez un label pour sortir de la boucle.
Quelles sont les formes d'une boucle for en Go ?
La spec définit une seule instruction for sous trois formes. Elles ne diffèrent que par les clauses que vous écrivez.
La boucle à trois clauses
L'instruction d'initialisation s'exécute une fois, la condition est vérifiée avant chaque itération, et l'instruction post s'exécute après chaque itération. Cette boucle relance un health check instable avec un backoff exponentiel :
example.gogopackage main import ( "errors" "fmt" "time" ) var errUnavailable = errors.New("503 service unavailable") func fetchStatus(attempt int) error { if attempt < 3 { return errUnavailable } return nil } func main() { backoff := 100 * time.Millisecond for attempt := 1; attempt <= 5; attempt++ { err := fetchStatus(attempt) if err == nil { fmt.Printf("attempt %d: ok\n", attempt) break } fmt.Printf("attempt %d: %v, retrying in %v\n", attempt, err, backoff) time.Sleep(backoff) backoff *= 2 } }
example.texttextattempt 1: 503 service unavailable, retrying in 100ms attempt 2: 503 service unavailable, retrying in 200ms attempt 3: ok
attempt n'existe qu'à l'intérieur de la boucle. Chacune des trois clauses peut être omise. for ; attempt <= 5; { est valide, et gofmt le réécrit en for attempt <= 5 {.
L'habitude héritée de C qui consiste à entourer les clauses de parenthèses ne compile pas :
example.gogofor (i := 0; i < 3; i++) {
example.texttext./main.go:6:9: syntax error: unexpected :=, expected )
La boucle à condition seule (while)
Supprimez les instructions d'initialisation et post, et for devient une boucle while. Celle-ci vide une file de jobs jusqu'à ce qu'elle soit vide :
example.gogopackage main import "fmt" func main() { queue := []string{"resize:42.png", "resize:43.png", "thumbnail:44.png"} for len(queue) > 0 { job := queue[0] queue = queue[1:] fmt.Println("processing", job) } fmt.Println("queue empty") }
example.texttextprocessing resize:42.png processing resize:43.png processing thumbnail:44.png queue empty
Écrire while à la place provoque une erreur d'analyse, car while est un identifiant ordinaire en Go :
example.texttext./main.go:7:8: syntax error: unexpected name pending at end of statement
Le compilateur lit while comme une expression, trouve un autre nom après, et abandonne. Le message ne mentionne jamais les boucles, d'où la confusion chez les développeurs qui viennent d'autres langages.
La boucle infinie
Sans aucune clause, for { ... } s'exécute jusqu'à ce qu'une instruction à l'intérieur en sorte. Les serveurs utilisent cette forme pour les boucles d'acceptation de connexions et les pollers :
example.gogofor { conn, err := ln.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return } log.Printf("accept: %v", err) continue } go handle(conn) }
La boucle se termine quand le listener est fermé et que Accept renvoie net.ErrClosed. for {} remplace while (true) et for (;;), et le compilateur sait qu'elle ne se termine jamais d'elle-même. Une fonction qui se termine par un for {} sans break n'a pas besoin d'un return final.
Comment fonctionne for range ?
range transforme une boucle for en itération sur une valeur. Ce que vous obtenez dépend du type :
| Range sur | Première valeur | Seconde valeur |
|---|---|---|
| Slice ou tableau | indice | copie de l'élément |
| Chaîne | offset en octets | rune (point de code Unicode) |
| Map | clé | valeur |
| Channel | valeur reçue | aucune |
Entier n (Go 1.22) | de 0 à n-1 | aucune |
| Fonction itératrice (Go 1.23) | ce qu'elle produit | ce qu'elle produit |
Vous pouvez omettre la seconde variable (for i := range items) ou ignorer la première avec _ (for _, item := range items). for range n sans variable exécute un bloc n fois.
example.gogopackage main import ( "fmt" "maps" "slices" ) func main() { endpoints := []string{"/health", "/orders", "/users"} for i, path := range endpoints { fmt.Println(i, path) } statusCounts := map[int]int{200: 812, 404: 17, 500: 3} for _, code := range slices.Sorted(maps.Keys(statusCounts)) { fmt.Printf("%d: %d\n", code, statusCounts[code]) } for offset, r := range "café" { fmt.Printf("%d:%c ", offset, r) } fmt.Println() for attempt := range 3 { fmt.Print(attempt, " ") } fmt.Println() results := make(chan string, 2) results <- "job 1 done" results <- "job 2 done" close(results) for msg := range results { fmt.Println(msg) } }
example.texttext0 /health 1 /orders 2 /users 200: 812 404: 17 500: 3 0:c 1:a 2:f 3:é 0 1 2 job 1 done job 2 done
C'est la boucle sur la map qui demande le plus d'attention. Le runtime rend volontairement l'ordre d'itération d'une map aléatoire, pour que le code ne puisse pas en dépendre. Pour afficher les codes de statut dans l'ordre, maps.Keys renvoie un itérateur sur les clés et slices.Sorted les collecte et les trie. Le mot-clé map en Go traite l'itération sur les maps plus en détail.
La boucle sur la chaîne produit des runes, et les offsets sont des positions en octets, donc é commence à l'offset 3 et occupe deux octets. Utilisez for i := 0; i < len(s); i++ avec s[i] quand vous voulez les octets bruts.
La boucle sur le channel s'arrête quand le channel est fermé. Si personne ne le ferme, elle bloque indéfiniment une fois le buffer vide. Le mot-clé chan en Go explique qui doit fermer un channel.
Les fonctions itératrices (Go 1.23) permettent à toute fonction de signature func(yield func(K, V) bool) de fonctionner avec range. La bibliothèque standard en renvoie depuis maps.Keys, maps.All, slices.All, slices.Backward et strings.Lines, et vous pouvez écrire les vôtres pour des résultats d'API paginés ou des lignes de base de données. range a plus de règles que ce qui tient ici. La section range du guide des mots-clés Go en donne la version courte.
Qu'est-ce qui a changé pour les variables de boucle en Go 1.22 ?
Avant Go 1.22, une boucle for déclarait ses variables une seule fois et les réutilisait à chaque itération. Une closure ou une goroutine qui capturait la variable voyait la valeur qu'elle contenait au moment où la closure s'exécutait, en général la dernière. Go 1.22 a changé ce comportement pour que chaque itération reçoive une nouvelle variable (Go blog, Fixing For Loops in Go 1.22).
Ce health checker lance une goroutine par hôte de base de données :
example.gogopackage main import ( "fmt" "sync" ) func main() { hosts := []string{"db-1", "db-2", "db-3"} var wg sync.WaitGroup for _, host := range hosts { wg.Add(1) go func() { defer wg.Done() fmt.Println("checking", host) }() } wg.Wait() }
Avec go 1.21 dans go.mod, trois exécutions ont chacune affiché trois fois checking db-3, et go vet a signalé :
example.texttextmain.go:16:28: loop variable host captured by func literal
Avec go 1.27 dans go.mod, le même code vérifie les trois hôtes, dans l'ordre où les goroutines se terminent, et go vet ne signale rien.
Le changement dépend de la ligne go de go.mod, pas de la toolchain utilisée pour le build. Un compilateur Go 1.27 compile toujours un module qui déclare go 1.21 avec l'ancienne sémantique, si bien qu'une dépendance écrite avant la 1.22 continue de fonctionner comme ses auteurs l'ont testée. Le changement s'applique à la fois à la forme à trois clauses et à range. L'ancien contournement host := host à l'intérieur de la boucle est désormais redondant. Le modernizer forvar de go fix le supprime pour vous. Le mot-clé func en Go traite plus en détail les closures et les variables capturées.
Pourquoi modifier la valeur de range ne change-t-il pas la slice ?
La seconde variable de range est une copie de l'élément. Pour une slice de structs, modifier un champ de la copie laisse la slice intacte :
example.gogopackage main import "fmt" type Invoice struct { ID string Amount int Paid bool } func main() { invoices := []Invoice{ {ID: "inv-101", Amount: 4200}, {ID: "inv-102", Amount: 1800}, } for _, inv := range invoices { inv.Paid = true } fmt.Println(invoices[0].Paid, invoices[1].Paid) for i := range invoices { invoices[i].Paid = true } fmt.Println(invoices[0].Paid, invoices[1].Paid) }
example.texttextfalse false true true
La première boucle définit Paid sur une copie jetée à la fin de chaque itération. La seconde boucle accède à la slice par indice et modifie l'élément réel. Go 1.22 n'a rien changé ici. Le correctif de la 1.22 a donné à chaque itération sa propre variable, et cette variable reste une copie. Pour une slice de pointeurs ([]*Invoice), la copie est un pointeur et inv.Paid = true met bien à jour la facture.
Conserver un pointeur vers un élément
&invoices[i] vous donne un pointeur vers le tableau sous-jacent de la slice. Ce pointeur devient obsolète si la slice dépasse plus tard sa capacité, car append copie alors les éléments dans un nouveau tableau :
example.gogovar large *Invoice for i := range invoices { if invoices[i].Amount > 4000 { large = &invoices[i] } } invoices = append(invoices, Invoice{"inv-103", 900}) large.Amount = 0 fmt.Println(invoices[0].Amount, large.Amount)
example.texttext4200 0
large pointe toujours vers l'ancien tableau, donc l'écriture n'atteint jamais invoices. Conservez l'indice (largeIdx = i) plutôt qu'un pointeur quand la slice peut grandir.
Comment fonctionnent break et continue dans une boucle for ?
break quitte le for, le switch ou le select le plus interne. continue passe à l'itération suivante du for le plus interne. Les deux acceptent un label facultatif qui désigne une boucle externe.
Un label est un identifiant suivi de deux-points, placé sur la ligne qui précède la boucle. Ce parcours s'arrête à la première erreur rencontrée dans plusieurs fichiers de log :
example.gogopackage main import "fmt" func main() { logLines := [][]string{ {"INFO start", "INFO ready"}, {"INFO request", "ERROR db timeout", "INFO retry"}, {"INFO shutdown"}, } files: for file, lines := range logLines { for _, line := range lines { if line[:5] == "ERROR" { fmt.Printf("file %d: first error %q\n", file, line) break files } } } }
example.texttextfile 1: first error "ERROR db timeout"
Sans le label, break ne quitterait que la boucle interne, et le parcours passerait au troisième fichier. continue files ignorerait le reste du fichier en cours et passerait au suivant.
break à l'intérieur d'un select
Ce bug compile et semble correct à la première lecture. Un worker lit des jobs jusqu'à l'annulation de son contexte :
example.gogofunc worker(ctx context.Context, jobs <-chan string) { for { select { case <-ctx.Done(): fmt.Println("worker stopping:", ctx.Err()) break case job := <-jobs: fmt.Println("handled", job) } } fmt.Println("worker flushed metrics") }
Le break quitte le select, pas le for. Le worker affiche worker stopping et retourne directement dans le select, où ctx.Done() est toujours fermé, si bien qu'il affiche la même ligne en boucle serrée. Les métriques ne sont jamais flushées. go vet signale le code mort après la boucle :
example.texttextmain.go:20:2: unreachable code
et staticcheck signale le break lui-même :
example.texttextmain.go:15:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)
La correction consiste à poser un label sur la boucle :
example.gogofunc worker(ctx context.Context, jobs <-chan string) { loop: for { select { case <-ctx.Done(): fmt.Println("worker stopping:", ctx.Err()) break loop case job := <-jobs: fmt.Println("handled", job) } } fmt.Println("worker flushed metrics") }
example.texttexthandled email:1 handled email:2 worker stopping: context deadline exceeded worker flushed metrics
Un return dans le cas ctx.Done() fonctionne aussi, quand il n'y a rien à faire après la boucle. Le même piège s'applique à un break placé dans un switch à l'intérieur d'une boucle.
Erreurs courantes avec les boucles for
defer dans une boucle
defer s'exécute au retour de la fonction, pas à la fin de l'itération. Une boucle qui ouvre un fichier et diffère Close garde tous les fichiers ouverts jusqu'à la fin de la fonction entière :
example.gogofor _, p := range paths { f, err := os.Open(p) if err != nil { return 0, err } defer f.Close() // ... }
Avec une poignée de chemins, rien ne se voit, mais dix mille fichiers de log épuiseront les descripteurs de fichiers du processus. Ni go vet ni staticcheck ne le signalent. Déplacez le corps dans une fonction pour que chaque defer s'exécute à la fin de son propre appel, ou appelez f.Close() directement à la fin de l'itération. Les erreurs Go courantes à éviter traite ce cas avec un exemple complet.
Ajouter des éléments à une slice pendant son parcours
range évalue son opérande une seule fois, avant la première itération. Un append dans la boucle ne la fait pas durer plus longtemps :
example.gogourls := []string{"/", "/about"} for _, u := range urls { urls = append(urls, u+"/sitemap.xml") } fmt.Println(len(urls), urls)
example.texttext4 [/ /about //sitemap.xml /about/sitemap.xml]
La boucle s'est exécutée deux fois, une fois par élément d'origine. C'est donc sans danger, mais cela signifie aussi qu'un crawler ne peut pas découvrir de nouvelles URL de cette façon. Utilisez une boucle à condition sur une file, comme for len(queue) > 0 plus haut, quand la liste de travail grandit au fil du traitement.
Modifier une map pendant un range
Supprimer la clé courante pendant un range sur une map est autorisé et sans danger. Cette boucle supprime les sessions inactives :
example.gogofor id, idle := range sessions { if idle > 30 { delete(sessions, id) } }
Ajouter des clés pendant la boucle est aussi autorisé, mais la spec précise qu'une nouvelle entrée « peut être produite pendant l'itération ou être ignorée ». Collectez les nouvelles clés dans une map séparée et fusionnez-les ensuite si chacune doit être traitée.
Capturer l'adresse de la variable de boucle
ptrs = append(ptrs, &inv) à l'intérieur de for _, inv := range invoices vous donne des pointeurs vers des copies, pas vers les éléments de la slice. Avant Go 1.22, tous les pointeurs pointaient vers la même variable et affichaient la dernière facture. Depuis Go 1.22, chaque pointeur pointe vers sa propre copie, ce qui est correct mais reste déconnecté de la slice. Utilisez &invoices[i] si vous voulez modifier les originaux, et gardez en tête l'avertissement sur la capacité ci-dessus.
Existe-t-il une boucle while ou do-while en Go ?
Non. Effective Go indique que la boucle for de Go « unifie for et while, et il n'y a pas de do-while ». La forme à condition seule couvre ce pour quoi les autres langages utilisent while.
Un do-while exécute le corps une fois avant de vérifier la condition. En Go, vous l'écrivez comme une boucle infinie avec la vérification à la fin. Les appels d'API paginés en sont le cas typique, car vous récupérez toujours la première page :
example.gogocursor := "" for { page := fetchPage(cursor) fmt.Println("fetched", page.Items) cursor = page.Next if cursor == "" { break } }
example.texttextfetched [order-1 order-2] fetched [order-3]
C'est deux lignes de plus que ne le serait do { } while (cursor != ""), et la condition de sortie se trouve en bas, là où on la cherche.
La place de LevelUpGo
LevelUpGo enseigne Go à travers des exercices qui exécutent du vrai code Go dans le navigateur. Go Basics consacre deux leçons à for : l'une sur les trois formes de boucle, l'autre sur range, break et continue. Concurrency Fundamentals couvre les boucles qui piègent les développeurs dans le code concurrent, comme lancer des goroutines dans une boucle, faire un range sur des channels et sortir d'une boucle for/select lors d'une annulation. 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 une boucle while ?
Non, Go n'a pas de mot-clé while. Écrivez for cond { ... } pour une boucle while et for { ... } pour une boucle infinie. Taper while provoque une erreur de syntaxe, car le compilateur le traite comme un nom ordinaire.
Go a-t-il une boucle foreach ?
for ... range est le foreach de Go. for _, v := range items parcourt chaque élément d'une slice, et la même forme fonctionne pour les tableaux, les maps, les chaînes, les channels, les entiers et les fonctions itératrices.
Comment faire une boucle N fois en Go ?
Depuis Go 1.22, for i := range n s'exécute avec i allant de 0 à n-1, et for range n s'exécute n fois sans variable. La forme à trois clauses for i := 0; i < n; i++ fonctionne dans toutes les versions.
Comment parcourir une map dans l'ordre trié ?
Collectez et triez les clés, puis faites un range dessus. Depuis Go 1.23, for _, k := range slices.Sorted(maps.Keys(m)) fait les deux en une ligne. L'ordre d'itération d'une map est volontairement aléatoire, donc n'en dépendez jamais.
Comment parcourir une slice à l'envers en Go ?
Utilisez for i := len(s) - 1; i >= 0; i--, ou depuis Go 1.23, for i, v := range slices.Backward(s), qui produit l'indice et la valeur du dernier élément jusqu'au premier.
Pourquoi ma goroutine dans une boucle for affiche-t-elle toujours la même valeur ?
Votre module est sur une version de Go antérieure à 1.22, où la variable de boucle est partagée par toutes les itérations. Remplacez la ligne go de go.mod par go 1.22 ou une version ultérieure, et chaque itération reçoit sa propre variable. Sur une version plus ancienne, passez la valeur en argument (go func(h string) { ... }(host)) ou copiez-la avec host := host à l'intérieur de la boucle.
Sources
- The Go Programming Language Specification, For statements : https://go.dev/ref/spec#For_statements
- The Go Programming Language Specification, Break statements : https://go.dev/ref/spec#Break_statements
- Effective Go, For : https://go.dev/doc/effective_go#for
- The Go Blog, Fixing For Loops in Go 1.22 : https://go.dev/blog/loopvar-preview
- Notes de version de Go 1.22 : https://go.dev/doc/go1.22
- Notes de version de Go 1.23 : https://go.dev/doc/go1.23
- The Go Blog, Range Over Function Types : https://go.dev/blog/range-functions
- staticcheck SA4011 : https://staticcheck.dev/docs/checks/#SA4011
