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.