Como desarrollador backend, paso la mayor parte del tiempo trabajando con PHP y Go. Sin duda, PHP es práctico y rápido para prototipar, especialmente en el modo php‑fpm tradicional (un proceso por petición). Sin embargo, para servicios de streaming en vivo con alta concurrencia, PHP se quedaba corto. Aunque existen alternativas como Swoole, su ecosistema es pequeño y antes tenía muchos bugs, lo que aumentaba la carga mental. Go, en cambio, destaca por su manejo nativo de concurrencia y está siendo adoptado por grandes empresas como Tencent, Baidu, Didi, etc. Además, su sintaxis simple permite aprender lo básico en unas semanas y empezar a colaborar rápidamente, aunque dominarlo requiere más tiempo.
El problema de concurrencia que Go resuelve
Imaginemos la entrada de un usuario a una sala de streaming: necesitamos obtener información de versión, detalles de la transmisión, datos del usuario, derechos asociados y estadísticas. En PHP esto se haría de forma secuencial, con un tiempo total igual a la suma de todas las operaciones. Con Go podemos ejecutar estas tareas en paralelo, de modo que el tiempo de respuesta sea solo el de la operación más lenta.
Ilustración conceptual: En lugar de ejecutar A → B → C → D → E secuencialmente, Go lanza cinco gorutinas simultáneas, cada una con una tarea, y espera a que todas terminen.
Método 1: sync.WaitGroup
package main
import (
"context"
"fmt"
"sync"
"time"
)
func main() {
var (
version, live, user, equity, stats int
)
ctx := context.Background()
// Lanzar todas las tareas concurrentemente usando WaitGroup
waitGroupExample(ctx,
func() {
version = 1
time.Sleep(1 * time.Second)
fmt.Println("Tarea 1 completada")
},
func() {
live = 2
time.Sleep(2 * time.Second)
fmt.Println("Tarea 2 completada")
},
func() {
user = 3
time.Sleep(3 * time.Second)
fmt.Println("Tarea 3 completada")
},
func() {
equity = 4
time.Sleep(4 * time.Second)
fmt.Println("Tarea 4 completada")
},
func() {
stats = 5
time.Sleep(5 * time.Second)
fmt.Println("Tarea 5 completada")
},
)
fmt.Println("Resultados:", version, live, user, equity, stats)
}
// waitGroupExample ejecuta una lista de funciones en paralelo usando sync.WaitGroup
func waitGroupExample(ctx context.Context, tasks ...func()) {
var wg sync.WaitGroup
for _, t := range tasks {
wg.Add(1)
go func(task func()) {
task()
wg.Done()
}(t)
}
wg.Wait()
}
Método 2: errgroup (librería oficial de extensión)
package main
import (
"context"
"fmt"
"time"
"golang.org/x/sync/errgroup"
)
func main() {
var (
version, live, user, equity, stats int
)
ctx := context.Background()
// Lanzar tareas concurrentes con captura de errores mediante errgroup
err := processWithErrGroup(ctx,
func() error {
version = 1
time.Sleep(1 * time.Second)
fmt.Println("Tarea 1 completada")
return nil
},
func() error {
live = 2
time.Sleep(2 * time.Second)
fmt.Println("Tarea 2 completada")
return nil
},
func() error {
user = 3
time.Sleep(3 * time.Second)
fmt.Println("Tarea 3 completada")
return nil
},
func() error {
equity = 4
time.Sleep(4 * time.Second)
fmt.Println("Tarea 4 completada")
return nil
},
func() error {
stats = 5
time.Sleep(5 * time.Second)
fmt.Println("Tarea 5 completada")
return nil
},
)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Resultados:", version, live, user, equity, stats)
}
// processWithErrGroup ejecuta funciones que devuelven error en paralelo usando errgroup
func processWithErrGroup(ctx context.Context, tasks ...func() error) error {
eg := new(errgroup.Group)
for i := range tasks {
// Importante: crear una copia local de la variable para evitar el problema de captura de closures
t := tasks[i]
eg.Go(func() error {
if err := t(); err != nil {
// Podríamos registrar el error aquí
return err
}
return nil
})
}
return eg.Wait()
}
⚠️ Atención al closure en bucles
En Go, cuando usas una variable de iteración dentro de una gorutina (o cualquier closure), la gorutina captura la referencia a la varible, no su valor en el momento de la iteración. Si escribes directamente:
for _, f := range tasks {
eg.Go(func() error {
return f() // ❌ Puede usar el último valor de f
})
}
todas las gorutinas terminarán llamando a la última función del slice. Para evitarlo, se debe crear una copia local:
for _, f := range tasks {
fp := f // ✅ Copia local
eg.Go(func() error {
return fp()
})
}
O bien iterar por índice y asignar a una variable local, como se muestra en el ejemplo anterior.
Conclusión práctica
Tanto sync.WaitGroup como errgroup son herramientas fundamentales para dividir una tarea padre en múltiples subtareas concurrentes y esperar su finalización. Son las técnicas más básicas y útiles que acompañan a cualquier desarrollader Go en su día a día. Los fragmentos presentados pueden usarse directamente en proyectos reales.