O que é Range em Go?

A palavra-chave range é o mecanismo principal de iteração em Go. Ela permite percorrer elementos de estruturas de dados como slices, maps, strings, arrays e channels de forma idiomática e segura. Diferente de loops tradicionais baseados em índices, range abstrai os detalhes de iteração e fornece acesso direto tanto ao índice quanto ao valor de cada elemento.

Em Go, range é sempre usado dentro de um loop for, formando a construção for ... range. Essa é a forma recomendada pela comunidade para percorrer coleções, pois elimina erros comuns como acessar índices fora dos limites (out of bounds) e torna o código mais legível e expressivo. Se você vem de linguagens como Python (for item in list) ou JavaScript (for...of), o range de Go serve ao mesmo propósito.

A versatilidade do range é uma das razões pelas quais Go consegue manter uma sintaxe minimalista — uma única construção serve para iterar sobre todos os tipos de coleções da linguagem, desde slices simples até channels concorrentes.

Range com slices e arrays

A forma mais comum de usar range é iterar sobre slices e arrays. O range retorna dois valores: o índice e uma cópia do elemento:

frutas := []string{"maçã", "banana", "laranja", "manga"}

// Usando índice e valor
for i, fruta := range frutas {
    fmt.Printf("Índice %d: %s\n", i, fruta)
}

// Saída:
// Índice 0: maçã
// Índice 1: banana
// Índice 2: laranja
// Índice 3: manga

Ignorando o índice com blank identifier

Quando você precisa apenas do valor, use o identificador blank _ para descartar o índice:

numeros := []int{10, 20, 30, 40, 50}

soma := 0
for _, n := range numeros {
    soma += n
}
fmt.Println("Soma:", soma) // Soma: 150

Usando apenas o índice

Se você precisa apenas do índice, omita a segunda variável:

nomes := []string{"Ana", "Bruno", "Carla"}

for i := range nomes {
    fmt.Printf("Posição %d disponível\n", i)
}

Esse padrão é útil quando você precisa modificar os elementos originais do slice usando o índice para acessar diretamente a posição na memória, já que range fornece cópias dos valores.

Range com maps

Ao iterar sobre um map, range retorna a chave e o valor de cada par:

capitais := map[string]string{
    "Brasil":    "Brasília",
    "Argentina": "Buenos Aires",
    "Chile":     "Santiago",
}

for pais, capital := range capitais {
    fmt.Printf("Capital de %s: %s\n", pais, capital)
}

Importante: a ordem de iteração sobre maps em Go é aleatória por design. O runtime deliberadamente randomiza a ordem para evitar que desenvolvedores dependam de uma ordenação específica. Se você precisa de ordem determinística, ordene as chaves em um slice separado:

import "sort"

paises := make([]string, 0, len(capitais))
for pais := range capitais {
    paises = append(paises, pais)
}
sort.Strings(paises)

for _, pais := range paises {
    fmt.Printf("%s → %s\n", pais, capitais[pais])
}

Range com strings

Quando usado com strings, range itera sobre runes (pontos de código Unicode), não bytes. Isso é crucial para textos em português que contêm acentos e caracteres especiais:

texto := "Olá, São Paulo!"

for i, r := range texto {
    fmt.Printf("Byte %d: %c (U+%04X)\n", i, r, r)
}

Cada iteração retorna o índice do byte inicial da rune e o valor da rune como rune (alias para int32). Caracteres multi-byte como “ã” e “ó” ocupam mais de um byte em UTF-8, então os índices podem pular valores. Essa é a forma correta de processar texto Unicode em Go — nunca itere por bytes quando estiver lidando com texto legível.

// Contando caracteres reais (runes) vs bytes
palavra := "programação"
fmt.Println("Bytes:", len(palavra))              // 12 (ã = 2 bytes)
fmt.Println("Caracteres:", utf8.RuneCountInString(palavra)) // 11

Range com channels

O range sobre um channel recebe valores continuamente até que o canal seja fechado:

func gerador(nums ...int) <-chan int {
    ch := make(chan int)
    go func() {
        for _, n := range nums {
            ch <- n
        }
        close(ch) // ESSENCIAL: sem close, range bloqueia para sempre
    }()
    return ch
}

func main() {
    for valor := range gerador(1, 2, 3, 4, 5) {
        fmt.Println(valor)
    }
}

Esse padrão é fundamental em pipelines de concorrência e no modelo producer-consumer. O range sobre channel bloqueia automaticamente quando não há dados disponíveis e termina quando o canal é fechado. Se o canal nunca for fechado com close(), o loop bloqueará indefinidamente — um cenário que pode causar vazamentos de goroutines.

Range over integers (Go 1.22+)

A partir do Go 1.22, o range pode iterar sobre números inteiros, eliminando a necessidade de loops for i := 0; i < n; i++ em muitos casos:

// Go 1.22+: range sobre inteiro
for i := range 5 {
    fmt.Println(i) // 0, 1, 2, 3, 4
}

// Equivalente ao loop clássico:
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

Essa adição torna o código mais expressivo e alinhado com a filosofia de simplicidade de Go. É especialmente útil quando você precisa executar algo N vezes sem necessariamente usar o índice:

for range 3 {
    fmt.Println("Tentando conexão...")
}

Controle de fluxo dentro do range

Break — sair do loop

numeros := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

