Retour au blog

Le mot-clé for en Go : boucles, range, variables de boucle et labels

Le fonctionnement du mot-clé for en Go : les trois formes de boucle, for range sur les slices, maps, chaînes, channels, entiers et itérateurs, les variables de boucle de Go 1.22, break et continue avec labels, et les erreurs de boucle courantes.

Le mot-clé for en Go : boucles, range, variables de boucle et labels

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, et for { ... } boucle jusqu'à un break, un return ou un panic.
  • for i, v := range x itè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.mod déclare go 1.22 ou 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 break sans label à l'intérieur d'un select ou d'un switch ne 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.gogo
package 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.texttext
attempt 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.gogo
	for (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.gogo
package 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.texttext
processing 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.gogo
	for {
		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 surPremière valeurSeconde valeur
Slice ou tableauindicecopie de l'élément
Chaîneoffset en octetsrune (point de code Unicode)
Mapclévaleur
Channelvaleur reçueaucune
Entier n (Go 1.22)de 0 à n-1aucune
Fonction itératrice (Go 1.23)ce qu'elle produitce 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.gogo
package 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.texttext
0 /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.gogo
package 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.texttext
main.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.gogo
package 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.texttext
false 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.gogo
	var 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.texttext
4200 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.gogo
package 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.texttext
file 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.gogo
func 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.texttext
main.go:20:2: unreachable code

et staticcheck signale le break lui-même :

example.texttext
main.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.gogo
func 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.texttext
handled 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.gogo
	for _, 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.gogo
	urls := []string{"/", "/about"}
	for _, u := range urls {
		urls = append(urls, u+"/sitemap.xml")
	}
	fmt.Println(len(urls), urls)
example.texttext
4 [/ /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.gogo
	for 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.gogo
	cursor := ""
	for {
		page := fetchPage(cursor)
		fmt.Println("fetched", page.Items)
		cursor = page.Next
		if cursor == "" {
			break
		}
	}
example.texttext
fetched [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

É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