Iniciando uma goroutine
Coloque go na frente de uma chamada de função e essa chamada passa a executar de forma concorrente. A instrução retorna na hora; quem chama não espera.
Quatro goroutines executam shout ao mesmo tempo. A ordem em que terminam não é definida, mas a saída sai sempre na ordem original, porque cada goroutine escreve no seu próprio índice e o main só lê o slice depois que wg.Wait() retorna.
Esse exemplo já contém as três coisas de que quase todo programa com goroutines precisa: um jeito de iniciar o trabalho (go), um jeito de esperar por ele (sync.WaitGroup) e um jeito de receber os resultados sem que duas goroutines mexam na mesma memória (um elemento do slice para cada uma).
O main não espera
Quando main retorna, o programa sai. Goroutines que ainda estão rodando são interrompidas onde estiverem. Nada espera por elas.
Isto normalmente imprime só from main. Às vezes a goroutine é escalonada a tempo e você vê as duas linhas. Esse "normalmente" é o problema: código que funciona na sua máquina e falha em um servidor carregado.
Um time.Sleep no fim do main faz a demonstração imprimir as duas linhas, e é a correção errada. Ele chuta quanto tempo o trabalho leva. Espere pelo próprio trabalho, com um WaitGroup (a página de WaitGroup cobre isso em detalhes) ou com um channel.
Recebendo os resultados
Uma instrução go joga fora os valores de retorno da função. x := go f() não compila. Há dois jeitos padrão de devolver dados.
Uma posição por goroutine, como no primeiro exemplo. Pré-dimensione um slice, dê a cada goroutine o seu índice e leia depois do Wait. A ordem é preservada e nenhum lock é necessário, porque duas goroutines nunca escrevem no mesmo elemento.
Um channel. Cada goroutine envia o seu resultado; quem recebe os junta. Os resultados chegam na ordem em que terminam, não na ordem em que começaram.
Receber exatamente len(nums) valores funciona também como espera: o main não passa do laço até todas as goroutines terem enviado. A ordem de chegada muda entre execuções, então o programa ordena antes de imprimir qualquer coisa que dependa de ordem. A página de channels cobre channels com buffer, fechamento e range sobre um channel.
Variáveis de laço e closures (mudança do Go 1.22)
Desde o Go 1.22, cada iteração de um laço for recebe uma cópia nova das variáveis do laço. Uma closure iniciada em uma goroutine captura o valor daquela iteração, então isto está correto:
for i, w := range words {
go func() {
results[i] = shout(w) // Go 1.22+: i and w belong to this iteration
}()
}
Antes do Go 1.22, todas as iterações compartilhavam um só i e um só w, e todas as goroutines tendiam a ver o último valor. Código antigo contorna isso passando os valores como argumentos, go func(i int, w string) { ... }(i, w), ou sombreando, i := i. As duas formas são inofensivas no Go 1.22 em diante, e você ainda vai vê-las em código existente. O novo comportamento vale quando o go.mod do módulo diz go 1.22 ou mais.
Goroutines são baratas
Uma goroutine começa com uma pilha pequena (alguns kilobytes) que o runtime aumenta e diminui conforme a necessidade. O escalonador do Go executa goroutines em um pool de threads do sistema, com no máximo GOMAXPROCS delas executando código Go ao mesmo tempo, e por padrão GOMAXPROCS é igual ao número de CPUs. Bloquear em um channel, um mutex, um sleep ou I/O de rede estaciona a goroutine e libera a thread para outra.
Então iniciar uma goroutine por tarefa não tem problema mesmo em grandes quantidades:
Cem mil goroutines terminam em uma fração de segundo. A soma é sempre 4999950000, porque atomic.Int64 torna cada adição indivisível. Barato não quer dizer de graça, porém: cada goroutine que continua bloqueada mantém viva a sua pilha e tudo o que ela referencia.
| Thread do sistema | Goroutine | |
|---|---|---|
| Criada por | o kernel | o runtime do Go |
| Pilha inicial | fixa, muitas vezes 1 MB ou mais | alguns KB, cresce sob demanda |
| Troca de contexto | troca de contexto do kernel | escalonador do Go, no espaço de usuário |
| Identidade | tem um ID de thread | nenhum ID que você possa ler, de propósito |
| Quantidade típica | centenas | de milhares a milhões |
Data races
Duas goroutines acessando a mesma variável ao mesmo tempo, com pelo menos uma delas escrevendo, é uma data race. O resultado é imprevisível, não só "um pouco errado": atualizações se perdem, e uma race em um valor string, slice, map ou interface pode derrubar o programa ou corromper a memória.
Em uma máquina com vários núcleos, isto imprime um número diferente abaixo de 10000 na maioria das execuções, porque duas goroutines leem o mesmo valor antigo e as duas gravam esse valor mais um. Em um único núcleo pode imprimir 10000, o que é pior: o bug passa no seu teste e aparece em produção.
O Go vem com um detector de race. Execute o programa ou os testes com -race:
go run -race main.go
go test -race ./...
==================
WARNING: DATA RACE
Read at 0x00c000090038 by goroutine 8:
main.main.func1()
/tmp/race/main.go:16 +0x94
Previous write at 0x00c000090038 by goroutine 6:
main.main.func1()
/tmp/race/main.go:16 +0xa4
...
Found 2 data race(s)
exit status 66
Ele aponta a linha exata (counter++) e as duas goroutines. Ele só reporta races que realmente acontecem durante a execução, então use-o em testes que exercitam os caminhos concorrentes. Ele deixa o programa várias vezes mais lento, então é para testes e staging, não para produção.
As correções, da mais simples à mais geral:
- Não compartilhe. Dê a cada goroutine os seus próprios dados e combine no final (o padrão de uma posição por goroutine).
- Use
sync/atomicpara um único contador ou flag:var n atomic.Int64; n.Add(1). - Use um
sync.Mutexem volta de qualquer coisa maior, como um map ou uma struct com vários campos. A página de mutex também cobreRWMutexesync.Once. - Envie os dados por um channel, para que só uma goroutine seja dona deles de cada vez.
Um panic em uma goroutine mata o programa
Se uma goroutine causa panic e nada o recupera dentro dessa mesma goroutine, o programa inteiro cai, incluindo o main e todas as outras goroutines. Um recover no main não ajuda, porque o recover só captura panics na própria goroutine.
Recuperar assim faz sentido na borda de um servidor de longa duração, em que uma requisição ruim não pode derrubar o resto. Dentro de código comum, um panic normalmente significa um bug, e cair de forma ruidosa é o resultado certo.
Vazamentos de goroutines
Uma goroutine que bloqueia para sempre nunca termina e nunca libera a sua memória. A causa clássica é um envio que ninguém nunca vai receber:
func firstResult(urls []string) string {
ch := make(chan string) // unbuffered
for _, u := range urls {
go func() { ch <- fetch(u) }()
}
return <-ch // takes the first result; the other senders block forever
}
Cada chamada vaza len(urls) - 1 goroutines. Em um servidor que atende essa requisição milhares de vezes, a memória sobe até o processo morrer. Duas correções: fazer o channel grande o bastante para que todo remetente consiga terminar (make(chan string, len(urls))), ou dar às goroutines um jeito de desistir, normalmente um context.Context mais um select em ctx.Done(). Você pode vigiar vazamentos com runtime.NumGoroutine() nos testes.
Limitando quantas executam ao mesmo tempo
"Uma goroutine por item" não tem problema para 10.000 cálculos baratos. Tem problema para 10.000 requisições HTTP ao mesmo servidor ou 10.000 arquivos abertos. Limite a concorrência com um channel com buffer usado como semáforo:
O channel com buffer guarda no máximo 3 fichas, então no máximo 3 goroutines passaram da linha sem <- em qualquer momento. O pico nunca passa de 3, e com doze tarefas que dormem cada uma ele chega a 3 na prática. Um pool fixo de goroutines worker lendo de um channel de jobs é o outro formato comum; a página de WaitGroup monta um.
Fora da biblioteca padrão, golang.org/x/sync/errgroup combina um WaitGroup, o primeiro erro, o cancelamento por context e um limite de concorrência (g.SetLimit(n)) em um só tipo. É a escolha usual em código de produção que precisa dos quatro.
Erros comuns
- Esquecer de esperar. O
mainretorna e o trabalho simplesmente nunca acontece. Toda instruçãogoprecisa de um jeito correspondente de saber que terminou. - Chamar
wg.Adddentro da goroutine. OWaitpode executar antes doAdd, ver o contador em zero e retornar cedo. ChameAddantes da instruçãogo. - Compartilhar uma variável sem sincronização. Maps são o caso comum: escritas concorrentes em um map normalmente são detectadas pelo runtime e derrubam o programa com
fatal error: concurrent map writes, que orecovernão consegue capturar. - Supor uma ordem. Goroutines executam na ordem que o escalonador escolher. Se a saída precisa ser ordenada, junte e ordene, ou escreva em posições indexadas.
- Usar
time.Sleeppara sincronizar. Deixa os testes lentos e ainda instáveis. Espere pelo evento, não por um chute. - Iniciar uma goroutine sem um jeito de pará-la. Tudo o que fica em laço ou espera por I/O deve receber um
context.Contextpara que quem chama possa cancelar.
Perguntas frequentes
O que é uma goroutine em Go?
Uma goroutine é uma chamada de função que executa de forma concorrente com o resto do programa. Você inicia uma colocando go antes de uma chamada: go work(). Goroutines são gerenciadas pelo runtime do Go, não pelo sistema operacional, e o runtime distribui muitas delas sobre um número pequeno de threads do sistema, então iniciar milhares delas é normal.
Como esperar goroutines terminarem em Go?
Use um sync.WaitGroup: chame wg.Add(1) antes de cada instrução go, defer wg.Done() no início da goroutine e wg.Wait() onde você precisa de todas terminadas. Se as goroutines produzem valores, receber um valor por goroutine de um channel também funciona como espera.
Qual a diferença entre goroutine e thread?
Uma thread do sistema tem uma pilha fixa (muitas vezes 1 MB ou mais) e é escalonada pelo kernel. Uma goroutine começa com uma pilha de poucos kilobytes que cresce conforme a necessidade, e o escalonador do Go alterna entre goroutines no espaço de usuário. O runtime executa goroutines em até GOMAXPROCS threads ao mesmo tempo (por padrão, o número de CPUs).
Como obter um valor de retorno de uma goroutine?
Uma instrução go descarta os valores de retorno da função. Envie o resultado por um channel (results <- compute(x)) ou escreva-o na sua própria posição de um slice pré-dimensionado (out[i] = compute(x)) e leia depois do wg.Wait().
Por que meu programa Go termina antes de a goroutine imprimir algo?
Quando main retorna, o programa termina e todas as outras goroutines são interrompidas sem executar o código que falta. Nada espera pelas goroutines automaticamente. Bloqueie o main até o trabalho terminar com um WaitGroup ou um recebimento de channel. Acrescentar time.Sleep só esconde o problema.