Golang Avanzado

Golang Avanzado

article author avatar

raulanto

Desarrollador Full Stack

Golan

29 Sep 2025

12 min read

Golang Avanzado: Dominando Concurrencia y Optimización de Memoria

Introducción a Go

Go (Golang) es un lenguaje de programación de código abierto desarrollado por Google que destaca por su simplicidad, rendimiento y capacidades de concurrencia nativas. A diferencia de otros lenguajes, Go fue diseñado desde cero para aprovechar sistemas multinúcleo modernos.

Parte 1: Concurrencia en Go

¿Qué es la Concurrencia?

La concurrencia es la capacidad de ejecutar múltiples tareas al mismo tiempo. Go hace esto increíblemente fácil con goroutines y channels.

Goroutines: Hilos Ligeros

Las goroutines son funciones que se ejecutan concurrentemente con otras funciones. Son extremadamente ligeras (inician con solo ~2KB de memoria) comparadas con los threads del sistema operativo.

Ejemplo Básico de Goroutines

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 5; i++ {
        fmt.Printf("Hola %s! (%d)\n", name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Ejecución secuencial
    sayHello("Juan")
    sayHello("María")
    
    fmt.Println("\n--- Ahora con goroutines ---\n")
    
    // Ejecución concurrente
    go sayHello("Pedro")
    go sayHello("Ana")
    
    // Esperar para ver los resultados
    time.Sleep(600 * time.Millisecond)
    fmt.Println("Programa finalizado")
}

Resultado: Las dos goroutines se ejecutan simultáneamente, intercalando sus mensajes.

Channels: Comunicación entre Goroutines

Los channels son tuberías tipadas que permiten a las goroutines comunicarse de forma segura.

Channels Básicos

package main

import (
    "fmt"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        fmt.Printf("Worker %d procesando job %d\n", id, job)
        time.Sleep(time.Second)
        results <- job * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)
    
    // Iniciar 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }
    
    // Enviar 9 trabajos
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)
    
    // Recoger resultados
    for a := 1; a <= 9; a++ {
        result := <-results
        fmt.Printf("Resultado recibido: %d\n", result)
    }
}

Buffered vs Unbuffered Channels

package main

import "fmt"

func main() {
    // Unbuffered channel (bloqueante)
    ch1 := make(chan int)
    
    // Buffered channel (no bloqueante hasta llenar el buffer)
    ch2 := make(chan int, 3)
    
    // Esto funcionaría solo con goroutine
    // ch1 <- 1 // Bloquearía aquí
    
    // Esto funciona sin goroutine
    ch2 <- 1
    ch2 <- 2
    ch2 <- 3
    
    fmt.Println(<-ch2) // 1
    fmt.Println(<-ch2) // 2
    fmt.Println(<-ch2) // 3
}

Select: Multiplexación de Channels

El statement select permite esperar en múltiples operaciones de channels.

package main

import (
    "fmt"
    "time"
)

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("Finalizando...")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)
    
    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    
    fibonacci(c, quit)
}

WaitGroup: Sincronización de Goroutines

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Marcar como completado al finalizar
    
    fmt.Printf("Worker %d iniciando\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d finalizado\n", id)
}

func main() {
    var wg sync.WaitGroup
    
    for i := 1; i <= 5; i++ {
        wg.Add(1) // Incrementar contador
        go worker(i, &wg)
    }
    
    wg.Wait() // Esperar a que todos terminen
    fmt.Println("Todos los workers completados")
}

Mutex: Protección de Datos Compartidos

package main

import (
    "fmt"
    "sync"
)

type SafeCounter struct {
    mu    sync.Mutex
    count map[string]int
}

func (c *SafeCounter) Inc(key string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count[key]++
}

func (c *SafeCounter) Value(key string) int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count[key]
}

func main() {
    counter := SafeCounter{count: make(map[string]int)}
    var wg sync.WaitGroup
    
    // 1000 goroutines incrementando el contador
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Inc("visitas")
        }()
    }
    
    wg.Wait()
    fmt.Println("Total visitas:", counter.Value("visitas"))
}

Patrón Producer-Consumer

package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

type Task struct {
    ID   int
    Data string
}

func producer(tasks chan<- Task, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for i := 1; i <= 10; i++ {
        task := Task{
            ID:   i,
            Data: fmt.Sprintf("Tarea-%d", i),
        }
        fmt.Printf("Produciendo: %s\n", task.Data)
        tasks <- task
        time.Sleep(time.Duration(rand.Intn(500)) * time.Millisecond)
    }
}

