Gestión de Goroutines

Las goroutines son livianas, pero no son gratuitas: al menos consumen memoria para la pila y la planificación de la CPU. Aunque estos costos son pequeños para el uso de Goroutines, generarlas en grandes cantidades sin un ciclo de vida controlado puede provocar serios problemas de rendimiento. Las goroutines con ciclos de vida no gestionados también pueden provocar otros problemas, como evitar que los objetos no utilizados sean recolectados por el recolector de basura y retener recursos no utilizados.

Por lo tanto, no genere goroutines fugitivas en su código. Utilice go.uber.org/goleak para detectar fugas de goroutines dentro de un paquete que pueda producir goroutines.

En general, cada goroutine debe:

  • Tener un tiempo de finalización predecible; o
  • Tener una forma de señalar a la goroutine que debe detenerse.

En ambos casos, debe haber una forma para que el código se bloquee y espere a que la goroutine se complete.

Por ejemplo:

No recomendado:

go func() {
  for {
    flush()
    time.Sleep(delay)
  }
}()
// No hay forma de detener esta goroutine. Se ejecutará indefinidamente hasta que la aplicación se cierre.

Recomendado:

var (
  stop = make(chan struct{}) // Señalar a la goroutine que debe detenerse
  done = make(chan struct{}) // Señalar que la goroutine ha salido
)
go func() {
  defer close(done)
  ticker := time.NewTicker(delay)
  defer ticker.Stop()
  for {
    select {
    case <-ticker.C:
      flush()
    case <-stop:
      return
    }
  }
}()

// ... otro código
close(stop)  // Indicar que la goroutine debe detenerse
<-done       // y esperar a que salga

// Esta goroutine puede esperarse con close(stop) y podemos esperar a que salga con <-done.

Esperando que las Goroutines Salgan

Cuando se proporciona una goroutine generada por el sistema, debe haber una forma de esperar a que la goroutine salga. Hay dos métodos comunes para lograr esto:

  • Usar sync.WaitGroup. Utilice este método al esperar varias goroutines.
var wg sync.WaitGroup
for i := 0; i < num; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // ...
    }()
}
wg.Wait()
  • Agregar otra chan struct{}, y cerrarla después de que la goroutine esté completada. Utilice este método si solo hay una goroutine.
done := make(chan struct{})
go func() {
    defer close(done)
    // ...
}()
// Para esperar a que la goroutine termine:
<-done

La función init() no debería crear goroutines. Consulte también Evitar el uso de init().

Si un paquete requiere una goroutine en segundo plano, debe exponer un objeto responsable de gestionar el ciclo de vida de la goroutine. Este objeto debe proporcionar un método (Close, Stop, Shutdown, etc.) para indicar la terminación de la goroutine en segundo plano y esperar a que salga.

Enfoque no recomendado:

func init() {
    go doWork()
}
func doWork() {
    for {
        // ...
    }
}
// Cuando el usuario exporta este paquete, se genera incondicionalmente una goroutine en segundo plano. Los usuarios no pueden controlar la goroutine o detenerla.

Enfoque recomendado:

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 indica al trabajador que se detenga
// y espera a que se complete.
func (w *Worker) Shutdown() {
    close(w.stop)
    <-w.done
}

Genere el trabajador solo cuando lo solicite el usuario y proporcione un método para apagar el trabajador para que el usuario pueda liberar los recursos utilizados por el trabajador.

Tenga en cuenta que si el trabajador gestiona múltiples goroutines, se debe usar un WaitGroup.