Menu

range em Golang: todas as formas, o que cada uma entrega e as armadilhas

O que o for range entrega para slices, arrays, strings, maps, channels, inteiros e funções iteradoras, por que a variável de valor é uma cópia, o que acontece ao alterar um slice ou map durante o range e o range sobre funções do Go 1.23.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

range é a forma do for que percorre uma coleção. Ele entrega um ou dois valores por iteração, e o que esses valores são depende do tipo que você percorre:

OperandoPrimeiro valorSegundo valorObservações
slice []T, array [n]Tíndice intelemento T, uma cópia
ponteiro para array *[n]Tíndiceelemento
stringíndice em bytes intrunedecodifica UTF-8
map map[K]Vchave Kvalor Vordem aleatória
channel chan Telemento Tnenhumaté o channel ser fechado
inteiro nde 0 a n menos 1nenhumGo 1.22
func(yield func() bool)nenhumnenhumGo 1.23
func(yield func(V) bool)VnenhumGo 1.23
func(yield func(K, V) bool)KVGo 1.23

Escolhendo quais variáveis receber

for i, v := range s {}  // both
for i := range s {}     // index (or key) only
for _, v := range s {}  // value only
for range s {}          // neither: just repeat len(s) times

Declarar uma variável que você não usa é erro de compilação, então omita ou descarte com _ as que não precisar. for range 3 {} é o jeito mais curto de repetir algo três vezes.

O valor é uma cópia

A cada iteração, o elemento é copiado para a variável de valor. Mudar a variável não muda a coleção:

Copiar também custa tempo quando os elementos são structs grandes. Para um slice de structs grandes, for i := range s com s[i] evita a cópia.

Strings entregam runes

Percorrer uma string com range decodifica o UTF-8. O primeiro valor é o offset em bytes onde cada caractere começa, então ele pode pular números:

Para percorrer bytes em vez de caracteres, use um laço por índice, for i := 0; i < len(s); i++, ou faça range sobre []byte(s). Veja runes e bytes para os detalhes do UTF-8.

Maps: ordem aleatória

A ordem de iteração de um map não é especificada, e o runtime a embaralha para que o código não passe a depender de uma ordem. Para uma saída determinística, ordene as chaves:

Apagar entradas durante o range é seguro, e uma entrada apagada que ainda não foi alcançada não é entregue. Acrescentar entradas é permitido, mas imprevisível: uma chave nova pode ou não aparecer mais tarde no mesmo laço. fmt.Println de um map imprime as chaves ordenadas, e é por isso que a última linha é estável.

Channels: até serem fechados

Percorrer um channel com range recebe valores até que o channel seja fechado e esvaziado. Se ninguém fechá-lo, o laço bloqueia para sempre (e, se todas as goroutines estiverem bloqueadas, o runtime para com fatal error: all goroutines are asleep - deadlock!):

Quem envia fecha o channel, nunca quem recebe. Veja channels para channels com buffer e select.

Inteiros (Go 1.22)

range n conta de 0 até n, sem incluir n. O tipo da variável do laço é o tipo de n:

for i := range 3 {}         // i is int: 0, 1, 2
for i := range uint8(3) {}  // i is uint8
for range 0 {}              // runs zero times

Um n negativo também executa zero vezes. n é avaliado uma vez, antes de o laço começar.

O que o range avalia, e quando

A expressão depois de range é avaliada uma vez, antes da primeira iteração. Para um slice, o range passa a usar aquele cabeçalho de slice, então o tamanho fica fixo:

O 99 acrescentado nunca é visitado porque o tamanho do laço ficou fixo em 3. Mas nums[2] = 30 é visto, porque nesse momento nums ainda compartilha o array subjacente com o slice que está sendo percorrido. Troque as duas linhas de lugar e o 30 também some: o append passa da capacidade de 3, aloca um array novo, e a escrita cai nele. Percorrer um array por valor copia o array inteiro antes, então a mudança em arr[2] não é vista; faça range sobre &arr ou arr[:] para evitar a cópia.

range sobre funções (Go 1.23)

Desde o Go 1.23, o range também aceita funções iteradoras. Um iterador recebe um callback yield, chama-o uma vez por valor e para quando yield devolve false (o que acontece quando o corpo do laço executa break ou return):

iter.Seq[V] e iter.Seq2[K, V] são os nomes padrão dos dois formatos de iterador. A biblioteca padrão os devolve em slices.All, slices.Values, slices.Backward, maps.Keys, maps.Values, maps.All e, no Go 1.24, em strings.SplitSeq, strings.Lines e bytes.SplitSeq. O iterador precisa respeitar o valor devolvido por yield: chamar yield de novo depois que ele devolveu false causa panic.

Variáveis de laço por iteração (Go 1.22)

Desde o Go 1.22, as variáveis declaradas por for ... range são novas a cada iteração. Capturar v em uma closure ou goroutine captura o valor daquela iteração, e não uma variável compartilhada que acaba guardando o último elemento. Antes do Go 1.22 você precisava de v := v dentro do laço; essa linha agora é redundante. A regra é definida pela versão go do go.mod, então um módulo antigo mantém o comportamento antigo.

Erros comuns

  • Pegar o endereço da variável de valor. Antes do Go 1.22, ptrs = append(ptrs, &v) guardava o mesmo endereço toda vez. Desde o 1.22 guarda endereços distintos, mas cada um aponta para uma cópia, não para o elemento do slice. Use &items[i] para apontar para dentro do slice.
  • Esperar ordem em map. Uma saída que parece ordenada em um teste pequeno não vai continuar ordenada.
  • Fazer range sobre um channel que ninguém fecha. O laço, e a goroutine que o executa, esperam para sempre.
  • Fazer range sobre um array grande por valor. O array inteiro é copiado antes. Faça range sobre um slice dele.

Perguntas frequentes

O que o range devolve em Go?

Depende do operando. Para um slice ou array: índice e elemento. Para uma string: índice em bytes e rune. Para um map: chave e valor. Para um channel: cada valor recebido. Para um inteiro n (Go 1.22): de 0 a n menos 1. Para uma função iteradora (Go 1.23): o que a função entregar. Você pode omitir a segunda variável, ou descartar a primeira com _.

Por que alterar o valor do range não muda o meu slice?

A variável de valor é uma cópia do elemento. for _, v := range items { v.Price = 0 } altera só a cópia. Use o índice: for i := range items { items[i].Price = 0 }, ou percorra um slice de ponteiros.

Dá para usar range sobre um inteiro em Go?

Sim, desde o Go 1.22: for i := range 5 { ... } executa com i de 0 a 4. for range 5 { ... } repete cinco vezes sem variável. O go.mod do módulo precisa dizer go 1.22 ou mais recente.

É seguro apagar de um map enquanto se percorre com range?

Sim. Apagar uma entrada que ainda não foi alcançada faz com que ela não seja entregue, e apagar a entrada atual não tem problema. Acrescentar entradas durante o laço também é permitido, mas uma entrada nova pode ou não ser visitada, então não dependa disso.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR