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, efor { ... }repete até encontrar umbreak, umreturnou umpanic.for i, v := range xpercorre 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.moddeclarargo 1.22ou 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
breaksem label dentro de umselectouswitchsó 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.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 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.gogofor (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.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
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.gogofor { 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 sobre | Primeiro valor | Segundo valor |
|---|---|---|
| Slice ou array | índice | cópia do elemento |
| String | offset em bytes | rune (code point Unicode) |
| Map | chave | valor |
| Channel | valor recebido | nenhum |
Inteiro n (Go 1.22) | 0 a n-1 | nenhum |
| Função iteradora (Go 1.23) | o que ela produzir | o 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.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
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.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() }
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.texttextmain.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.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
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.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 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.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"
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.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") }
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.texttextmain.go:20:2: unreachable code
e o staticcheck reporta o próprio break:
example.texttextmain.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.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
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.gogofor _, 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.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]
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.gogofor 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.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]
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
- 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
- Go 1.22 Release Notes: https://go.dev/doc/go1.22
- Go 1.23 Release Notes: 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
