Patrones de Concurrencia en Go: 7 Patrones de Producción desde Worker Pool hasta Pipeline
Cuando las Fugas de Goroutines se Encuentran con Concurrencia Sin Límites: Una Pesadilla en Producción
3 AM, alerta de OOM en producción. La investigación revela: un handler HTTP lanzando goroutines sin límites — 3 goroutines por petición, QPS 1000 significa 3,000, y en 10 minutos, 180,000 goroutines filtradas consumen toda la memoria. Peor aún, estas goroutines mantienen referencias de canales que bloquean el GC, eventualmente colapsando todo el nodo.
Este no es un caso aislado. Las primitivas de concurrencia de Go son simples — go func() lanza concurrencia en una línea — pero la programación concurrente en producción es mucho más que simplemente iniciar goroutines. Necesitas controlar los niveles de concurrencia, manejar la propagación de errores, implementar apagado graceful y prevenir fugas de recursos. Este artículo cubre 7 patrones de concurrencia de grado de producción para ayudarte a construir servicios concurrentes robustos en Go.
Referencia de Conceptos Fundamentales
| Primitiva | Propósito | Características Clave | Caso de Uso Típico |
|---|---|---|---|
goroutine |
Unidad de ejecución concurrente ligera | Pila creciente (2KB inicial), planificación del runtime de Go | Cualquier tarea que requiera ejecución concurrente |
channel |
Comunicación entre goroutines | Seguro por tipo, con/sin búfer, cerrable | Paso de datos, notificación de señales, recolección de resultados |
sync.WaitGroup |
Esperar un grupo de goroutines | Trío Add/Done/Wait |
Espera de tareas por lotes, fan-out concurrente |
sync.Mutex |
Exclusión mutua para estado compartido | Utilizable con valor cero, soporte TryLock (Go 1.18+) |
Contadores, actualizaciones de caché, hot-reload de configuración |
context.Context |
Propagar cancelación y timeout | Inmutable, solo se pueden derivar contextos hijos | Timeout de peticiones, apagado graceful, tracing |
errgroup.Group |
Ejecución concurrente + recolección de errores | El primer error cancela todas las goroutines | Llamadas API por lotes, obtención paralela de datos |
semaphore.Weighted |
Limitación de tasa con semáforo ponderado | Soporta pesos, adquisición consciente de timeout | Limitación de tasa de API, control de cuota de recursos |
5 Desafíos de la Programación Concurrente en Producción
Desafío 1: Concurrencia Sin Límites Causa Agotamiento de Recursos
func handleRequests(urls []string) {
for _, url := range urls {
go fetch(url)
}
}
Cada URL genera una goroutine — 10,000 URLs significa 10,000 conexiones concurrentes. Los pools de conexiones a base de datos se agotan, los servicios descendentes se colapsan, la memoria se dispara.
Desafío 2: Fugas de Goroutines
func process(ch <-chan int) {
for {
val := <-ch
fmt.Println(val)
}
}
Si el channel nunca se cierra, esta goroutine nunca sale. En servicios de larga duración, las goroutines filtradas se acumulan continuamente.
Desafío 3: Errores Silenciosamente Ignorados
func fetchAll(urls []string) {
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
resp, err := http.Get(u)
if err != nil {
log.Printf("fetch %s failed: %v", u, err)
return
}
process(resp)
}(url)
}
wg.Wait()
}
Los errores solo se registran — el llamador no tiene idea de los fallos. Si 2 de 3 URLs fallan, el llamador asume que todas tuvieron éxito.
Desafío 4: Sin Apagado Graceful
Cuando el servicio recibe SIGTERM, las goroutines en ejecución son interrumpidas forzosamente. Las escrituras en curso pueden corromper datos, las transacciones en progreso pueden quedar incompletas.
Desafío 5: Dificultad para Combinar Patrones de Concurrencia
Worker Pool necesita limitación de tasa, Pipeline necesita propagación de errores, Fan-out necesita agregación de resultados. Implementar cada patrón por separado no es difícil, pero combinarlos en un solo servicio crea interacciones que fácilmente llevan a deadlocks.
7 Patrones de Concurrencia de Grado de Producción
Patrón 1: Worker Pool — Pool de Goroutines Limitado
Worker Pool es el patrón de concurrencia más fundamental e importante. Idea central: un número fijo de workers extrae tareas de una cola de tareas, evitando concurrencia sin límites.
package workerpool
import (
"context"
"sync"
)
type Task func(ctx context.Context) error
type Pool struct {
workers int
tasks chan Task
wg sync.WaitGroup
}
func NewPool(workers int, bufferSize int) *Pool {
return &Pool{
workers: workers,
tasks: make(chan Task, bufferSize),
}
}
func (p *Pool) Start(ctx context.Context) {
for i := 0; i < p.workers; i++ {
p.wg.Add(1)
go func(workerID int) {
defer p.wg.Done()
for {
select {
case <-ctx.Done():
return
case task, ok := <-p.tasks:
if !ok {
return
}
_ = task(ctx)
}
}
}(i)
}
}
func (p *Pool) Submit(task Task) bool {
select {
case p.tasks <- task:
return true
default:
return false
}
}
func (p *Pool) Stop() {
close(p.tasks)
p.wg.Wait()
}
Ejemplo de uso:
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
pool := NewPool(10, 100)
pool.Start(ctx)
urls := []string{
"https://api.example.com/users",
"https://api.example.com/orders",
"https://api.example.com/products",
}
for _, url := range urls {
u := url
pool.Submit(func(ctx context.Context) error {
return fetchURL(ctx, u)
})
}
pool.Stop()
}
Puntos clave de diseño:
- Cantidad fija de workers previene explosión de goroutines
- Channel de tareas con búfer sirve como cola de tareas
- Soporte de Context para cancelación, los workers pueden salir de forma graceful
Submites no bloqueante, retorna false cuando la cola está llena
Patrón 2: Fan-out/Fan-in — Dispersión-Recolección Paralela
Fan-out distribuye una fuente de datos a múltiples goroutines para procesamiento paralelo, Fan-in fusiona resultados de múltiples goroutines en un solo channel.
package fan
import (
"context"
"sync"
)
func FanOut[T any](ctx context.Context, source <-chan T, workers int) []<-chan T {
channels := make([]<-chan T, workers)
for i := 0; i < workers; i++ {
ch := make(chan T)
channels[i] = ch
go func() {
defer close(ch)
for {
select {
case <-ctx.Done():
return
case val, ok := <-source:
if !ok {
return
}
select {
case ch <- val:
case <-ctx.Done():
return
}
}
}
}()
}
return channels
}
func FanIn[T any](ctx context.Context, channels ...<-chan T) <-chan T {
out := make(chan T)
var wg sync.WaitGroup
wg.Add(len(channels))
for _, ch := range channels {
go func(c <-chan T) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case val, ok := <-c:
if !ok {
return
}
select {
case out <- val:
case <-ctx.Done():
return
}
}
}
}(ch)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
Ejemplo de uso — procesamiento paralelo de pedidos:
func processOrders(ctx context.Context, orders <-chan Order) <-chan Result {
workers := FanOut(ctx, orders, 5)
return FanIn(ctx, workers...)
}
Puntos clave de diseño:
- Soporte de genéricos (Go 1.18+), aplicable a cualquier tipo
- Cada goroutine de fan-out consume independientemente del channel fuente
- Fan-in usa WaitGroup para esperar que todos los canales de entrada se cierren
- Todas las goroutines pueden salir con la cancelación del context
Patrón 3: Pipeline — Procesamiento por Etapas
Pipeline descompone un procesamiento complejo en etapas, cada una ejecutándose como una goroutine conectada por channels.
package pipeline
import (
"context"
)
type Stage[In any, Out any] func(ctx context.Context, in <-chan In) <-chan Out
func NewPipeline[In any, Out any](
ctx context.Context,
source <-chan In,
stages ...Stage[In, In],
) <-chan Out {
current := source
for _, stage := range stages {
current = stage(ctx, current)
}
return any(current).(<-chan Out)
}
func NewStage[In any, Out any](
process func(ctx context.Context, in In) (Out, error),
bufferSize int,
) Stage[In, Out] {
return func(ctx context.Context, in <-chan In) <-chan Out {
out := make(chan Out, bufferSize)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case val, ok := <-in:
if !ok {
return
}
result, err := process(ctx, val)
if err != nil {
continue
}
select {
case out <- result:
case <-ctx.Done():
return
}
}
}
}()
return out
}
}
Ejemplo de uso — pipeline de procesamiento de datos:
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
raw := make(chan RawData, 100)
validate := NewStage[RawData, ValidData](func(ctx context.Context, in RawData) (ValidData, error) {
if err := in.Validate(); err != nil {
return ValidData{}, err
}
return in.ToValid(), nil
}, 50)
enrich := NewStage[ValidData, EnrichedData](func(ctx context.Context, in ValidData) (EnrichedData, error) {
return fetchExtraInfo(ctx, in)
}, 50)
transform := NewStage[EnrichedData, FinalData](func(ctx context.Context, in EnrichedData) (FinalData, error) {
return in.Transform()
}, 50)
result := NewPipeline(ctx, raw, validate, enrich, transform)
go func() {
for r := range result {
saveToDB(r)
}
}()
}
Puntos clave de diseño:
- Cada etapa es una goroutine independiente, puede escalarse independientemente
- Los channels proporcionan backpressure entre etapas
- Los errores se manejan dentro de las etapas, no interrumpen todo el pipeline
- Todas las etapas salen de forma graceful con la cancelación del context
Patrón 4: errgroup — Manejo Concurrente de Errores
errgroup es la versión con manejo de errores de sync.WaitGroup: el primer error cancela todas las goroutines.
package main
import (
"context"
"fmt"
"net/http"
"time"
"golang.org/x/sync/errgroup"
)
type FetchResult struct {
URL string
Body string
Size int
}
func fetchMultiple(ctx context.Context, urls []string) ([]FetchResult, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([]FetchResult, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("create request %s: %w", url, err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("fetch %s: status %d", url, resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read %s: %w", url, err)
}
results[i] = FetchResult{
URL: url,
Body: string(body),
Size: len(body),
}
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
errgroup con límite de concurrencia:
func fetchWithLimit(ctx context.Context, urls []string, maxConcurrent int) ([]FetchResult, error) {
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(maxConcurrent)
results := make([]FetchResult, len(urls))
for i, url := range urls {
i, url := i, url
g.Go(func() error {
results[i], _ = fetchOne(ctx, url)
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
Puntos clave de diseño:
errgroup.WithContextpropaga automáticamente la cancelacióng.SetLimit(n)controla la concurrencia máxima (Go 1.20+)- El primer error cancela todas las goroutines en progreso
- La captura de variables en closures requiere
i, url := i, url
Patrón 5: Semáforo — Limitación de Tasa
semaphore.Weighted proporciona semáforos ponderados para asignación de recursos con diferentes pesos.
package ratelimit
import (
"context"
"fmt"
"golang.org/x/sync/semaphore"
)
type RateLimiter struct {
sem *semaphore.Weighted
}
func NewRateLimiter(maxWeight int64) *RateLimiter {
return &RateLimiter{
sem: semaphore.NewWeighted(maxWeight),
}
}
func (r *RateLimiter) Do(ctx context.Context, weight int64, fn func() error) error {
if err := r.sem.Acquire(ctx, weight); err != nil {
return fmt.Errorf("acquire semaphore: %w", err)
}
defer r.sem.Release(weight)
return fn()
}
Ejemplo de uso — limitación de tasa de API por niveles:
func main() {
limiter := NewRateLimiter(100)
err := limiter.Do(context.Background(), 10, func() error {
return callLightAPI()
})
err = limiter.Do(context.Background(), 50, func() error {
return callHeavyAPI()
})
}
Semáforo con timeout:
func (r *RateLimiter) DoWithTimeout(ctx context.Context, weight int64, timeout time.Duration, fn func() error) error {
acquireCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
if err := r.sem.Acquire(acquireCtx, weight); err != nil {
return fmt.Errorf("semaphore acquire timeout: %w", err)
}
defer r.sem.Release(weight)
return fn()
}
Puntos clave de diseño:
- Semáforo ponderado, diferentes operaciones consumen diferentes cuotas
Acquiresoporta cancelación de context y timeoutReleasedebe emparejarse conAcquire- Adecuado para limitación de tasa de API por niveles, control de cuota de recursos
Patrón 6: Cancelación de Context y Timeout
Context es la "línea de vida" de la programación concurrente en Go, utilizado para propagar señales de cancelación, timeouts y deadlines.
package ctxutil
import (
"context"
"fmt"
"time"
)
type Result struct {
Data string
Error error
}
func FetchWithTimeout(ctx context.Context, url string, timeout time.Duration) (string, error) {
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return "", fmt.Errorf("create request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return "", fmt.Errorf("fetch %s timed out after %v: %w", url, timeout, ctx.Err())
}
return "", fmt.Errorf("fetch %s: %w", url, err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("read response: %w", err)
}
return string(body), nil
}
func BatchFetch(ctx context.Context, urls []string, timeout time.Duration) []Result {
results := make([]Result, len(urls))
var wg sync.WaitGroup
for i, url := range urls {
wg.Add(1)
go func(idx int, u string) {
defer wg.Done()
data, err := FetchWithTimeout(ctx, u, timeout)
results[idx] = Result{Data: data, Error: err}
}(i, url)
}
wg.Wait()
return results
}
Patrón de apagado graceful:
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
server := &http.Server{Addr: ":8080"}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
_ = server.ListenAndServe()
}
Puntos clave de diseño:
WithTimeout/WithDeadlinepara establecer timeoutsWithCancelpara control manual de cancelaciónsignal.NotifyContextpara escuchar señales del sistema- La cancelación del context se propaga a todas las goroutines hijas
defer cancel()previene fugas de context
Patrón 7: Servicio Concurrente en Producción — Combinando Patrones
Combina los patrones anteriores para construir un servicio concurrente de grado de producción: Worker Pool + Pipeline + errgroup + Context.
package concurrencyservice
import (
"context"
"fmt"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
type Service struct {
workers int
bufferSize int
timeout time.Duration
}
func NewService(workers, bufferSize int, timeout time.Duration) *Service {
return &Service{
workers: workers,
bufferSize: bufferSize,
timeout: timeout,
}
}
type Job struct {
ID string
Input any
}
type Output struct {
Job Job
Result any
Err error
}
func (s *Service) Process(ctx context.Context, jobs []Job) ([]Output, error) {
ctx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.SetLimit(s.workers)
jobCh := make(chan Job, s.bufferSize)
resultCh := make(chan Output, s.bufferSize)
g.Go(func() error {
defer close(jobCh)
for _, job := range jobs {
select {
case <-ctx.Done():
return ctx.Err()
case jobCh <- job:
}
}
return nil
})
var processWg sync.WaitGroup
for i := 0; i < s.workers; i++ {
processWg.Add(1)
go func() {
defer processWg.Done()
for job := range jobCh {
output := Output{Job: job}
output.Result, output.Err = s.processOne(ctx, job)
select {
case resultCh <- output:
case <-ctx.Done():
return
}
}
}()
}
go func() {
processWg.Wait()
close(resultCh)
}()
var outputs []Output
for result := range resultCh {
outputs = append(outputs, result)
}
if err := g.Wait(); err != nil {
return outputs, fmt.Errorf("service process: %w", err)
}
return outputs, nil
}
func (s *Service) processOne(ctx context.Context, job Job) (any, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
return fmt.Sprintf("processed-%s", job.ID), nil
}
Puntos clave de diseño:
- errgroup controla concurrencia + propagación de errores
- Channels para distribución de tareas y recolección de resultados
- Context para timeout y cancelación unificados
- WaitGroup asegura que todos los workers completen antes de cerrar el channel de resultados
- Diseño por capas: planificación (errgroup) + ejecución (goroutines worker) + recolección (range resultCh)
5 Errores Comunes y Sus Soluciones
Error 1: Captura de Variables en Closures
❌ Incorrecto:
for _, url := range urls {
go func() {
fetch(url)
}()
}
Todas las goroutines comparten la misma variable url, terminando por obtener la última URL.
✅ Correcto:
for _, url := range urls {
url := url
go func() {
fetch(url)
}()
}
Error 2: Channel Sin Cerrar Causa Fuga de Goroutine
❌ Incorrecto:
func producer() <-chan int {
ch := make(chan int)
go func() {
for i := 0; i < 100; i++ {
ch <- i
}
}()
return ch
}
Los consumidores que iteran sobre el channel nunca terminan, fuga de goroutine.
✅ Correcto:
func producer() <-chan int {
ch := make(chan int)
go func() {
defer close(ch)
for i := 0; i < 100; i++ {
ch <- i
}
}()
return ch
}
Error 3: Posición Incorrecta de WaitGroup Add
❌ Incorrecto:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
go func() {
wg.Add(1)
defer wg.Done()
doWork()
}()
}
wg.Wait()
Las goroutines pueden no haber alcanzado wg.Add(1) aún, y el wg.Wait() de la goroutine principal pasa inmediatamente.
✅ Correcto:
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
}
wg.Wait()
Error 4: Fuga de time.After en Select
❌ Incorrecto:
select {
case result := <-ch:
process(result)
case <-time.After(5 * time.Second):
return errors.New("timeout")
}
Cada select crea un nuevo channel time.After. Llamadas frecuentes filtran timers.
✅ Correcto:
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case result := <-ch:
process(result)
case <-ctx.Done():
return ctx.Err()
}
Error 5: Copia de Mutex
❌ Incorrecto:
type SafeMap struct {
mu sync.Mutex
data map[string]string
}
func copyMap(m SafeMap) SafeMap {
return m
}
El valor cero de Mutex es "desbloqueado". Copiar un Mutex bloqueado causa deadlock o data race.
✅ Correcto:
type SafeMap struct {
mu *sync.Mutex
data map[string]string
}
func NewSafeMap() *SafeMap {
return &SafeMap{
mu: &sync.Mutex{},
data: make(map[string]string),
}
}
Referencia de Solución de Errores
| Síntoma | Causa Posible | Investigación | Solución |
|---|---|---|---|
fatal error: all goroutines are asleep - deadlock! |
Todas las goroutines bloqueadas, sin goroutines activas | Verificar emparejamiento lectura/escritura de channels, select default | Asegurar que los channels tengan productores y consumidores, agregar cancelación de context |
| El conteo de goroutines sigue creciendo | Fuga de goroutines, channels sin cerrar | Monitoreo con runtime.NumGoroutine(), pprof goroutine profile |
Asegurar que todas las goroutines tengan rutas de salida, defer close(ch) |
| La memoria sigue creciendo | Goroutines mantienen referencias a objetos grandes, acumulación en channels | pprof heap profile, verificar tamaño de búfer de channels | Limitar búfer de channels, liberar referencias oportunamente |
Errores frecuentes de context canceled |
Context ascendente cancelado, timeout demasiado corto | Verificar cadena de contexts, confirmar que el timeout es razonable | Ajustar timeout, distinguir errores de negocio de errores de timeout |
| Resultados concurrentes faltantes | Goroutine sale antes de escribir al channel | Verificar lógica de salida de goroutines, confirmar cierre de channels | Usar WaitGroup para asegurar que todas las goroutines completen antes de cerrar el channel |
race condition detectado |
Múltiples goroutines leen/escriben variables compartidas | go test -race, verificar variables globales y capturas en closures |
Usar Mutex/RWMutex para protección, o cambiar a comunicación por channels |
| Channel bloqueado, sin respuesta | Desajuste de velocidad entre productor/consumidor | Verificar tamaño de búfer del channel, monitorear tasas de producción/consumo | Aumentar búfer, usar select+default para envío no bloqueante |
| Fallo en apagado graceful | Goroutines no responden a la cancelación del context | Verificar si las goroutines hacen select en ctx.Done() | Asegurar que todas las goroutines de larga duración verifiquen ctx.Done() |
| errgroup retorna solo un error | errgroup diseñado para cancelación por primer error | Verificar si necesitas recolectar todos los errores | Usar colector de errores personalizado o múltiples errgroups |
| Timeout en adquisición de semáforo | Límite de concurrencia demasiado estricto, solicitudes encoladas demasiado tiempo | Monitorear tiempo de espera del semáforo, ajustar cuotas de peso | Aumentar capacidad del semáforo, optimizar asignación de pesos |
Técnicas Avanzadas de Optimización
Optimización 1: Worker Pool Dinámico
Ajustar dinámicamente la cantidad de workers según la carga del sistema:
package dynamicpool
import (
"context"
"runtime"
"sync"
"sync/atomic"
"time"
)
type DynamicPool struct {
minWorkers int64
maxWorkers int64
active atomic.Int64
tasks chan Task
wg sync.WaitGroup
adjustTick *time.Ticker
}
func NewDynamicPool(minW, maxW int) *DynamicPool {
return &DynamicPool{
minWorkers: int64(minW),
maxWorkers: int64(maxW),
tasks: make(chan Task, maxW*2),
adjustTick: time.NewTicker(5 * time.Second),
}
}
func (p *DynamicPool) Start(ctx context.Context) {
for i := 0; i < int(p.minWorkers); i++ {
p.addWorker(ctx)
}
go func() {
for {
select {
case <-ctx.Done():
p.adjustTick.Stop()
return
case <-p.adjustTick.C:
p.adjustWorkers(ctx)
}
}
}()
}
func (p *DynamicPool) addWorker(ctx context.Context) {
p.active.Add(1)
p.wg.Add(1)
go func() {
defer func() {
p.active.Add(-1)
p.wg.Done()
}()
for task := range p.tasks {
_ = task(ctx)
}
}()
}
func (p *DynamicPool) adjustWorkers(ctx context.Context) {
current := p.active.Load()
queueLen := len(p.tasks)
if queueLen > int(current) && current < p.maxWorkers {
p.addWorker(ctx)
} else if queueLen == 0 && current > p.minWorkers {
for i := 0; i < 5 && int(p.active.Load()) > int(p.minWorkers); i++ {
p.tasks <- nil
}
}
}
func (p *DynamicPool) Stop() {
close(p.tasks)
p.wg.Wait()
}
Optimización 2: Batching
Combinar múltiples tareas pequeñas en una operación por lotes para reducir overhead de syscalls e IO:
package batcher
import (
"context"
"sync"
"time"
)
type Batcher[T any, R any] struct {
batchSize int
flushInterval time.Duration
handler func(ctx context.Context, batch []T) ([]R, error)
mu sync.Mutex
buffer []T
results []R
}
func NewBatcher[T any, R any](
batchSize int,
flushInterval time.Duration,
handler func(ctx context.Context, batch []T) ([]R, error),
) *Batcher[T, R] {
return &Batcher[T, R]{
batchSize: batchSize,
flushInterval: flushInterval,
handler: handler,
buffer: make([]T, 0, batchSize),
}
}
func (b *Batcher[T, R]) Add(ctx context.Context, item T) (R, error) {
b.mu.Lock()
b.buffer = append(b.buffer, item)
if len(b.buffer) >= b.batchSize {
batch := b.buffer
b.buffer = make([]T, 0, b.batchSize)
b.mu.Unlock()
results, err := b.handler(ctx, batch)
if err != nil {
var zero R
return zero, err
}
b.results = append(b.results, results...)
var zero R
if len(results) > 0 {
return results[0], nil
}
return zero, nil
}
b.mu.Unlock()
var zero R
return zero, nil
}
func (b *Batcher[T, R]) Flush(ctx context.Context) ([]R, error) {
b.mu.Lock()
batch := b.buffer
b.buffer = make([]T, 0, b.batchSize)
b.mu.Unlock()
if len(batch) == 0 {
return b.results, nil
}
results, err := b.handler(ctx, batch)
if err != nil {
return nil, err
}
b.results = append(b.results, results...)
return b.results, nil
}
Optimización 3: Paso por Channel Sin Copia (Zero-Copy)
Usar punteros y sync.Pool para reducir la asignación de memoria durante el paso por channels:
package zerocopy
import (
"sync"
)
type Buffer struct {
Data []byte
}
var bufferPool = sync.Pool{
New: func() any {
return &Buffer{Data: make([]byte, 0, 4096)}
},
}
func GetBuffer() *Buffer {
return bufferPool.Get().(*Buffer)
}
func PutBuffer(buf *Buffer) {
buf.Data = buf.Data[:0]
bufferPool.Put(buf)
}
func ProcessPipeline(ctx context.Context, input <-chan []byte) <-chan *Buffer {
out := make(chan *Buffer, 64)
go func() {
defer close(out)
for {
select {
case <-ctx.Done():
return
case data, ok := <-input:
if !ok {
return
}
buf := GetBuffer()
buf.Data = append(buf.Data[:0], data...)
select {
case out <- buf:
case <-ctx.Done():
PutBuffer(buf)
return
}
}
}
}()
return out
}
Comparación de Patrones de Concurrencia
| Característica | goroutine+channel | sync.WaitGroup | errgroup | Worker Pool |
|---|---|---|---|---|
| Control de concurrencia | Sin control integrado | Sin control integrado | SetLimit |
Cantidad fija de workers |
| Manejo de errores | Implementación manual | Implementación manual | Auto cancelación por primer error | Implementación manual |
| Backpressure | Búfer de channel | Ninguno | Ninguno | Channel de tareas |
| Propagación de cancelación | Context manual | Context manual | Context automático | Context manual |
| Recolección de resultados | Recepción por channel | Variables compartidas | Retorna slice | Recepción por channel |
| Caso de uso | Procesamiento de datos en flujo | Espera de tareas por lotes | Llamadas API concurrentes | Procesamiento de tareas con límite de tasa |
| Complejidad | Media | Baja | Baja | Media |
| Cantidad de goroutines | Variable | Variable | Limitada | Fija |
| Recomendación para producción | ⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
Resumen
La programación concurrente en Go no se trata de "simplemente hacer que funcione" — se trata de responder cuatro preguntas: ¿Cuántas goroutines se están ejecutando? ¿Cuándo salen? ¿Qué pasa cuando hay un error? ¿Cómo se recuperan los recursos? Worker Pool responde "cuántas", Context responde "cuándo salir", errgroup responde "qué hacer en caso de error", y defer close() responde "cómo recuperar". Domina estos 7 patrones, y tendrás la metodología central para la programación concurrente en Go de grado de producción.
Herramientas Recomendadas
- Formateador JSON — Formatea respuestas JSON de servicios concurrentes, depura rápidamente problemas de estructura de datos
- Calculadora de Hash — Calcula firmas de peticiones y checksums de datos, asegura consistencia de datos en peticiones concurrentes
- Codificación/Decodificación Base64 — Maneja codificación de datos binarios en servicios concurrentes
Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →