Blog

Controle de fluxo em Go: if, for e switch

Go se vira com if, uma única palavra-chave de laço chamada for e um switch que não cai no próximo caso. Aprenda cada forma, a mudança do Go 1.22 que dá a cada iteração sua própria variável, e o break com label.

Go tem menos palavras-chave de controle de fluxo que a maioria das linguagens. Tem if, tem switch, e tem exatamente uma palavra-chave de laço, for. Nada de while, nada de do, nada de foreach. Cada um desses trabalhos é um formato diferente de for.

Este post passa por cada formato, além de break, continue, labels e a mudança do Go 1.22 que corrigiu um bug clássico de laço. Todo programa abaixo rodou no Go 1.26, e a saída foi colada direto da execução.

if sem parênteses e com chaves obrigatórias

Um if em Go não tem parênteses em volta da condição, e as chaves são sempre obrigatórias, mesmo para uma linha só.

package main

import "fmt"

func main() {
	temp := 31
	if temp > 30 {
		fmt.Println("hot")
	} else if temp > 15 {
		fmt.Println("mild")
	} else {
		fmt.Println("cold")
	}
}

Ele imprime:

hot

A condição precisa ser um bool. O Go não trata 0, uma string vazia ou nil como falso, então if count { ... } não compila quando count é um int. Você escreve if count > 0 e diz o que quer dizer.

if com uma instrução curta

Um if em Go pode rodar uma instrução curta antes da condição, separada por ponto e vírgula. Você vai ver esse formato o tempo todo, porque é assim que código Go busca algo e confere o resultado numa linha só:

package main

import "fmt"

func main() {
	ages := map[string]int{"ana": 31, "ben": 0}

	if age, ok := ages["ben"]; ok {
		fmt.Println("ben is", age)
	}
	if age, ok := ages["cai"]; ok {
		fmt.Println("cai is", age)
	} else {
		fmt.Println("no age for cai, got", age)
	}
}

Ele imprime:

ben is 0
no age for cai, got 0

age, ok := ages["ben"] roda primeiro. Depois ok é a condição. Ler uma chave que não existe num map dá o zero value, então age é 0 tanto para “ben” quanto para “cai”. Só o ok diz que “ben” está mesmo lá com idade 0 e “cai” não está lá de jeito nenhum. A parte sobre maps cobre essa forma comma-ok por completo.

Repare que age também pode ser usada no else. Uma variável declarada na instrução curta vive durante o if inteiro, incluindo cada else if e else.

Quando o if termina, a variável some:

package main

import "fmt"

func main() {
	ages := map[string]int{"ana": 31}
	if age, ok := ages["ana"]; ok {
		fmt.Println("found", age)
	}
	fmt.Println(age)
}

O build falha:

./main.go:10:14: undefined: age

Esse é o motivo de usar a forma curta. A variável existe exatamente onde ela é útil e não vaza para o resto da função, onde alguém poderia ler um valor velho por engano.

for: a única palavra-chave de laço

Go tem uma palavra-chave de laço, for, e ela tem três formatos que cobrem o que outras linguagens dividem entre for, while e do.

package main

import "fmt"

func main() {
	// Three parts: init; condition; post.
	var seen []int
	for i := 0; i < 3; i++ {
		seen = append(seen, i)
	}
	fmt.Println(seen)

	// Condition only: this is Go's while loop.
	n := 1
	for n < 100 {
		n *= 3
	}
	fmt.Println(n)

	// No condition: loop until something breaks out.
	tries := 0
	for {
		tries++
		if tries == 4 {
			break
		}
	}
	fmt.Println("tries:", tries)
}

Ele imprime:

[0 1 2]
243
tries: 4

A forma de três partes é a do C e do Java, sem os parênteses. Tire a inicialização e a instrução pós-iteração e você tem um laço while. Tire a condição também e o laço roda para sempre, até um break ou um return parar. O laço de accept de um servidor e um worker esperando jobs têm os dois a cara desse for sozinho.

i++ é uma instrução em Go, não uma expressão. Você não pode escrever x := i++, e não existe ++i.

for range sobre slices e strings

Um laço for range percorre os elementos de uma coleção e te dá um índice e um valor a cada passada.

package main

import "fmt"

func main() {
	fruits := []string{"fig", "kiwi", "plum"}
	for i, f := range fruits {
		fmt.Println(i, f)
	}

	prices := []int{10, 20, 30}
	for _, p := range prices {
		p *= 2
	}
	fmt.Println(prices)

	for i := range prices {
		prices[i] *= 2
	}
	fmt.Println(prices)
}

Ele imprime:

0 fig
1 kiwi
2 plum
[10 20 30]
[20 40 60]