for _, n := range numeros {
    if n > 5 {
        break // sai do loop completamente
    }
    fmt.Println(n)
}
// Imprime: 1, 2, 3, 4, 5

Continue — pular iteração

for _, n := range numeros {
    if n%2 == 0 {
        continue // pula números pares
    }
    fmt.Println(n)
}
// Imprime: 1, 3, 5, 7, 9

Labels para loops aninhados

Quando há loops range aninhados, labels permitem controlar qual loop o break ou continue afeta:

matriz := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}

externo:
for _, linha := range matriz {
    for _, valor := range linha {
        if valor == 5 {
            break externo // sai de ambos os loops
        }
        fmt.Print(valor, " ")
    }
}
// Imprime: 1 2 3 4

Armadilhas comuns com range

Captura de variável de loop (antes do Go 1.22)

Um dos bugs mais comuns em Go (corrigido no Go 1.22) envolve closures em goroutines:

// PROBLEMA (Go < 1.22): todas as goroutines capturam o mesmo ponteiro
valores := []int{1, 2, 3, 4, 5}
for _, v := range valores {
    go func() {
        fmt.Println(v) // sempre imprime 5 (o último valor)
    }()
}

// SOLUÇÃO 1: passar como parâmetro
for _, v := range valores {
    go func(val int) {
        fmt.Println(val) // valor correto
    }(v)
}

// SOLUÇÃO 2 (Go 1.22+): o compilador cria nova variável por iteração
for _, v := range valores {
    go func() {
        fmt.Println(v) // funciona corretamente no Go 1.22+
    }()
}

Range fornece cópias, não referências

type Pessoa struct {
    Nome  string
    Idade int
}

pessoas := []Pessoa{
    {"Ana", 25},
    {"Bruno", 30},
}

// ERRADO: modifica a cópia, não o original
for _, p := range pessoas {
    p.Idade++ // não afeta o slice original!
}

// CORRETO: use o índice para modificar
for i := range pessoas {
    pessoas[i].Idade++
}

Esse comportamento existe porque range copia cada elemento para a variável de iteração. Para structs grandes, isso pode ter impacto no desempenho — nesses casos, trabalhe com ponteiros ou use o índice diretamente.

Range e performance

Para slices de structs grandes, usar apenas o índice evita cópias desnecessárias:

type DadosGrandes struct {
    Buffer [1024]byte
    // ... muitos campos
}

dados := make([]DadosGrandes, 1000)

// LENTO: copia cada struct de 1KB+ a cada iteração
for _, d := range dados {
    _ = d.Buffer[0]
}

// RÁPIDO: acessa diretamente sem cópia
for i := range dados {
    _ = dados[i].Buffer[0]
}

Em benchmarks, a diferença pode ser significativa para structs acima de 64 bytes. Use go test -bench e profiling para identificar gargalos reais no seu código.

Iteradores customizados (Go 1.23+)

A partir do Go 1.23, Go introduziu suporte a iteradores via range over function, permitindo criar tipos iteráveis customizados:

// Iterador que gera números de Fibonacci
func Fibonacci(max int) iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1
        for a <= max {
            if !yield(a) {
                return
            }
            a, b = b, a+b
        }
    }
}

for n := range Fibonacci(100) {
    fmt.Println(n)
}

Esse recurso avançado permite que qualquer tipo customizado seja iterável com a sintaxe familiar do range, abrindo possibilidades para bibliotecas mais ergonômicas e expressivas.

Boas práticas com range

  1. Prefira range a loops manuais — mais legível e menos propenso a erros de off-by-one
  2. Use _ para valores não utilizados — evite warnings e deixe a intenção clara
  3. Cuidado com maps — nunca assuma ordem de iteração
  4. Feche channels antes de iterar — evite deadlocks com goroutines
  5. Use índice para modificar — lembre-se que range fornece cópias
  6. Considere performance com structs grandes — prefira acesso por índice

Para aprofundar, explore como range se integra com select em padrões de concorrência e como usar context para cancelar iterações longas.

Perguntas frequentes (FAQ)

Qual a diferença entre range e for clássico em Go?

O range é uma forma declarativa de iterar que fornece índice e valor automaticamente, enquanto o for clássico (for i := 0; i < n; i++) oferece controle manual total sobre o loop. Use range para percorrer coleções (slices, maps, strings, channels) e o for clássico quando precisar de lógica de incremento customizada ou loops infinitos.

Por que a ordem de iteração com range sobre map é aleatória?

O runtime de Go randomiza deliberadamente a ordem de iteração sobre maps para prevenir que código dependa de uma ordenação específica que nunca foi garantida. Se você precisa de iteração ordenada, extraia as chaves para um slice, ordene com sort.Strings() ou slices.Sort(), e itere sobre o slice ordenado.

Range copia os elementos durante a iteração?

Sim, range cria uma cópia de cada elemento a cada iteração. Isso significa que modificar a variável do loop não altera a coleção original. Para modificar elementos in-place, use o índice retornado pelo range para acessar diretamente a posição no slice ou array. Para structs grandes, usar apenas o índice também melhora a performance evitando cópias desnecessárias.

Como parar um range sobre channel sem fechar o canal?

Use select com um canal de sinalização (done channel) ou context com cancelamento. O padrão idiomático é for { select { case v, ok := <-ch: ... case <-ctx.Done(): return } }. Alternativamente, em Go 1.23+ com iteradores, a função yield pode retornar false para interromper a iteração de forma cooperativa.