func consumer(id int, tasks <-chan Task, wg *sync.WaitGroup) {
    defer wg.Done()
    
    for task := range tasks {
        fmt.Printf("Consumer %d procesando: %s\n", id, task.Data)
        time.Sleep(time.Duration(rand.Intn(1000)) * time.Millisecond)
    }
}

func main() {
    tasks := make(chan Task, 5)
    var producerWg sync.WaitGroup
    var consumerWg sync.WaitGroup
    
    // Iniciar producer
    producerWg.Add(1)
    go producer(tasks, &producerWg)
    
    // Iniciar 3 consumers
    for i := 1; i <= 3; i++ {
        consumerWg.Add(1)
        go consumer(i, tasks, &consumerWg)
    }
    
    // Esperar a que el producer termine
    producerWg.Wait()
    close(tasks)
    
    // Esperar a que todos los consumers terminen
    consumerWg.Wait()
    fmt.Println("Procesamiento completado")
}

Context: Control de Cancelación y Timeouts

package main

import (
    "context"
    "fmt"
    "time"
)

func operation(ctx context.Context, duration time.Duration) {
    select {
    case <-time.After(duration):
        fmt.Println("Operación completada")
    case <-ctx.Done():
        fmt.Println("Operación cancelada:", ctx.Err())
    }
}

func main() {
    // Context con timeout
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    
    fmt.Println("Iniciando operación con timeout de 2s...")
    operation(ctx, 3*time.Second)
    
    // Context con cancelación manual
    ctx2, cancel2 := context.WithCancel(context.Background())
    
    go func() {
        time.Sleep(1 * time.Second)
        cancel2() // Cancelar después de 1 segundo
    }()
    
    fmt.Println("\nIniciando operación con cancelación manual...")
    operation(ctx2, 3*time.Second)
}

Parte 2: Optimización de Memoria

Entendiendo la Gestión de Memoria en Go

Go utiliza un recolector de basura (Garbage Collector) automático, pero entender cómo funciona la memoria es crucial para escribir código eficiente.

Stack vs Heap

package main

import "fmt"

// Esto se almacena en el STACK (más rápido)
func stackAllocation() int {
    x := 42
    return x
}

// Esto PUEDE escaparse al HEAP
func heapAllocation() *int {
    x := 42
    return &x // El puntero escapa
}

func main() {
    a := stackAllocation()
    b := heapAllocation()
    
    fmt.Println(a, *b)
}

Análisis de escape:

go build -gcflags="-m" main.go

Slices: Uso Eficiente de Memoria

package main

import "fmt"

func inefficientSlice() []int {
    // Ineficiente: múltiples reasignaciones
    var nums []int
    for i := 0; i < 10000; i++ {
        nums = append(nums, i)
    }
    return nums
}

func efficientSlice() []int {
    // Eficiente: pre-asignar capacidad
    nums := make([]int, 0, 10000)
    for i := 0; i < 10000; i++ {
        nums = append(nums, i)
    }
    return nums
}

func main() {
    // Ver diferencia con benchmarks
    s1 := inefficientSlice()
    s2 := efficientSlice()
    
    fmt.Printf("Slice 1 len: %d, cap: %d\n", len(s1), cap(s1))
    fmt.Printf("Slice 2 len: %d, cap: %d\n", len(s2), cap(s2))
}

String vs byte: Optimización

package main

import (
    "fmt"
    "strings"
)

func inefficientStringConcat(words []string) string {
    result := ""
    for _, word := range words {
        result += word + " " // Crea nueva string cada vez
    }
    return result
}

func efficientStringConcat(words []string) string {
    var builder strings.Builder
    for _, word := range words {
        builder.WriteString(word)
        builder.WriteString(" ")
    }
    return builder.String()
}

func main() {
    words := []string{"Go", "es", "increíble", "para", "concurrencia"}
    
    s1 := inefficientStringConcat(words)
    s2 := efficientStringConcat(words)
    
    fmt.Println(s1)
    fmt.Println(s2)
}

Sync.Pool: Reutilización de Objetos

package main

import (
    "bytes"
    "fmt"
    "sync"
)

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func processData(data string) string {
    // Obtener buffer del pool
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf) // Devolver al pool
    }()
    
    buf.WriteString("Procesado: ")
    buf.WriteString(data)
    return buf.String()
}