Use _ para a parte que você não precisa. O Go se recusa a compilar uma variável sem uso, então _ é como você diz “estou ignorando o índice de propósito”.

O segundo laço é uma armadilha que vale ver uma vez. p é uma cópia de cada elemento, então dobrar p não muda nada no slice. Para mudar os elementos, percorra os índices e escreva através de prices[i], como faz o terceiro laço.

Percorrer uma string dá algo um pouco diferente:

package main

import "fmt"

func main() {
	for i, r := range "héllo" {
		fmt.Println(i, string(r))
	}
}

Ele imprime:

0 h
1 é
3 l
4 l
5 o

O índice pula de 1 para 3. Um range sobre uma string percorre runes (code points Unicode), e o índice é a posição em bytes onde cada uma começa. O é ocupa dois bytes em UTF-8, então a próxima rune começa no byte 3. A parte sobre strings, bytes e runes explica por quê.

for range sobre maps: a ordem é aleatória de propósito

Percorrer um map te dá cada chave e valor, mas o Go não promete nada sobre a ordem, e o runtime muda essa ordem de propósito de um laço para outro:

package main

import "fmt"

func main() {
	stock := map[string]int{"apples": 5, "bread": 2, "cheese": 7, "dates": 1}

	for k, v := range stock {
		fmt.Println(k, v)
	}
}

Uma execução imprimiu:

cheese 7
apples 5
dates 1
bread 2

Rode de novo e as linhas podem sair em outra ordem. Nada no map mudou. Cada range sobre um map começa de uma posição escolhida ao acaso.

O time do Go colocou essa aleatoriedade para que programas não pudessem depender, sem ninguém perceber, de uma ordem que nunca foi garantida. Código que funciona porque as chaves “sempre” saem numa ordem quebra no dia em que essa ordem muda. Embaralhar toda vez faz o bug aparecer nos seus testes.

Quando você precisa de uma ordem estável, peça por ela. maps.Keys devolve as chaves, slices.Sorted junta essas chaves num slice ordenado, e você percorre esse slice:

package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	stock := map[string]int{"apples": 5, "bread": 2, "cheese": 7, "dates": 1}

	for _, k := range slices.Sorted(maps.Keys(stock)) {
		fmt.Println(k, stock[k])
	}
}

Ele imprime:

apples 5
bread 2
cheese 7
dates 1

Esta versão imprime a mesma coisa toda vez. A parte sobre maps vai mais fundo.

for range sobre channels

Um for range sobre um channel recebe valores até o channel ser fechado.

package main

import "fmt"

func main() {
	jobs := make(chan string, 3)
	jobs <- "resize"
	jobs <- "upload"
	jobs <- "notify"
	close(jobs)

	for job := range jobs {
		fmt.Println("doing", job)
	}
	fmt.Println("channel closed, loop done")
}

Ele imprime:

doing resize
doing upload
doing notify
channel closed, loop done

Aqui só tem uma variável de laço, o valor. Um channel não tem índice. Se ninguém fechasse jobs, o laço esperaria para sempre por um quarto valor. Channels ganham uma parte só deles mais adiante na série. Por enquanto, basta saber que for range é como você lê de um channel.

for range sobre um inteiro

Desde o Go 1.22, for i := range n conta de 0 até n, sem incluir esse último valor.

package main

import "fmt"

func main() {
	var seen []int
	for i := range 5 {
		seen = append(seen, i)
	}
	fmt.Println(seen)

	total := 0
	for i := range 4 {
		total += i
	}
	fmt.Println(total)
}

Ele imprime:

[0 1 2 3 4]
6

É o mesmo que for i := 0; i < 5; i++, com menos coisa para errar. Não tem condição para digitar errado como <= nem incremento para esquecer. Agora esse é o jeito normal de repetir um número fixo de vezes, e você vai ver ele por toda esta série.

Cada iteração ganha sua própria variável de laço

Desde o Go 1.22, cada passada por um laço for ganha uma cópia nova da variável de laço, e isso importa assim que uma closure captura essa variável.

package main

import "fmt"

func main() {
	var prints []func()
	for i := 0; i < 3; i++ {
		prints = append(prints, func() { fmt.Println(i) })
	}
	for _, p := range prints {
		p()
	}
}

Ele imprime:

0
1
2

O primeiro laço monta três funções pequenas, e cada uma imprime i. Nenhuma delas roda até o segundo laço. Nessa hora o primeiro laço já terminou, e mesmo assim cada função imprime o valor que i tinha quando aquela função foi criada.

Parece óbvio. Antes do Go 1.22 não era verdade, e o mesmo programa imprimia 3 3 3. Era um dos bugs mais comuns em Go, e doía mais quando a closure era uma goroutine iniciada dentro de um laço.

