Gestão de Goroutines
As Goroutines são leves, mas não são grátis: no mínimo, consomem memória para pilha e agendamento de CPU. Embora esses custos sejam pequenos para o uso de Goroutines, gerá-las em grande quantidade sem um ciclo de vida controlado pode levar a sérios problemas de desempenho. Goroutines com ciclos de vida não gerenciados também podem causar outros problemas, como impedir que objetos não utilizados sejam coletados pelo coletor de lixo e reter recursos não utilizados.
Portanto, não vaze goroutines em seu código. Utilize go.uber.org/goleak para testar vazamentos de goroutines dentro de um pacote que pode produzir goroutines.
Em geral, cada goroutine deve:
- Ter um tempo de parada previsível; ou
- Ter uma maneira de sinalizar a goroutine que ela deve parar.
Em ambos os casos, deve haver uma maneira para o código bloquear e esperar a conclusão da goroutine.
Por exemplo:
Não recomendado:
go func() {
for {
flush()
time.Sleep(delay)
}
}()
// Não há como parar esta goroutine. Ela será executada indefinidamente até a saída da aplicação.
Recomendado:
var (
stop = make(chan struct{}) // Sinalizar a goroutine para parar
done = make(chan struct{}) // Sinalizar que a goroutine saiu
)
go func() {
defer close(done)
ticker := time.NewTicker(delay)
defer ticker.Stop()
for {
select {
case <-ticker.C:
flush()
case <-stop:
return
}
}
}()
// ... outro código
close(stop) // Indicar que a goroutine deve parar
<-done // e esperar que ela saia
// Esta goroutine pode ser esperada com close(stop), e podemos aguardar sua saída com <-done.
Aguardando a Saída das Goroutines
Ao lidar com uma goroutine gerada pelo sistema, deve haver uma maneira de aguardar a saída da goroutine. Existem dois métodos comuns para alcançar isso:
- Usando
sync.WaitGroup
. Use este método ao aguardar múltiplas goroutines.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// ...
}()
}
wg.Wait()
- Adicionando outro
chan struct{}
e fechando-o após a conclusão da goroutine. Use este método se houver apenas uma goroutine.
done := make(chan struct{})
go func() {
defer close(done)
// ...
}()
// Para aguardar a conclusão da goroutine:
<-done
A função init()
não deve criar goroutines. Além disso, consulte Evite o uso de init().
Se um pacote requer uma goroutine em segundo plano, ele deve expor um objeto responsável por gerenciar o ciclo de vida da goroutine. Este objeto deve fornecer um método (Close
, Stop
, Shutdown
, etc.) para indicar a terminação da goroutine em segundo plano e aguardar sua saída.
Abordagem Não Recomendada:
func init() {
go doWork()
}
func doWork() {
for {
// ...
}
}
// Quando o usuário exporta este pacote, uma goroutine em segundo plano é gerada incondicionalmente. Os usuários não podem controlar a goroutine ou pará-la.
Abordagem Recomendada:
type Worker struct{ /* ... */ }
func NewWorker(...) *Worker {
w := &Worker{
stop: make(chan struct{}),
done: make(chan struct{}),
// ...
}
go w.doWork()
}
func (w *Worker) doWork() {
defer close(w.done)
for {
// ...
select {
case <-w.stop:
return
}
}
}
// Shutdown instrui o trabalhador a parar
// e aguarda sua conclusão.
func (w *Worker) Shutdown() {
close(w.stop)
<-w.done
}
Gere o trabalhador apenas quando solicitado pelo usuário e forneça um método para desligar o trabalhador de forma que o usuário possa liberar os recursos usados pelo trabalhador.
Observe que se o trabalhador gerenciar múltiplas goroutines, um WaitGroup deve ser utilizado.