Voltar ao blog

A palavra-chave for em Go: ciclos, range, variáveis de ciclo e labels

Como funciona a palavra-chave for do Go: as três formas de ciclo, for range sobre slices, maps, strings, channels, inteiros e iteradores, as variáveis de ciclo do Go 1.22, break e continue com labels e os erros mais comuns com ciclos.

A palavra-chave for em Go: ciclos, range, variáveis de ciclo e labels

O for é a única palavra-chave de ciclo em Go. Existe em três formas: o ciclo de três cláusulas for i := 0; i < n; i++, o ciclo só com condição for cond, que é o while do Go, e o ciclo infinito for { ... }. Acrescenta range e a mesma palavra-chave percorre slices, arrays, maps, strings, channels, inteiros e funções iteradoras. O Go não tem as palavras-chave while, do, foreach nem loop (especificação do Go, For statements).

Resumo rápido

  • for init; cond; post { ... } é o ciclo com contador. Não leva parênteses à volta das cláusulas, e as chavetas são obrigatórias.
  • for cond { ... } é um ciclo while, e for { ... } repete até encontrar um break, um return ou um panic.
  • for i, v := range x percorre um slice, array, map, string, channel, inteiro (Go 1.22) ou função iteradora (Go 1.23). A ordem de um map é aleatória.
  • Desde o Go 1.22, cada iteração recebe uma variável de ciclo nova se o go.mod declarar go 1.22 ou posterior. As goroutines lançadas num ciclo já não veem todas o último valor.
  • O valor do range é uma cópia. Atribui a items[i] para alterar o slice.
  • Um break sem label dentro de um select ou switch só sai dessa instrução. Usa uma label para sair do ciclo.

Quais são as formas de um ciclo for em Go?

A especificação define uma única instrução for com três formatos. Só diferem nas cláusulas que escreves.

O ciclo de três cláusulas

A instrução de inicialização corre uma vez, a condição é verificada antes de cada iteração e a instrução final corre depois de cada iteração. Este ciclo repete um health check instável com backoff exponencial:

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 só existe dentro do ciclo. Qualquer uma das três cláusulas pode ser omitida. for ; attempt <= 5; { é válido, e o gofmt reescreve-o como for attempt <= 5 {.

O hábito de C de pôr as cláusulas entre parênteses não compila:

example.gogo
	for (i := 0; i < 3; i++) {
example.texttext
./main.go:6:9: syntax error: unexpected :=, expected )

O ciclo só com condição (while)

Retira a instrução de inicialização e a instrução final e o for passa a ser um ciclo while. Este esvazia uma fila de tarefas até ela ficar vazia:

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

Se escreveres while em vez disso, obténs um erro de parsing, porque while é um identificador comum em Go:

example.texttext
./main.go:7:8: syntax error: unexpected name pending at end of statement

O compilador lê while como uma expressão, encontra outro nome a seguir e desiste. A mensagem não diz nada sobre ciclos, o que confunde quem vem de outras linguagens.

O ciclo infinito

Sem nenhuma cláusula, for { ... } corre até que algo lá dentro saia. Os servidores usam este formato para ciclos de accept e para 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)
	}

O ciclo termina quando o listener é fechado e o Accept devolve net.ErrClosed. O for {} substitui o while (true) e o for (;;), e o compilador sabe que nunca termina sozinho. Uma função que acaba num for {} sem break não precisa de um return final.

Como funciona o for range?

O range transforma um ciclo for numa iteração sobre um valor. O que recebes depende do tipo:

Range sobrePrimeiro valorSegundo valor
Slice ou arrayíndicecópia do elemento
Stringoffset em bytesrune (code point Unicode)
Mapchavevalor
Channelvalor recebidonenhum
Inteiro n (Go 1.22)0 a n-1nenhum
Função iteradora (Go 1.23)o que ela produziro que ela produzir

Podes omitir a segunda variável (for i := range items) ou ignorar a primeira com _ (for _, item := range items). for range n sem variáveis executa um bloco n vezes.

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

O ciclo sobre o map é o que merece mais atenção. O runtime torna a ordem de iteração dos maps aleatória de propósito, para que o código não possa depender dela. Para imprimir os códigos de estado por ordem, o maps.Keys devolve um iterador sobre as chaves e o slices.Sorted recolhe-as e ordena-as. A palavra-chave map em Go aborda a iteração de maps com mais detalhe.

O ciclo sobre a string produz runes, e os offsets são posições em bytes, por isso o é começa no offset 3 e ocupa dois bytes. Usa for i := 0; i < len(s); i++ com s[i] quando quiseres os bytes em bruto.

O ciclo sobre o channel para quando o channel é fechado. Se ninguém o fechar, o ciclo bloqueia para sempre assim que o buffer fica vazio. A palavra-chave chan em Go explica quem deve fechar um channel.