Explicado como se você tivesse dez anos

Imagine que você está entregando três envelopes lacrados, e do lado de fora de cada um você escreve “abra depois para ver o número no quadro”.

O Go antigo tinha um quadro branco. Você escrevia 0 nele e entregava um envelope, depois apagava e escrevia 1, depois 2, depois 3. Quando seus amigos finalmente abriam os envelopes e olhavam para o quadro, todos viam 3.

O Go novo dá a cada envelope o seu próprio quadrinho, grampeado lá dentro. O envelope um tem um quadro com 0, e nada que você faça depois mexe nele.

A versão precisa

Uma closure não copia as variáveis que usa. Ela guarda uma referência a elas. Então o que importa é quantas variáveis existem.

Antes do Go 1.22, um laço for declarava sua variável uma vez, e cada iteração atualizava essa mesma variável. Toda closure apontava para o único i, e todas liam o valor final dele, 3.

A partir do Go 1.22, cada iteração declara uma variável nova. Na forma de três partes, o Go copia o valor da iteração anterior para a variável nova antes de rodar a instrução pós-iteração. Cada closure aponta para o seu próprio i, que ninguém muda depois que aquela iteração termina. A mesma regra vale para laços for range.

Onde a analogia falha: os quadrinhos não são só de leitura. Se o código dentro de uma iteração muda i, a closure criada naquela iteração vê a mudança. O que deixa de acontecer é iterações posteriores sobrescreverem o valor.

Qual regra vale é definido pelo go.mod

O comportamento novo depende da linha go no go.mod do seu módulo, não da versão do Go que você tem instalada. Rode aquele mesmo programa com o Go 1.26, num módulo cujo go.mod diz go 1.21, e você tem o resultado antigo:

$ go run .
3
3
3

O compilador é novo, mas mantém a semântica antiga de laço para módulos antigos, para que atualizar o Go não mude o que código existente faz. Se você copiar um laço para um projeto antigo, confira o go.mod dele. A parte sobre pacotes e módulos cobre esse arquivo. As closures em si aparecem direito na próxima parte, sobre funções.

break, continue e labels

break sai do laço mais interno, e continue pula para a próxima iteração dele.

package main

import "fmt"

func main() {
	var kept []int
	for i := range 10 {
		if i%2 == 1 {
			continue
		}
		if i > 6 {
			break
		}
		kept = append(kept, i)
	}
	fmt.Println(kept)
}

Ele imprime:

[0 2 4 6]

Números ímpares caem no continue, então nunca chegam ao append. No 8 o laço cai no break e termina, então o 8 também nunca aparece.

As duas palavras-chave só alcançam o laço em que estão diretamente. Com laços aninhados, muitas vezes esse é o laço errado. Digamos que você está procurando algo numa grade e quer parar no momento em que encontra. Um break simples no laço interno só termina a linha atual, e o laço externo continua.

Um label resolve isso. Você dá um nome ao laço externo, e depois faz break ou continue com esse nome:

package main

import "fmt"

func main() {
	grid := [][]int{
		{1, 4, 7},
		{2, 5, 8},
		{3, 6, 9},
	}

	checked := 0
search:
	for r, row := range grid {
		for c, v := range row {
			checked++
			if v == 5 {
				fmt.Println("found 5 at", r, c)
				break search
			}
		}
	}
	fmt.Println("cells checked:", checked)

	for r, row := range grid {
	cells:
		for _, v := range row {
			if v%2 == 0 {
				fmt.Println("row", r, "first even:", v)
				break cells
			}
		}
	}
}

Ele imprime:

found 5 at 1 1
cells checked: 5
row 0 first even: 4
row 1 first even: 2
row 2 first even: 6

break search termina o laço externo, então a busca para depois de 5 células em vez de conferir as 9. O label fica numa linha própria, seguido de dois-pontos, logo antes do laço que ele nomeia.

O segundo laço mostra um label no laço interno. Ali, break cells faz exatamente o que um break simples faria. É válido, mas não serve para nada, e código de verdade só coloca label no laço que precisa alcançar lá de dentro. O continue também aceita label, e continue search pularia para a próxima linha.

Go também tem goto, que salta para um label na mesma função, mas você raramente vai encontrar ele fora de código gerado.

switch: sem fallthrough por padrão

Um switch em Go roda o primeiro caso que bate e para. Você não escreve break no fim de cada caso.

package main

import "fmt"

func kind(day string) string {
	switch day {
	case "sat", "sun":
		return "weekend"
	case "mon", "tue", "wed", "thu", "fri":
		return "weekday"
	default:
		return "not a day"
	}
}