func main() {
    var wg sync.WaitGroup
    
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            result := processData(fmt.Sprintf("dato-%d", id))
            fmt.Println(result)
        }(i)
    }
    
    wg.Wait()
}

Estructuras Compactas: Alineación de Memoria

package main

import (
    "fmt"
    "unsafe"
)

// Estructura mal alineada (24 bytes en arquitectura de 64 bits)
type BadStruct struct {
    a bool   // 1 byte + 7 padding
    b int64  // 8 bytes
    c bool   // 1 byte + 7 padding
}

// Estructura bien alineada (16 bytes)
type GoodStruct struct {
    b int64  // 8 bytes
    a bool   // 1 byte
    c bool   // 1 byte + 6 padding
}

func main() {
    fmt.Printf("BadStruct size: %d bytes\n", unsafe.Sizeof(BadStruct{}))
    fmt.Printf("GoodStruct size: %d bytes\n", unsafe.Sizeof(GoodStruct{}))
}

Profiling de Memoria

package main

import (
    "fmt"
    "runtime"
    "time"
)

func allocateMem() {
    data := make([][]int, 1000)
    for i := range data {
        data[i] = make([]int, 1000)
    }
    time.Sleep(2 * time.Second)
}

func printMemStats() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    
    fmt.Printf("Alloc = %v MB", bToMb(m.Alloc))
    fmt.Printf("\tTotalAlloc = %v MB", bToMb(m.TotalAlloc))
    fmt.Printf("\tSys = %v MB", bToMb(m.Sys))
    fmt.Printf("\tNumGC = %v\n", m.NumGC)
}

func bToMb(b uint64) uint64 {
    return b / 1024 / 1024
}

func main() {
    fmt.Println("Antes de allocateMem:")
    printMemStats()
    
    allocateMem()
    
    fmt.Println("\nDespués de allocateMem:")
    printMemStats()
    
    runtime.GC()
    
    fmt.Println("\nDespués de GC:")
    printMemStats()
}

Patrones Avanzados de Concurrencia

Worker Pool Pattern

package main

import (
    "fmt"
    "sync"
    "time"
)

type Job struct {
    ID     int
    Params map[string]interface{}
}

type Result struct {
    JobID  int
    Output interface{}
    Error  error
}

type WorkerPool struct {
    workers    int
    jobs       chan Job
    results    chan Result
    wg         sync.WaitGroup
}

func NewWorkerPool(workers int) *WorkerPool {
    return &WorkerPool{
        workers: workers,
        jobs:    make(chan Job, 100),
        results: make(chan Result, 100),
    }
}

func (wp *WorkerPool) worker(id int) {
    defer wp.wg.Done()
    
    for job := range wp.jobs {
        fmt.Printf("Worker %d procesando job %d\n", id, job.ID)
        
        // Simular trabajo
        time.Sleep(500 * time.Millisecond)
        
        wp.results <- Result{
            JobID:  job.ID,
            Output: fmt.Sprintf("Resultado del job %d", job.ID),
            Error:  nil,
        }
    }
}

func (wp *WorkerPool) Start() {
    for i := 1; i <= wp.workers; i++ {
        wp.wg.Add(1)
        go wp.worker(i)
    }
}

func (wp *WorkerPool) Submit(job Job) {
    wp.jobs <- job
}

func (wp *WorkerPool) Close() {
    close(wp.jobs)
}

func (wp *WorkerPool) Wait() {
    wp.wg.Wait()
    close(wp.results)
}

func main() {
    pool := NewWorkerPool(3)
    pool.Start()
    
    // Enviar jobs
    go func() {
        for i := 1; i <= 10; i++ {
            pool.Submit(Job{
                ID:     i,
                Params: map[string]interface{}{"data": i * 10},
            })
        }
        pool.Close()
    }()
    
    // Recoger resultados
    go func() {
        for result := range pool.results {
            fmt.Printf("Resultado recibido: %v\n", result.Output)
        }
    }()
    
    pool.Wait()
    time.Sleep(100 * time.Millisecond)
    fmt.Println("Todos los jobs procesados")
}

Pipeline Pattern

package main

import "fmt"

func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func filter(in <-chan int, predicate func(int) bool) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            if predicate(n) {
                out <- n
            }
        }
        close(out)
    }()
    return out
}