As funções iteradoras (Go 1.23) permitem que qualquer função com a assinatura func(yield func(K, V) bool) funcione com range. A biblioteca padrão devolve-as a partir de maps.Keys, maps.All, slices.All, slices.Backward e strings.Lines, e podes escrever as tuas para resultados paginados de uma API ou para linhas de uma base de dados. O range tem mais regras do que cabem aqui. A secção sobre range do guia das palavras-chave do Go é a versão resumida.

O que mudou nas variáveis de ciclo no Go 1.22?

Antes do Go 1.22, um ciclo for declarava as suas variáveis uma única vez e reutilizava-as em todas as iterações. Uma closure ou goroutine que capturasse a variável obtinha o valor que ela tivesse no momento em que a closure corria, que normalmente era o último. O Go 1.22 mudou isto para que cada iteração receba uma variável nova (blog do Go, Fixing For Loops in Go 1.22).

Este health checker lança uma goroutine por cada host de base de dados:

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()
}

Com go 1.21 no go.mod, três execuções imprimiram checking db-3 três vezes cada uma, e o go vet reportou:

example.texttext
main.go:16:28: loop variable host captured by func literal

Com go 1.27 no go.mod, o mesmo código verifica os três hosts pela ordem em que as goroutines terminarem, e o go vet não tem nada a dizer.

A mudança está ligada à linha go do go.mod, não à toolchain com que fazes o build. Um compilador Go 1.27 continua a compilar um módulo que declara go 1.21 com a semântica antiga, por isso uma dependência escrita antes do 1.22 continua a funcionar da forma como os seus autores a testaram. A mudança aplica-se tanto à forma de três cláusulas como ao range. O antigo truque host := host dentro do ciclo é agora redundante. O modernizador forvar do go fix apaga-o por ti. A palavra-chave func em Go aborda as closures e as variáveis capturadas com mais detalhe.

Porque é que alterar o valor do range não altera o slice?

A segunda variável do range é uma cópia do elemento. Num slice de structs, alterar um campo da cópia deixa o slice intacto:

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

O primeiro ciclo define Paid numa cópia que é deitada fora no fim de cada iteração. O segundo ciclo acede ao slice pelo índice e altera o elemento real. O Go 1.22 não mudou isto. A correção do 1.22 deu a cada iteração a sua própria variável, e essa variável continua a ser uma cópia. Num slice de apontadores ([]*Invoice), a cópia é um apontador e inv.Paid = true atualiza mesmo a fatura.

Guardar um apontador para um elemento

&invoices[i] dá-te um apontador para o array subjacente do slice. Esse apontador fica desatualizado se mais tarde o slice crescer para além da sua capacidade, porque o append copia os elementos para um array novo:

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 continua a apontar para o array antigo, por isso a escrita nunca chega a invoices. Guarda o índice (largeIdx = i) em vez de um apontador quando o slice puder crescer.

Como funcionam o break e o continue num ciclo for?

O break sai do for, switch ou select mais interior. O continue salta para a iteração seguinte do for mais interior. Ambos aceitam uma label opcional que dá nome a um ciclo exterior.

Uma label é um identificador seguido de dois pontos, colocado na linha antes do ciclo. Esta pesquisa para no primeiro erro encontrado em vários ficheiros 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"

Sem a label, o break só sairia do ciclo interior e a pesquisa passaria para o terceiro ficheiro. continue files saltaria o resto do ficheiro atual e passaria para o seguinte.

break dentro de select

Este bug compila e parece correto numa leitura rápida. Um worker lê tarefas até o seu context ser cancelado:

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")
}

O break sai do select, não do for. O worker imprime worker stopping e volta diretamente para o select, onde o ctx.Done() continua fechado, por isso imprime a mesma linha outra vez num ciclo apertado. As métricas nunca são despejadas. O go vet reporta o código morto a seguir ao ciclo:

example.texttext
main.go:20:2: unreachable code

e o staticcheck reporta o próprio break:

example.texttext
main.go:15:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)

A correção é pôr uma label no ciclo:

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

Um return no case ctx.Done() também funciona, quando não há nada a fazer depois do ciclo. A mesma armadilha aplica-se a um break dentro de um switch que está dentro de um ciclo.

Erros comuns com ciclos for

defer dentro de um ciclo

O defer corre quando a função retorna, não quando a iteração termina. Um ciclo que abre um ficheiro e faz defer do Close mantém todos os ficheiros abertos até a função inteira terminar:

example.gogo
	for _, p := range paths {
		f, err := os.Open(p)
		if err != nil {
			return 0, err
		}
		defer f.Close()
		// ...
	}