func main() {
	for _, d := range []string{"sun", "wed", "moon"} {
		fmt.Println(d, kind(d))
	}
}

Ele imprime:

sun weekend
wed weekday
moon not a day

Um caso pode listar vários valores, separados por vírgula, e bate se qualquer um deles bater. O default roda quando nada mais bateu. Ele é opcional, e pode ficar em qualquer posição da lista.

Em C e Java, esquecer o break faz um caso seguir direto para o próximo. O Go inverteu isso porque o break esquecido era bug muito mais vezes do que escolha.

switch sem condição

Um switch sem nada depois da palavra-chave testa cada caso como um booleano, e é o jeito mais limpo de escrever uma longa cadeia de if-else.

package main

import "fmt"

func grade(score int) string {
	switch {
	case score >= 90:
		return "A"
	case score >= 75:
		return "B"
	case score >= 50:
		return "C"
	default:
		return "F"
	}
}

func main() {
	for _, s := range []int{95, 75, 60, 12} {
		fmt.Println(s, grade(s))
	}
}

Ele imprime:

95 A
75 B
60 C
12 F

Os casos são conferidos de cima para baixo, e o primeiro verdadeiro vence. É por isso que 95 tira A e não B, mesmo que 95 >= 75 também seja verdade. Ordene os casos do mais específico para o menos específico.

Um switch também aceita uma instrução curta, igual ao if: switch n := len(items); { ... } limita o escopo de n ao switch.

fallthrough, quando você realmente quer

A palavra-chave fallthrough faz um caso continuar no corpo do caso seguinte. Você precisa pedir por isso explicitamente.

package main

import "fmt"

func main() {
	level := 2
	switch level {
	case 3:
		fmt.Println("send a page")
		fallthrough
	case 2:
		fmt.Println("send an email")
		fallthrough
	case 1:
		fmt.Println("write to the log")
	}
}

Ele imprime:

send an email
write to the log

O nível 2 bateu com case 2, que imprimiu e depois caiu para o case 1. Repare que o fallthrough não confere o valor do próximo caso. Ele roda o próximo corpo sem condição. Essa é a surpresa que pega quem espera que ele continue comparando.

O fallthrough precisa ser a última instrução de um caso, e não pode ser usado no último caso. Você raramente vai precisar dele. Quando um problema parece pedir fallthrough, um caso com vários valores ou um if curto normalmente fica mais legível.

Uma primeira olhada no type switch

Um type switch escolhe o caminho pelo tipo dinâmico de um valor, e não pelo valor em si.

package main

import "fmt"

func describe(x any) string {
	switch v := x.(type) {
	case int:
		return fmt.Sprintf("an int, doubled is %d", v*2)
	case string:
		return fmt.Sprintf("a string of %d bytes", len(v))
	case nil:
		return "nothing at all"
	default:
		return fmt.Sprintf("something else: %T", v)
	}
}

func main() {
	fmt.Println(describe(21))
	fmt.Println(describe("go"))
	fmt.Println(describe(nil))
	fmt.Println(describe(2.5))
}

Ele imprime:

an int, doubled is 42
a string of 2 bytes
nothing at all
something else: float64

any quer dizer “um valor de qualquer tipo”. Dentro de cada caso, v tem o tipo daquele caso, então v*2 é válido no caso int e len(v) é válido no caso string. A parte sobre interfaces explica o que any realmente é e quando um type switch é a ferramenta certa.

O que lembrar

  • Um if pode começar com uma instrução curta, if v, ok := f(); ok. A variável vive durante toda a cadeia de if e else, e em nenhum outro lugar.
  • for é o único laço do Go: de três partes, só com condição como um laço while, sozinho para “para sempre”, e for range sobre slices, strings, maps, channels e inteiros.
  • A ordem de iteração de um map é aleatória de propósito. Ordene as chaves com slices.Sorted(maps.Keys(m)) quando a ordem importa.
  • Desde o Go 1.22, cada iteração ganha sua própria variável de laço, então as closures veem o valor da sua própria passada. A linha go no go.mod decide qual regra vale.
  • break e continue alcançam o laço mais interno. Coloque um label no laço externo para alcançar esse.
  • O switch para depois do primeiro caso que bate. Use vários valores num caso, ou switch sem condição para uma cadeia de if-else. O fallthrough roda o próximo corpo sem conferir.

Go tem um laço só e um switch que para sozinho, então há menos para lembrar e menos para errar.

Quanto este post te ajudou?

Clique em um coração para avaliar!

Média das avaliações 0 / 5. Total de votos: 0

Nenhum voto até agora. Seja o primeiro a avaliar este post.