func main() {
    // Pipeline: generator -> square -> filter
    nums := generator(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    squared := square(nums)
    filtered := filter(squared, func(n int) bool {
        return n > 25 // Solo números > 25
    })
    
    for n := range filtered {
        fmt.Println(n)
    }
}

Fan-Out, Fan-In Pattern

package main

import (
    "fmt"
    "sync"
    "time"
)

func source() <-chan int {
    out := make(chan int)
    go func() {
        for i := 1; i <= 10; i++ {
            out <- i
            time.Sleep(100 * time.Millisecond)
        }
        close(out)
    }()
    return out
}

func worker(id int, in <-chan int) <-chan string {
    out := make(chan string)
    go func() {
        for num := range in {
            result := fmt.Sprintf("Worker %d procesó %d", id, num)
            time.Sleep(500 * time.Millisecond)
            out <- result
        }
        close(out)
    }()
    return out
}

func merge(channels ...<-chan string) <-chan string {
    var wg sync.WaitGroup
    out := make(chan string)
    
    output := func(c <-chan string) {
        defer wg.Done()
        for msg := range c {
            out <- msg
        }
    }
    
    wg.Add(len(channels))
    for _, c := range channels {
        go output(c)
    }
    
    go func() {
        wg.Wait()
        close(out)
    }()
    
    return out
}

func main() {
    src := source()
    
    // Fan-out: distribuir trabajo a 3 workers
    w1 := worker(1, src)
    w2 := worker(2, src)
    w3 := worker(3, src)
    
    // Fan-in: combinar resultados
    results := merge(w1, w2, w3)
    
    for result := range results {
        fmt.Println(result)
    }
}

Benchmarking y Testing

Benchmark de Concurrencia

package main

import (
    "sync"
    "testing"
)

func sequentialSum(n int) int {
    sum := 0
    for i := 0; i <= n; i++ {
        sum += i
    }
    return sum
}

func concurrentSum(n int, workers int) int {
    var wg sync.WaitGroup
    var mu sync.Mutex
    sum := 0
    chunkSize := n / workers
    
    for i := 0; i < workers; i++ {
        wg.Add(1)
        start := i * chunkSize
        end := start + chunkSize
        
        go func(s, e int) {
            defer wg.Done()
            localSum := 0
            for j := s; j < e; j++ {
                localSum += j
            }
            mu.Lock()
            sum += localSum
            mu.Unlock()
        }(start, end)
    }
    
    wg.Wait()
    return sum
}

func BenchmarkSequential(b *testing.B) {
    for i := 0; i < b.N; i++ {
        sequentialSum(1000000)
    }
}

func BenchmarkConcurrent(b *testing.B) {
    for i := 0; i < b.N; i++ {
        concurrentSum(1000000, 4)
    }
}

Ejecutar benchmarks:

go test -bench=. -benchmem

Mejores Prácticas

1. Evitar Goroutine Leaks

// MAL - Goroutine leak
func badFunction() {
    ch := make(chan int)
    go func() {
        ch <- 1 // Se bloquea eternamente si nadie lee
    }()
    // La goroutine nunca termina
}

// BIEN - Usar buffered channel o leer el valor
func goodFunction() {
    ch := make(chan int, 1)
    go func() {
        ch <- 1 // No se bloquea
    }()
}

2. Usar defer con Mutex

func (c *Counter) Inc() {
    c.mu.Lock()
    defer c.mu.Unlock() // Garantiza unlock incluso con panic
    c.count++
}

3. Limitar el Número de Goroutines

func processItems(items []Item) {
    sem := make(chan struct{}, 10) // Máximo 10 goroutines
    
    for _, item := range items {
        sem <- struct{}{} // Adquirir
        go func(i Item) {
            defer func() { <-sem }() // Liberar
            process(i)
        }(item)
    }
}

Conclusión

Go ofrece herramientas poderosas para concurrencia y optimización de memoria:

Concurrencia:

  • ✅ Goroutines ligeras y eficientes
  • ✅ Channels para comunicación segura
  • ✅ Primitivas de sincronización (WaitGroup, Mutex)
  • ✅ Patrones avanzados (Worker Pool, Pipeline, Fan-Out/Fan-In)

Memoria:

  • ✅ Entender Stack vs Heap
  • ✅ Pre-asignar capacidad en slices
  • ✅ Usar strings.Builder para concatenación
  • ✅ sync.Pool para reutilización
  • ✅ Optimizar alineación de estructuras

rauantodev