Com meia dúzia de caminhos não se nota nada, mas dez mil ficheiros de log deixam o processo sem descritores de ficheiro. Nem o go vet nem o staticcheck o assinalam. Passa o corpo para uma função, para que cada defer corra no fim da sua própria chamada, ou chama f.Close() diretamente no fim da iteração. Erros comuns em Go a evitar aborda este caso com um exemplo completo.

Acrescentar elementos a um slice enquanto o percorres com range

O range avalia o seu operando uma única vez, antes da primeira iteração. Fazer append dentro do ciclo não o faz correr mais vezes:

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]

O ciclo correu duas vezes, uma por cada elemento original. Isso torna-o seguro, mas também significa que um crawler não consegue descobrir URLs novos desta forma. Usa um ciclo com condição sobre uma fila, como o for len(queue) > 0 acima, quando a lista de trabalho cresce à medida que avanças.

Alterar um map durante o range

Apagar a chave atual enquanto percorres um map com range é permitido e seguro. Este ciclo remove as sessões inativas:

example.gogo
	for id, idle := range sessions {
		if idle > 30 {
			delete(sessions, id)
		}
	}

Acrescentar chaves durante o ciclo também é permitido, mas a especificação diz que uma entrada nova «pode ser produzida durante a iteração ou pode ser ignorada» ("may be produced during the iteration or may be skipped"). Recolhe as chaves novas num map separado e junta-as no fim se cada uma tiver de ser processada.

Capturar o endereço da variável de ciclo

ptrs = append(ptrs, &inv) dentro de for _, inv := range invoices dá-te apontadores para cópias, não para os elementos do slice. Antes do Go 1.22, todos os apontadores apontavam para a mesma variável e mostravam a última fatura. Desde o Go 1.22, cada apontador aponta para a sua própria cópia, o que está correto mas continua desligado do slice. Usa &invoices[i] se quiseres alterar os originais, e tem em conta o aviso sobre a capacidade acima.

Existe um ciclo while ou do-while em Go?

Não. O Effective Go diz que o ciclo for do Go «unifica o for e o while e não existe do-while». A forma só com condição cobre aquilo para que outras linguagens usam o while.

Um do-while executa o corpo uma vez antes de verificar a condição. Em Go, escreves isso como um ciclo infinito com a verificação no fim. As chamadas a APIs paginadas são o caso habitual, porque vais sempre buscar a primeira página:

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]

São mais duas linhas do que teria um do { } while (cursor != ""), e a condição de saída fica no fundo, onde a irias procurar.

Onde entra o LevelUpGo

O LevelUpGo ensina Go através de exercícios que executam código Go real no navegador. O Go Basics tem duas lições sobre o for: uma sobre as três formas de ciclo e outra sobre range, break e continue. O Concurrency Fundamentals aborda os ciclos que mais complicam a vida em código concorrente, como lançar goroutines num ciclo, percorrer channels com range e sair de um ciclo for/select quando há cancelamento. Para as outras 24 palavras reservadas, vê Palavras-chave do Go: as 25 explicadas.

Perguntas frequentes

O Go tem um ciclo while?

Não, o Go não tem a palavra-chave while. Escreve for cond { ... } para um ciclo while e for { ... } para um ciclo infinito. Escrever while dá um erro de sintaxe, porque o compilador trata-o como um nome comum.

O Go tem um ciclo foreach?

O for ... range é o foreach do Go. for _, v := range items visita todos os elementos de um slice, e a mesma forma funciona para arrays, maps, strings, channels, inteiros e funções iteradoras.

Como faço um ciclo que repete N vezes em Go?

Desde o Go 1.22, for i := range n corre com i de 0 a n-1, e for range n corre n vezes sem variável. A forma de três cláusulas for i := 0; i < n; i++ funciona em todas as versões.

Como percorro um map com as chaves ordenadas?

Recolhe e ordena as chaves e depois percorre-as com range. Desde o Go 1.23, for _, k := range slices.Sorted(maps.Keys(m)) faz as duas coisas numa só linha. A ordem de iteração de um map é aleatória de propósito, por isso nunca dependas dela.

Como percorro um slice de trás para a frente em Go?

Usa for i := len(s) - 1; i >= 0; i-- ou, desde o Go 1.23, for i, v := range slices.Backward(s), que produz o índice e o valor do último elemento até ao primeiro.

Porque é que a minha goroutine num ciclo for imprime sempre o mesmo valor?

O teu módulo está numa versão do Go anterior à 1.22, em que a variável de ciclo é partilhada por todas as iterações. Muda a linha go do go.mod para go 1.22 ou posterior e cada iteração passa a ter a sua própria variável. Numa versão mais antiga, passa o valor como argumento (go func(h string) { ... }(host)) ou copia-o com host := host dentro do ciclo.

Fontes

Escreve Go como um engenheiro sénior

Lições interativas no navegador. As primeiras são grátis.

Experimenta uma lição grátisOu cria uma conta gratuita