Guía de Go (Golang): De 0 a 100

Guía de Go (Golang): De 0 a 100

Desde la primera línea de código hasta producción: instalación, ecosistema, frameworks, proyectos reales y recursos en español.

FECHA:
ACTUALIZADO:
AUTOR:Jorge Beneyto Castelló
LECTURA:135 MINUTOS DE LECTURA

Guía de Go (Golang): De 0 a 100

¿Qué es Go?

Go (o Golang) es un lenguaje de programación compilado, de tipado estático y concurrente, creado en Google por Robert Griesemer, Rob Pike y Ken Thompson y lanzado públicamente en 2009. Su diseño prioriza la simplicidad, la legibilidad y la productividad del equipo por encima de la expresividad del lenguaje.

Filosofía:

  • Compilación rápida, binarios estáticos sin dependencias
  • Concurrencia nativa con goroutines y canales
  • Tipado estático con inferencia
  • Sin herencia de clases (composición sobre herencia)
  • Sin genéricos hasta 1.18 (llegaron en 2022 con polémica)
  • gofmt como estándar de formato — el formateo no es negociable

¿Dónde se usa?

  • CLI y herramientas de sistema: Docker, Kubernetes, Terraform, Hugo, Prometheus, Caddy, Syncthing
  • APIs REST y microservicios: net/http, Gin, Echo, Fiber, Chi
  • Infraestructura cloud: Docker, Kubernetes, etcd, Consul, Vault, Traefik
  • Bases de datos: InfluxDB, CockroachDB, TiDB, Dgraph, Badger
  • Redes y proxies: Caddy, Traefik, Envoy (data plane), gRPC
  • DevOps: Terraform, Packer, Vault, Consul, Nomad (todo HashiCorp)
  • Blockchain: Hyperledger Fabric, go-ethereum

¿Quién lo usa? Google (YouTube, Chrome), Docker Inc, HashiCorp, Cloudflare, Uber, Twitch, Dropbox, Netflix (partes), PayPal, Mercado Libre, Salesforce.

Fuente: Go dev — About, Wikipedia — Go, Go at Google (Rob Pike)


Prerrequisitos

Antes de empezar, necesitas:

  • Conocimientos básicos de terminal: navegar directorios, ejecutar comandos, usar un editor de texto
  • El lenguaje/herramienta instalado en tu sistema (sigue la sección de Instalación si aún no lo tienes)

Si no cumples algún requisito, no te preocupes: cada sección te guiará paso a paso.

¿Cómo empezar? Instalación en todos los entornos

Linux (Debian/Ubuntu)

# Opción 1: paquete oficial
wget https://go.dev/dl/go1.24.2.linux-amd64.tar.gz
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.24.2.linux-amd64.tar.gz
echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.bashrc
source ~/.bashrc
go version

# Opción 2: gestor de paquetes
sudo apt install golang-go  # versión del repo, suele ir atrasada

# Opción 3: snap
sudo snap install go --classic

macOS

# Homebrew (recomendado)
brew install go@1.24

# Binario oficial
wget https://go.dev/dl/go1.24.2.darwin-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.24.2.darwin-amd64.tar.gz

Windows

  1. Descarga el instalador MSI desde go.dev/dl
  2. El instalador añade Go al PATH automáticamente
  3. Verifica en PowerShell: go version
  4. Alternativa vía winget: winget install GoLang.Go

gvm (Go Version Manager) — múltiples versiones

bash < <(curl -sSL https://raw.githubusercontent.com/moovweb/gvm/master/binscripts/gvm-installer)
source ~/.gvm/scripts/gvm

gvm install go1.24.2
gvm install go1.23.6
gvm use go1.24.2 --default

Docker multi-stage (producción)

# Build stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server .

# Runtime stage — imagen mínima
FROM scratch
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]

Usa golang:1.24-alpine para build (~350 MB) y scratch o distroless para runtime (~5 MB). CGO_ENABLED=0 produce binarios estáticos que funcionan en cualquier Linux sin dependencias.

Alias útiles para el día a día

Añade a tu ~/.bashrc o ~/.zshrc:

alias grun='go run .'
alias gtest='go test -v ./...'
alias gmod='go mod tidy'
alias gbuild='go build -o bin/ ./...'
alias gfmt='gofmt -l -w .'
alias gvet='go vet ./...'
alias gall='go fmt ./... && go vet ./... && go mod tidy'

Fuente: Go dev — Download, gvm GitHub, Docker Official Images — Golang


Escala de aprendizaje: de 0 a 100

Cada nivel incluye lo que debes saber y un proyecto para fijar los conceptos. Las secciones nuevas de esta guía (Paradigmas, Tipos de datos, Control de flujo, Sistema de archivos, Algoritmos, Multimedia) se integran en los niveles según su dificultad conceptual.

Nivel 0–15: Fundamentos absolutos

Qué aprender:

  • Sintaxis básica: package main, func main(), import, fmt.Println
  • Variables: var, :=, tipos básicos (string, int, float64, bool)
  • Control de flujo: if, else, for (único bucle), switch — ver sección Control de flujo y modularidad
  • Funciones: func, parámetros, valores de retorno múltiples
  • Zero values: Go inicializa todo a cero/nil automáticamente
  • fmt.Printf() con %s, %d, %v, %+v, %T
  • Conceptos clave: Go es compilado — necesitas go build o go run. Tipado estático: no puedes sumar int y float64 sin convertir. Las llaves { van en la misma línea que la declaración (gofmt no da opción). Variables no usadas = error de compilación.

Proyecto: Adivina el número — el programa elige un número aleatorio entre 1 y 100 (rand.Intn), el usuario intenta adivinarlo con pistas “mayor/menor”. Contador de intentos. Manejo de entrada con fmt.Scanf o bufio.Scanner. Reflexión: ¿por qué Go fuerza a manejar el error de Scanf aunque no te importe? (no hay excepciones, los errores son valores)

Nivel 15–30: Structs, slices, maps y punteros

Qué aprender:

  • Slices: make([]int, 5), append, slicing[:3], copy — ver sección Tipos de datos y variables
  • Maps: make(map[string]int), mapeo["key"], comprobar existencia
  • Structs: type Usuario struct, campos, literales
  • Punteros: *T, &, cuándo pasar puntero vs valor
  • Métodos: receiver value vs pointer
  • Strings: strings.Split, strings.Join, strconv.Atoi, fmt.Sprintf
  • Archivos: os.Open, os.ReadFile, bufio.Scanner, io.Copy — ver sección Sistema de archivos
  • defer para cleanup — ver sección Control de flujo y modularidad
  • Algoritmos básicos: sort.Slice, slices.Sort, container/list — ver sección Algoritmos
  • Conceptos clave: un slice NO tiene datos, es un descriptor (puntero, longitud, capacidad). append puede reasignar el slice si no cabe. Los maps devuelven zero value si la clave no existe — usa val, ok := m["key"]. Los strings son inmutables y UTF-8. range sobre slice devuelve índice y valor, sobre map devuelve clave y valor (orden aleatorio).

Proyecto: Gestor de tareas CLI — añadir (add "Comprar leche"), listar (list), completar (done 3), eliminar (rm 3). Persistencia en JSON (encoding/json). Usa flag para los comandos. Struct Tarea con ID, Texto, Hecho. Slice de tareas. Punteros para modificar tareas existentes.

Nivel 30–45: Interfaces, errores y paquetes

Qué aprender:

  • Interfaces: definición, implementación implícita, io.Reader/io.Writer
  • Type assertions y type switches
  • Errores: error interface, fmt.Errorf, errors.Is, errors.As, errores personalizados
  • Paquetes: exportación pública/privada, internal, go.mod — ver sección Control de flujo y modularidad
  • Embedding de structs (composición) — ver sección Paradigmas
  • init() functions
  • encoding/json: marshal/unmarshal structs, tags json:
  • Paradigmas en acción: POO sin clases (structs + métodos + interfaces), funcional parcial (closures, funciones como valores) — ver sección Paradigmas
  • Conceptos clave: una interfaz es un tipo (valor + type descriptor). nil en una interfaz no es lo mismo que un puntero nil. La interfaz vacía interface{} (o any) es el “tope” del sistema de tipos. Go no tiene try/catch — cada error se maneja explícitamente. Las interfaces pequeñas (1 método) son la norma.

Proyecto: Sistema de inventario — tipos Producto, Inventario (map[string]*Producto). Interfaz Almacenable con métodos Guardar(), Cargar(). Implementación con JSON y otra con SQLite. Errores personalizados (ProductoNoExiste, StockInsuficiente). Embedding para ProductoFisico y ProductoDigital. Tests con table-driven tests.

Nivel 45–60: Testing, módulos y librerías estándar

Qué aprender:

  • go test: _test.go, t.Run, table-driven tests, t.Helper
  • go test -bench benchmarks
  • go test -race detector de data races
  • go mod, go mod tidy, go get, versionado semántico
  • net/http: handlers, http.HandlerFunc, http.ServeMux, middleware
  • context.Context: WithCancel, WithTimeout, WithDeadline, WithValue
  • time: time.Now, time.After, time.Ticker, time.Sleep
  • sync: WaitGroup, Mutex, RWMutex, Once
  • Conceptos clave: los tests se colocan en el mismo paquete o en _test (black-box testing). El race detector es obligatorio en cualquier código concurrente. context.Background() es la raíz, context.TODO() marca pendiente, nunca almacenes context en structs. Los middlewares HTTP son funciones que envuelven un http.Handler.

Proyecto: API de recetas con net/http — servidor HTTP sin frameworks. Rutas /recetas (GET), /recetas/{id} (GET), /recetas (POST). Mux manual con http.ServeMux. Middleware de logging y CORS. Tests con httptest.NewRecorder. Context para timeouts. Mutex para acceso concurrente al slice de recetas.

Nivel 60–75: Frameworks web y bases de datos

Qué aprender:

  • Gin, Echo o Chi para APIs REST
  • GORM o sqlx para bases de datos
  • Migraciones con golang-migrate
  • Validación de datos (struct tags, validator)
  • JWT: autenticación middleware
  • Variables de entorno: os.Getenv, godotenv o viper
  • slog (Go 1.21+): log estructurado nativo
  • Conceptos clave: los frameworks Go son más finos que Django/FastAPI — no hay tanto magic. El tipado estático evita muchas clases de bugs. Los handlers reciben *gin.Context o echo.Context que unifican request/response. La inyección de dependencias se hace a mano (funciones que reciben dependencias como parámetros).

Proyecto: API de recetas con Gin + PostgreSQL — endpoints CRUD para recetas e ingredientes. GORM para ORM, golang-migrate para migraciones. Autenticación JWT. Búsqueda por ingrediente con SQL parametrizado. Tests con BD de prueba en memoria (SQLite para tests, PostgreSQL en producción). slog para logging estructurado.

Nivel 75–90: Concurrencia avanzada y producción

Qué aprender:

  • Worker pools con errgroup
  • Rate limiting con golang.org/x/time/rate
  • Graceful shutdown de servidores HTTP
  • Docker multi-stage para Go
  • CI/CD: GitHub Actions con go build, go test, golangci-lint
  • Redis: caché de endpoints, rate limiting, sesiones
  • Pruebas de integración con testcontainers-go
  • Profiling: pprof, trace, memory profiling
  • Conceptos clave: signal.NotifyContext para graceful shutdown. errgroup para cancelar todas las goroutines si una falla. sync.WaitGroup + canales = worker pool. El binario estático de Go simplifica Docker enormemente (scratch image de ~10 MB). GOMAXPROCS por defecto = todos los cores.

Proyecto: Worker de procesamiento de imágenes — API que acepta URLs de imágenes, las encola en Redis, workers las procesan concurrentemente. errgroup para control de errores. Rate limiting por usuario. Graceful shutdown que espera a workers terminar. Docker multi-stage (imagen final ~15 MB). CI con golangci-lint, tests, race detector.

Nivel 90–100: Arquitectura y sistemas distribuidos

Qué aprender:

  • gRPC: protobuf, servidores y clientes, streaming bidireccional
  • Kafka: segmentio/kafka-go o confluent-kafka-go
  • Arquitectura hexagonal / clean architecture en Go
  • OpenTelemetry: tracing distribuido, métricas
  • Fuzzing con go test -fuzz
  • Go generics avanzados: constraints, type inference, patterns
  • Plugins con plugin package o WASM
  • Conceptos clave: gRPC es el protocolo RPC estándar en Go — Google lo usa internamente. Protobuf es más eficiente que JSON pero menos legible. Kafka ofrece garantías de entrega (at-least-once, exactly-once). Las arquitecturas hexagonales se implementan con interfaces: dominio puro, adaptadores de infraestructura. Los genéricos permiten librerías de colecciones, pipelines, y DSLs internos sin reflection.

Proyecto: Sistema de notificaciones distribuidas — microservicios con gRPC para comunicación interna. Kafka como event bus (servicio usuarios → servicio notificaciones → servicio email). OpenTelemetry para tracing. Arquitectura hexagonal: dominio (Notificacion, Usuario), puertos (interfaces), adaptadores (Kafka, gRPC, SMTP). Docker Compose con todos los servicios. Fuzzing en la validación de entrada.

Fuente: Go dev — doc, Gin docs, GORM docs, gRPC Go docs


Primeros pasos y configuración del entorno

Si prefieres ver directamente el “Hola Mundo” en Go, consulta El Atlas del Hola Mundo.

Estructura de proyecto con módulos

Go moderno usa módulos (go mod). El GOPATH es cosa del pasado desde Go 1.16.

# Inicializar un módulo
go mod init github.com/tu-usuario/mi-proyecto

# Añadir dependencias
go get github.com/gin-gonic/gin

# Limpiar dependencias no usadas
go mod tidy

Resultado: archivos go.mod (módulos y versiones) y go.sum (checksums de seguridad).

Hola Mundo

package main

import "fmt"

func main() {
    fmt.Println("Hola Mundo")
}

Ejecuta: go run main.go o go run . (todo el paquete).

Compilación

go build -o mi-programa .         # Binario para tu arquitectura
GOOS=linux GOARCH=amd64 go build  # Cross-compile para Linux
GOOS=windows GOARCH=amd64 go build  # Cross-compile para Windows

Go produce binarios estáticos: no necesitas runtime, ni JVM, ni nada. Copias el binario y funciona.

Herramientas del lenguaje

go fmt ./...      # Formatea todo el código (indentación, alineación)
go vet ./...      # Analiza bugs sospechosos (variable no usada, etc.)
go mod tidy       # Limpia go.mod y go.sum
go doc fmt.Println  # Documentación desde terminal
go env            # Variables de entorno del toolchain
go list ./...     # Lista todos los paquetes del módulo

Editores recomendados

Los IDEs y editores más usados son VSCode, JetBrains y Neovim. Extensiones y configuraciones recomendadas.

GOPATH vs módulos

AspectoGOPATH (obsoleto)Módulos (actual)
Ubicación$GOPATH/src/...Cualquier directorio
Dependenciasgo get globalVersionadas en go.mod
Versionesmaster siempreSemVer fijo, reproducible
Herramientadep/govendorgo mod nativo
ReproducibilidadNoSí (go.sum verifica checksums)

Desde Go 1.16, GO111MODULE=on es el comportamiento por defecto. Si ves tutoriales que hablan de GOPATH, son anteriores a 2020.

Fuente: Go dev — Getting Started, Go dev — Modules, gopls


Paradigmas de programación en Go

Go es un lenguaje multiparadigma que combina lo mejor de varios mundos sin llegar a ser extremo en ninguno.

Imperativo / Procedural

El núcleo de Go es imperativo: el programa se ejecuta secuencialmente, instrucción tras instrucción.

func main() {
    var sum int
    for i := 1; i <= 10; i++ {
        sum += i
    }
    fmt.Println("Suma:", sum)
}

Concurrente (el paradigma estrella)

Go está diseñado desde sus cimientos para la concurrencia. Goroutines y canales son el modelo de comunicación entre tareas concurrentes, inspirado en los Communicating Sequential Processes (CSP) de Hoare.

func main() {
    ch := make(chan string)
    go func() {
        ch <- "Hola desde goroutine"
    }()
    fmt.Println(<-ch)
}

La filosofía de Go: “Do not communicate by sharing memory; instead, share memory by communicating.”

POO sin clases

Go no tiene clases ni herencia. En su lugar ofrece:

  • Structs para definir tipos compuestos
  • Métodos con receptores explícitos
  • Interfaces implícitas (duck typing en tiempo de compilación)
  • Embedding (composición sobre herencia)
type Animal struct{ Nombre string }
func (a Animal) Saludar() string { return "Soy " + a.Nombre }

type Perro struct{ Animal }
func (p Perro) Saludar() string { return "Guau, soy " + p.Nombre }

Funcional parcial

Go soporta programación funcional de forma limitada:

  • First-class functions: las funciones son valores
  • Closures: funciones que capturan variables del ámbito exterior
  • Funciones como parámetros y retorno
func mapSlice(f func(int) int, nums []int) []int {
    result := make([]int, len(nums))
    for i, n := range nums {
        result[i] = f(n)
    }
    return result
}

func main() {
    doble := func(x int) int { return x * 2 }
    fmt.Println(mapSlice(doble, []int{1, 2, 3})) // [2, 4, 6]
}

Limitaciones: no hay genéricos hasta Go 1.18; no hay map/filter/reduce nativos en la stdlib; no hay tuplas; no hay pattern matching. La stdlib tiene slices y maps desde Go 1.21 con funciones utilitarias.

Lo que Go NO tiene

AusenciaConsecuencia
HerenciaNo hay extends. Se usa composición con embedding
ExcepcionesNo hay try/catch. Los errores son valores (error interface)
Genéricos (hasta 1.18)Había que usar interface{} / any o generar código
SobrecargaNo puedes tener dos funciones con el mismo nombre
Operadores personalizadosNo hay sobrecarga de operadores
Herencia múltipleNi falta que hace con interfaces implícitas
Aritmética de punterosLos punteros existen pero no puedes hacer p++

Fuente: Effective Go, Go FAQ, Rob Pike — Go at Google, Concurrency is not Parallelism (Rob Pike)


Tipos de datos y variables

Tipos built-in y zero values

¿Qué es?

Go tiene un conjunto pequeño de tipos built-in: booleanos, enteros (con/sin signo), flotantes, complejos y string. Todos tienen zero values automáticos sin necesidad de inicialización. Go no tiene null, solo nil para ciertos tipos.

🔗 Go dev — Spec types

Sintaxis básica

// Tipos built-in
var activo bool          // false
var edad int             // 0 (32 o 64 bits según arquitectura)
var peso float64         // 0.0
var nombre string        // ""
var puntero *int         // nil
var lista []string       // nil
var mapa map[string]int  // nil
var canal chan int       // nil

// byte y rune son alias
var b byte = 255         // uint8
var r rune = '世'         // int32 (code point Unicode)

// Zero values: no puedes usar variable sin inicializar
var s string
fmt.Println(s)           // "" (vacío, no panic)

🧪 Cómo probarlo

func TestZeroValues(t *testing.T) {
    var s string
    var i int
    var b bool
    if s != "" { t.Error("string zero value should be empty") }
    if i != 0 { t.Error("int zero value should be 0") }
    if b != false { t.Error("bool zero value should be false") }
}

💡 Memoria y rendimiento

  • int/uint son tamaño de palabra nativa (64 bits en x86-64) — óptimo para la CPU
  • byte y rune ocupan 1 y 4 bytes respectivamente
  • string es un descriptor de 16 bytes (puntero + longitud) — el contenido está en heap
  • nil en slices/maps/channels evita alocar hasta que se usan

✅ Buenas prácticas

  • ✅ Usa int por defecto para enteros (no int8/int16 a menos que ahorres memoria)
  • ✅ Usa float64 por defecto para flotantes
  • ⚠️ No confundas byte (uint8) con rune (int32)
  • ❌ No accedas a campos de un *struct que es nil — panic

🏗️ Metodología

  • APIs: usa int y float64 para valores numéricos
  • Serialización: int/float64 están optimizados en encoding/json
  • Juegos/embebido: usa int8/int16/float32 para ahorrar memoria

🔗 Para saber más

Declaración de variables e inferencia

¿Qué es?

Go soporta var (explícito, ámbito global o local) y := (corto, solo dentro de funciones). La inferencia de tipos siempre elige el tipo por defecto según el valor.

🔗 Go dev — Spec variables

Sintaxis básica

// Declaración explícita
var nombre string = "Ana"
var edad = 30  // inferido: int

// Short declaration (solo dentro de funciones)
nombre := "Ana"
edad := 30

// Múltiples variables
var x, y int = 1, 2
a, b := "hola", 42

// La inferencia elige tipos por defecto
var x = 42       // int
var y = 3.14     // float64
var z = 3 + 4i   // complex128

🧪 Cómo probarlo

func TestTypeInference(t *testing.T) {
    x := 42
    y := 3.14
    if fmt.Sprintf("%T", x) != "int" { t.Error("42 should be int") }
    if fmt.Sprintf("%T", y) != "float64" { t.Error("3.14 should be float64") }
}

✅ Buenas prácticas

  • ✅ Usa := dentro de funciones para código conciso
  • ✅ Usa var para valores zero explícitos (var i int)
  • ⚠️ No uses := fuera de funciones — no compila
  • ❌ No declares variables que no usas — error de compilación

Conversión explícita y tipos compuestos

¿Qué es?

Go no convierte implícitamente entre tipos. Incluso entre int y int32 necesitas cast explícito. Los tipos compuestos incluyen struct, array, slice, map, punteros, any y error.

🔗 Go dev — Conversions

Sintaxis básica

// Conversión explícita
var a int = 42
var b int32 = int32(a)
var c float64 = float64(a)
var d string = strconv.Itoa(a)  // int → string requiere strconv

// Struct
type Punto struct { X, Y float64 }
p := Punto{X: 3.0, Y: 4.0}

// Array (tamaño fijo) vs Slice (dinámico)
arr := [3]int{1, 2, 3}    // array fijo
sl := []int{1, 2, 3}      // slice dinámico
sl = append(sl, 4)

// Map
m := make(map[string]int)
m["clave"] = 42
val, ok := m["clave"]

// Punteros (sin aritmética)
var x int = 42
var p *int = &x
*p = 100  // modifica x

// any (interface{} genérico)
var v any = 42
v = "texto"

// error es una interfaz
type error interface { Error() string }

🧪 Cómo probarlo

func TestMapOk(t *testing.T) {
    m := map[string]int{"a": 1}
    v, ok := m["a"]
    if !ok { t.Error("key should exist") }
    if v != 1 { t.Error("value should be 1") }
}

func TestNilSlice(t *testing.T) {
    var s []int
    // nil slice: append funciona sin panic
    s = append(s, 1)
    if len(s) != 1 { t.Error("append should work on nil slice") }
}

💡 Memoria y rendimiento

  • Structs son value types — se copian al asignar o pasar por parámetro
  • Slice es un descriptor de 24 bytes (ptr + len + cap) — el array subyacente está en heap
  • Map es un puntero a una tabla hash interna — pasarlo es barato
  • any implica boxing para value types (como interface en C#/Java)

✅ Buenas prácticas

  • ✅ Usa make() para maps y slices con capacidad inicial conocida
  • ✅ Usa structs por puntero solo si necesitas mutación o el struct es grande (>64 bytes)
  • ⚠️ No uses any a menos que sea estrictamente necesario — pierdes type safety
  • ❌ No compares slices directamente con == (solo con nil) — usa reflect.DeepEqual o slices.Equal

🏗️ Metodología

  • Datos inmutables: structs por valor
  • Colecciones dinámicas: slices (no arrays de tamaño fijo)
  • Lookups: maps con ok idiom
  • Errores: siempre devuelve error como último valor

🔗 Para saber más

Strings (UTF-8)

¿Qué es?

Los strings en Go son inmutables y contienen UTF-8 (no ASCII). len(s) devuelve bytes, no caracteres. string es un slice de bytes de solo lectura.

🔗 Go dev — Strings

Sintaxis básica

s := "Hello, 世界"
fmt.Println(len(s))                    // 13 bytes
fmt.Println(utf8.RuneCountInString(s)) // 9 runas (caracteres)

// Convertir a bytes o runas
b := []byte(s)  // cada byte
r := []rune(s)  // cada code point Unicode

// Iteración correcta
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)  // i es offset en bytes
}

🧪 Cómo probarlo

func TestStringUTF8(t *testing.T) {
    s := "Hello, 世界"
    if len(s) != 13 { t.Error("len should be bytes, not chars") }
    if utf8.RuneCountInString(s) != 9 { t.Error("should be 9 runes") }
}

✅ Buenas prácticas

  • ✅ Usa range para iterar strings (itera por runa, no por byte)
  • ✅ Usa strings.Builder para concatenación en bucles
  • ⚠️ No asumas que un carácter = 1 byte — UTF-8 es variable
  • ❌ No modifiques un string directamente — son inmutables

Memoria: stack vs heap y GC

¿Qué es?

Go tiene escape analysis en compilación: decide si una variable va al stack o al heap. El GC es concurrente, no generacional, con pausas típicas <1ms desde Go 1.8.

🔗 Go dev — GC Guide

Sintaxis básica

// Stack: la variable no escapa
func enStack() int {
    x := 42
    return x
}

// Heap: la variable escapa (se referencia fuera)
func enHeap() *int {
    x := 42
    return &x
}

// Ver decisiones del compilador
// go build -gcflags='-m' ./...

💡 Memoria y rendimiento

  • Escape analysis decide stack vs heap — no hay control manual como en C/C++
  • El GC es concurrente (mark-sweep) con pausas de <1ms típicamente
  • Pasar structs por puntero no siempre es más rápido — los structs pequeños pueden ser más rápidos por valor
  • sync.Pool reusa objetos para reducir presión de GC

✅ Buenas prácticas

  • ✅ Prefiere devolver valores por valor (no punteros) a menos que necesites mutación
  • ✅ Usa -gcflags='-m' para diagnosticar fugas al heap inesperadas
  • ⚠️ No obsesiones con stack vs heap — el compilador optimiza bien
  • ❌ No uses punteros para “ahorrar memoria” en structs pequeños — suele ser peor

🔗 Para saber más

Control de flujo, modularidad y funciones

Control de flujo

¿Qué es?

Go tiene if, switch, for (único bucle), defer (ejecución diferida al salir del ámbito) y panic/recover (sin try/catch). El switch no tiene fallthrough por defecto.

🔗 Go dev — Control flow

Sintaxis básica

// if con short statement
if err := f(); err != nil {
    return err
}

// switch sin expresión (if/else if)
switch {
case nota >= 9: fmt.Println("sobresaliente")
case nota >= 7: fmt.Println("notable")
default: fmt.Println("aprobado o suspenso")
}

// for — único bucle, 3 formas
for i := 0; i < 10; i++ { fmt.Println(i) }
for x < 100 { x *= 2 }                          // como while
for i, v := range []int{10, 20, 30} { }         // range sobre slice/map/chan/string

// defer — LIFO, se ejecuta al salir del ámbito
f, _ := os.Open("archivo.txt")
defer f.Close()

// panic / recover (sin try/catch)
defer func() {
    if r := recover(); r != nil { fmt.Println("panic:", r) }
}()
panic("algo terrible")

🧪 Cómo probarlo

func TestSwitchSinExpresion(t *testing.T) {
    nota := 85
    resultado := ""
    switch {
    case nota >= 90: resultado = "sobresaliente"
    case nota >= 70: resultado = "notable"
    case nota >= 50: resultado = "aprobado"
    default: resultado = "suspenso"
    }
    if resultado != "notable" {
        t.Errorf("esperado notable, got %s", resultado)
    }
}

func TestDeferEjecutaAlFinal(t *testing.T) {
    resultado := ""
    func() {
        defer func() { resultado += "mundo" }()
        resultado += "hola "
    }()
    if resultado != "hola mundo" {
        t.Errorf("esperado 'hola mundo', got %s", resultado)
    }
}

💡 Memoria y rendimiento

  • defer tiene overhead mínimo (~50ns) — usa el patrón de cleanup siempre
  • for range sobre slices es tan rápido como indexación manual
  • switch compila a jump table cuando los casos son constantes
  • panic/recover no deben usarse para flujo normal — son costosos

✅ Buenas prácticas

  • ✅ Usa defer para todo cleanup (cerrar archivos, unlock mutex, cerrar BD)
  • ✅ Usa for range para iterar slices, maps, canales y strings
  • ⚠️ No uses panic para errores ordinarios — usa error
  • ❌ No abuses de recover — solo en servidores HTTP para evitar caídas

🏗️ Metodología

  • Errores predecibles: if err != nil idiom
  • Errores irrecuperables: log.Fatal en main, panic en init
  • Cleanup: siempre defer inmediatamente después de abrir recurso
  • Múltiples condiciones: switch sin expresión (más legible que if/else if)

🔗 Para saber más

Funciones

¿Qué es?

Las funciones en Go soportan múltiples retornos, valores nombrados, parámetros variádicos y closures. No hay sobrecarga ni parámetros con nombre.

🔗 Go dev — Spec functions

Sintaxis básica

// Múltiples retornos
func dividir(a, b float64) (float64, error) {
    if b == 0 { return 0, fmt.Errorf("división por cero") }
    return a / b, nil
}

// Retorno nombrado
func dividir(a, b float64) (result float64, err error) {
    if b == 0 { err = fmt.Errorf("división por cero"); return }
    result = a / b
    return
}

// Variádica
func sumar(nums ...int) int {
    total := 0
    for _, n := range nums { total += n }
    return total
}

🧪 Cómo probarlo

func TestMultiplesRetornos(t *testing.T) {
    _, err := dividir(10, 0)
    if err == nil { t.Error("esperaba error") }
}

func TestFuncionVariadica(t *testing.T) {
    if sumar(1, 2, 3) != 6 { t.Error("1+2+3 should be 6") }
}

✅ Buenas prácticas

  • ✅ Usa retornos nombrados para documentar el propósito de cada valor
  • ✅ Devuelve error como último valor de retorno
  • ⚠️ No hagas sobrecarga de funciones — Go no lo soporta
  • ❌ No ignores errores con _ sin pensar

Módulos y paquetes

¿Qué es?

Go organiza el código en paquetes (directorios). Cada archivo .go pertenece a un paquete. Un módulo es la unidad de versionado definida en go.mod. La exportación se controla por mayúscula/minúscula.

🔗 Go dev — Modules

Sintaxis básica

package usuario

type Usuario struct {   // exportado (mayúscula)
    Nombre string        // exportado
    email  string        // privado (minúscula)
}

func NuevoUsuario(nombre, email string) *Usuario {  // exportado
    return &Usuario{Nombre: nombre, email: email}
}
go mod init github.com/usuario/proyecto
go get github.com/gin-gonic/gin
go mod tidy

Internal packages (internal/) restringen importación al árbol padre del módulo. init() se ejecuta automáticamente al cargar el paquete.

🧪 Cómo probarlo

func TestExportacion(t *testing.T) {
    u := NuevoUsuario("Ana", "ana@test.com")
    if u.Nombre != "Ana" { t.Error("Nombre debería ser exportado") }
}

✅ Buenas prácticas

  • ✅ Nombra paquetes en singular y cortos (user, no users o user_utils)
  • ✅ Usa internal/ para código que no debe exponerse al exterior del módulo
  • ⚠️ No uses init() para lógica que podría hacerse explícita
  • ❌ No crees paquetes con nombres genéricos como utils o common

🔗 Para saber más

Conceptos clave explicados a fondo

Paquetes y exportación

¿Qué es?

Un paquete en Go es la unidad básica de organización del código. Todo archivo .go pertenece a un paquete. La visibilidad se controla por mayúscula (exportado) o minúscula (privado). Es como un archivador: cada cajón (paquete) tiene documentos, y solo los que tienen el nombre en mayúscula pueden verse desde fuera.

🔗 Go dev — Package reference

Sintaxis básica

package models   // El directorio se llama models/, el paquete se llama models

// Exportado: empieza con mayúscula — visible fuera del paquete
type Usuario struct {
    Nombre string  // Exportado
    email  string  // No exportado (privado al paquete)
}

// No exportado: solo visible dentro del paquete
func validarEmail(e string) bool {
    return strings.Contains(e, "@")
}

// Exportado
func NuevoUsuario(nombre, email string) *Usuario {
    return &Usuario{Nombre: nombre, email: email}
}

Convenciones:

  • package main → genera un ejecutable con func main()
  • Cualquier otro nombre → librería
  • El nombre del paquete coincide con el último segmento del directorio (models/models.go)
  • El directorio internal/ es especial: sus paquetes solo son importables por el árbol padre

El directorio internal:

// project/api/handler.go
import "project/internal/crypto"
// ✅ permitido — internal/ está dentro de project/

// otro-project/main.go
import "project/internal/crypto"
// ❌ error de compilación — internal/ no es accesible desde fuera

internal es el mecanismo de Go para crear APIs privadas dentro de un módulo. No hay private como en otros lenguajes: internal es la barrera de visibilidad entre módulos.

🧪 Cómo probarlo

func TestExportacion(t *testing.T) {
    // Probar acceso a símbolos exportados
    u := NuevoUsuario("Ana", "ana@test.com")
    if u.Nombre != "Ana" {
        t.Errorf("Nombre debería ser 'Ana', got %s", u.Nombre)
    }
    // email es privado — no se puede acceder desde otro paquete
}

💡 Memoria y rendimiento

  • Los paquetes se cargan una sola vez en go.mod y se cachean
  • internal no tiene overhead en runtime — solo es una restricción de compilación
  • Los import cíclicos se detectan en compilación — no pueden ocurrir en runtime
  • El paquete main no es importable por otros paquetes

✅ Buenas prácticas

  • ✅ Nombra paquetes en singular y cortos (user, no users o user_utils)
  • ✅ Usa internal/ para código que no debe exponerse al exterior del módulo
  • ⚠️ No uses init() para lógica que podría hacerse explícita
  • ❌ No crees paquetes con nombres genéricos como utils o common

🏗️ Metodología

  • APIs públicas: solo exporta lo que el usuario necesita, todo lo demás privado
  • Módulos grandes: divide en subpaquetes, usa internal/ para implementación
  • Librerías: paquete raíz con API pública, subpaquetes con implementación
  • Ejecutables: package main mínimo que delega en otros paquetes

🔗 Para saber más

Variables, tipos y zero values

¿Qué es?

Go es de tipado estático con inferencia de tipos mediante :=. Toda variable tiene un tipo conocido en compilación y un zero value automático (no hay null o undefined). Es como una taquilla numerada: sabes exactamente qué tamaño tiene cada compartimento y siempre está limpio aunque no metas nada.

🔗 Go dev — Variables

Sintaxis básica

// Declaración explícita
var nombre string = "Ana"
var edad int = 30
var activo bool = true

// Inferencia (short declaration — solo dentro de funciones)
nombre := "Ana"
edad := 30

// Zero values — Go inicializa todo a cero automáticamente
var s string    // "" (cadena vacía)
var i int       // 0
var f float64   // 0.0
var b bool      // false
var p *int      // nil (punteros, slices, maps, canales, interfaces)
var sl []string // nil
var m map[string]int // nil

// Múltiples variables
var x, y int = 1, 2
a, b := "hola", 42

// Tipos básicos
var entero int8     // -128 a 127
var entero32 int32  // -2^31 a 2^31-1
var entero64 int64  // -2^63 a 2^63-1
var sinSigno uint   // 0 a 2^64-1 (según arquitectura)
var decimal float32 // ~6 dígitos precisión
var decimal64 float64 // ~15 dígitos precisión
var texto string    // UTF-8, inmutable
var caracter rune   // alias de int32, representa un code point Unicode
var byte byte       // alias de uint8
var comaComplejo complex128 // números complejos

Conversiones explícitas — no hay coerciones automáticas:

var i int = 42
var f float64 = float64(i)   // ✅ explícita
var u uint = uint(f)          // ✅ explícita (pierde decimales)

// ❌ Esto no compila: Go no convierte implícitamente
// var f float64 = i

Incluso entre tipos numéricos del mismo tamaño, debes convertir explícitamente. Go prefiere la seguridad a la conveniencia.

iota — enumeradores automáticos:

type Estado int

const (
    Pendiente Estado = iota // 0
    Activo                  // 1
    Inactivo                // 2
    Archivado               // 3
)

type Permiso int

const (
    Leer Permiso = 1 << iota // 1 (1 << 0)
    Escribir                 // 2 (1 << 1)
    Ejecutar                 // 4 (1 << 2)
)

🧪 Cómo probarlo

func TestZeroValues(t *testing.T) {
    var s string
    var i int
    var b bool
    var p *int
    if s != "" { t.Error("string zero value debe ser vacío") }
    if i != 0 { t.Error("int zero value debe ser 0") }
    if b != false { t.Error("bool zero value debe ser false") }
    if p != nil { t.Error("pointer zero value debe ser nil") }
}

func TestIota(t *testing.T) {
    const (
        A = iota
        B
        C
    )
    if A != 0 || B != 1 || C != 2 {
        t.Errorf("iota esperado 0,1,2, got %d,%d,%d", A, B, C)
    }
}

func TestConversionExplicita(t *testing.T) {
    var a int = 42
    var b float64 = float64(a)
    if b != 42.0 { t.Errorf("esperado 42.0, got %f", b) }
}

💡 Memoria y rendimiento

  • int es tamaño de palabra nativa (64 bits en x86-64) — óptimo para la CPU
  • byte y rune ocupan 1 y 4 bytes respectivamente
  • string es un descriptor de 16 bytes (puntero + longitud) — contenido en heap
  • nil en slices/maps/channels evita alocar hasta que se usan
  • iota se evalúa en compilación — zero overhead en runtime

✅ Buenas prácticas

  • ✅ Usa := dentro de funciones para código conciso
  • ✅ Usa var para valores zero explícitos (var i int)
  • ⚠️ No confundas byte (uint8) con rune (int32)
  • ❌ No declares variables que no usas — error de compilación

🏗️ Metodología

  • Valores por defecto: Go inicializa a cero — no necesitas constructor
  • Conversión: siempre explícita — Go prioriza seguridad sobre conveniencia
  • Enumeraciones: iota para secuencias, bitsets con 1 << iota
  • APIs: usa int y float64 para valores numéricos genéricos

🔗 Para saber más

Structs, métodos y embedding

¿Qué es?

Los structs son la forma de Go de definir tipos compuestos. No hay clases. Los métodos son funciones con un receptor explícito (value o pointer). El embedding permite componer structs dentro de otros, promocionando sus campos y métodos. Es como una mochila modular: en vez de heredar una mochila entera, acoplas los compartimentos que necesitas.

🔗 Go dev — Structs

Sintaxis básica

type Direccion struct {
    Calle   string
    Ciudad string
    CP      string
}

type Usuario struct {
    Nombre    string
    Email     string
    Edad      int
    Direccion Direccion // composición: Usuario "tiene una" Direccion
    activo    bool      // campo privado al paquete
}

Métodos — funciones con receptor (receiver):

// Receptor por valor
func (u Usuario) Saludar() string {
    return "Hola, soy " + u.Nombre
}

// Receptor por puntero — puede modificar el struct
func (u *Usuario) Activar() {
    u.activo = true
}

// Convención: si un método tiene receptor puntero, todos deben tenerlo
func (u *Usuario) CambiarEmail(email string) {
    u.Email = email
}

Embedding — composición sobre herencia:

type Animal struct {
    Nombre string
}

func (a Animal) Sonido() string {
    return "..."
}

// Perro "extiende" Animal via embedding
type Perro struct {
    Animal          // embedding (sin nombre de campo)
    Raza string
}

// Override: Perro sobreescribe Sonido
func (p Perro) Sonido() string {
    return "Guau"
}

// Uso
p := Perro{Animal: Animal{Nombre: "Max"}, Raza: "Labrador"}
fmt.Println(p.Nombre)    // "Max" — campo promovido desde Animal
fmt.Println(p.Sonido())  // "Guau" — propio
fmt.Println(p.Animal.Sonido()) // "..." — acceso directo al embedido

El embedding NO es herencia. No hay polimorfismo de subtipos. Perro no es un Animal. Pero los métodos y campos del struct embebido se “promocionan” al struct contenedor. Es composición con sintaxis conveniente.

Tags de struct (metadata para serialización/validación):

type Usuario struct {
    ID        int       `json:"id" db:"id" validate:"required"`
    Nombre    string    `json:"nombre" validate:"min=2,max=100"`
    Email     string    `json:"email" validate:"email"`
    CreatedAt time.Time `json:"created_at" db:"created_at"`
}

Las tags son strings que las librerías interpretan via reflect. encoding/json, database/sql, validator las usan.

🧪 Cómo probarlo

func TestMetodoValorVsPuntero(t *testing.T) {
    u := Usuario{Nombre: "Ana"}
    if u.Saludar() != "Hola, soy Ana" {
        t.Errorf("saludo incorrecto: %s", u.Saludar())
    }
    u.Activar()
    // activo es privado — no podemos acceder directamente
}

func TestEmbedding(t *testing.T) {
    p := Perro{Animal: Animal{Nombre: "Max"}, Raza: "Labrador"}
    if p.Nombre != "Max" { t.Errorf("esperado Max, got %s", p.Nombre) }
    if p.Sonido() != "Guau" { t.Errorf("esperado Guau, got %s", p.Sonido()) }
}

💡 Memoria y rendimiento

  • Structs son value types — se copian al asignar o pasar por parámetro
  • Métodos con value receiver reciben una copia — seguros pero más caros para structs grandes
  • Métodos con pointer receiver reciben el original — eficientes pero permiten mutación
  • Embedding no añade overhead — es solo promoción de campos en compilación
  • Tags son strings guardadas como metadata en tiempo de compilación

✅ Buenas prácticas

  • ✅ Usa pointer receiver si el método modifica el struct
  • ✅ Usa value receiver si el método no modifica y el struct es pequeño (&lt;64 bytes)
  • ⚠️ Embedding no es herencia — no esperes polimorfismo de subtipos
  • ❌ No abuses de tags para lógica de negocio — son metadata, no validadores

🏗️ Metodología

  • DTOs: structs con tags json:"..." para serialización
  • Entidades: structs con métodos de negocio (pointer receiver)
  • Composición: embedding para reutilizar comportamiento
  • Configuración: structs con tags yaml/env para configuración

🔗 Para saber más

Interfaces (duck typing estructural)

¿Qué es?

Las interfaces en Go son implícitas y estructurales: no necesitas declarar que implementas una — si tienes los métodos, la implementas. Es como un enchufe universal: cualquier cable que tenga la clavija adecuada funciona, sin importar la marca.

🔗 Go dev — Interfaces

Sintaxis básica

// Definición de interfaz
type Saludador interface {
    Saludar() string
}

// Perro implementa Saludador implícitamente
type Perro struct{ Nombre string }
func (p Perro) Saludar() string {
    return "Guau, soy " + p.Nombre
}

// Gato también
type Gato struct{ Nombre string }
func (g Gato) Saludar() string {
    return "Miau, soy " + g.Nombre
}

// Función polimórfica
func Festejar(s Saludador) {
    fmt.Println(s.Saludar() + "! 🎉")
}

// Uso
Festejar(Perro{Nombre: "Max"})  // Guau, soy Max! 🎉
Festejar(Gato{Nombre: "Luna"})  // Miau, soy Luna! 🎉

Interfaz vacía (interface{} y any):

// Desde Go 1.18, 'any' es un alias de interface{}
var x any = 42
x = "hola"
x = true
x = []int{1, 2, 3}

any es útil cuando realmente necesitas aceptar cualquier tipo, pero pierdes seguridad de tipos. Prefiere interfaces específicas.

Type assertions:

var val any = "texto"

s, ok := val.(string)     // ok=true, s="texto"
n, ok := val.(int)        // ok=false, n=0 (no panic)
// n := val.(int)         // ❌ panic si no es int

// Sin el ok, si falla → panic

Type switch:

func describir(v any) string {
    switch v.(type) {
    case string:
        return "Cadena de longitud " + strconv.Itoa(len(v.(string)))
    case int:
        return "Entero: " + strconv.Itoa(v.(int))
    case bool:
        if v.(bool) {
            return "Verdadero"
        }
        return "Falso"
    case []int:
        return fmt.Sprintf("Slice de %d enteros", len(v.([]int)))
    default:
        return "Tipo desconocido"
    }
}

Interfaces pequeñas son la norma en Go:

  • io.Reader: Read(p []byte) (n int, err error)
  • io.Writer: Write(p []byte) (n int, err error)
  • fmt.Stringer: String() string
  • error: Error() string

La filosofía Go es: “cuanto más pequeña la interfaz, mejor”. Una interfaz de un solo método es idiomática.

🧪 Cómo probarlo

func TestInterfazImplicita(t *testing.T) {
    p := Perro{Nombre: "Max"}
    g := Gato{Nombre: "Luna"}

    var s Saludador
    s = p
    if s.Saludar() != "Guau, soy Max" {
        t.Errorf("esperado Guau, got %s", s.Saludar())
    }
    s = g
    if s.Saludar() != "Miau, soy Luna" {
        t.Errorf("esperado Miau, got %s", s.Saludar())
    }
}

func TestTypeAssertion(t *testing.T) {
    var val any = "texto"
    s, ok := val.(string)
    if !ok || s != "texto" {
        t.Errorf("type assertion falló")
    }
    _, ok = val.(int)
    if ok {
        t.Errorf("no debería ser int")
    }
}

💡 Memoria y rendimiento

  • Las interfaces se representan como un itable (tabla de métodos) + puntero al valor — 16 bytes
  • El type assertion hace una comparación de itable en tiempo constante
  • interface{} / any tiene el mismo costo que cualquier otra interfaz
  • Las type assertions con ok evitan panics — no hay overhead extra

✅ Buenas prácticas

  • ✅ Define interfaces pequeñas (1-3 métodos) — es idiomático en Go
  • ✅ Acepta interfaces, devuelve structs (conocido como “Go proverb”)
  • ✅ Usa any para valores genéricos no tipados (JSON, logging)
  • ⚠️ No declares interfaces en el paquete que las consume — decláralas donde se usan
  • ⚠️ Type assertion sin ok puede panic — siempre usa la forma segura
  • ❌ No crees interfaces antes de necesitarlas (YAGNI)

🏗️ Metodología

  • Contratos: las interfaces definen comportamiento, no datos
  • Mocking: interfaces pequeñas son fáciles de mockear en tests
  • Composición: io.ReadWriter combina io.Reader + io.Writer
  • Dependency injection: las funciones aceptan interfaces, no implementaciones concretas

🔗 Para saber más

Genéricos (Go 1.18+)

¿Qué es?

Los genéricos permiten escribir funciones y tipos parametrizados por tipos. Llegaron en Go 1.18 (marzo 2022). Son como una máquina de hacer moldes: en vez de fabricar una función para cada tipo, creas un molde ajustable.

🔗 Go dev — Generics tutorial

Sintaxis básica

// Función genérica
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Uso
fmt.Println(Min[int](5, 3))     // 3 (explícito)
fmt.Println(Min(5, 3))          // 3 (inferido)
fmt.Println(Min("a", "b"))      // "a" (también funciona)

Constraints (restringen qué tipos se aceptan):

// Constraint personalizada
type Numeric interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
        ~float32 | ~float64
}

func Suma[N Numeric](vals ...N) N {
    var total N
    for _, v := range vals {
        total += v
    }
    return total
}

// ~int significa "cualquier tipo cuyo tipo subyacente sea int"
type MiInt int
Suma(MiInt(1), MiInt(2))  // ✅ MiInt tiene ~int

Constraints predefinidas en golang.org/x/exp/constraints:

import "golang.org/x/exp/constraints"

// constraints.Ordered → tipos comparables con <, >, <=, >=
// constraints.Signed → int, int8, ..., int64
// constraints.Unsigned → uint, uint8, ..., uint64
// constraints.Integer → Signed + Unsigned
// constraints.Float → float32, float64
// constraints.Complex → complex64, complex128

comparable — constraint para tipos comparables:

func Contains[T comparable](slice []T, val T) bool {
    for _, v := range slice {
        if v == val {
            return true
        }
    }
    return false
}

Estructuras genéricas:

type Stack[T any] struct {
    items []T
}

func (s *Stack[T]) Push(item T) {
    s.items = append(s.items, item)
}

func (s *Stack[T]) Pop() (T, bool) {
    if len(s.items) == 0 {
        var zero T
        return zero, false
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Uso
s := Stack[string]{}
s.Push("hola")
s.Push("mundo")
val, ok := s.Pop()  // val="mundo", ok=true

Limitaciones de los genéricos en Go:

  1. No puedes usar operadores en tipos genéricos sin constraints (excepto == y != con comparable)
  2. No puedes hacer switch v.(type) directamente (necesitas reflexión)
  3. Type inference no siempre funciona con tipos complejos
  4. El rendimiento es bueno, pero no igual que monomorfización manual

🧪 Cómo probarlo

func TestGenericoMin(t *testing.T) {
    if Min(5, 3) != 3 { t.Error("Min(5,3) debería ser 3") }
    if Min("a", "b") != "a" { t.Error(`Min("a","b") debería ser "a"`) }
}

func TestStack(t *testing.T) {
    s := Stack[int]{}
    s.Push(1)
    s.Push(2)
    val, ok := s.Pop()
    if !ok || val != 2 { t.Errorf("Pop esperado 2, got %d", val) }
    val, ok = s.Pop()
    if !ok || val != 1 { t.Errorf("Pop esperado 1, got %d", val) }
    _, ok = s.Pop()
    if ok { t.Error("Pop de stack vacío debería dar false") }
}

func TestContains(t *testing.T) {
    nums := []int{1, 2, 3, 4, 5}
    if !Contains(nums, 3) { t.Error("debería contener 3") }
    if Contains(nums, 99) { t.Error("no debería contener 99") }
}

💡 Memoria y rendimiento

  • Go compila una copia especializada por cada conjunto único de tipos (monomorfización)
  • Las funciones genéricas pueden ser más lentas en compilación, pero en runtime son casi idénticas a versiones manuales
  • any en genéricos evita boxing (a diferencia de interface{})
  • Las constraints se verifican en compilación, zero overhead en runtime
  • El tamaño del binario crece con cada especialización única

✅ Buenas prácticas

  • ✅ Usa comparable para funciones que necesitan == / !=
  • ✅ Usa any como el tipo más permisivo
  • ✅ Prefiere interfaces específicas a genéricos en APIs públicas simples
  • ⚠️ No uses genéricos donde interfaces + type switch son más claros
  • ⚠️ Los genéricos no son para todo — YAGNI también aplica aquí
  • ❌ No abuses de genéricos anidados — el código se vuelve ilegible

🏗️ Metodología

  • Estructuras de datos: Stack[T], Queue[T], Tree[T] con genéricos
  • Algoritmos: funciones de ordenación/búsqueda genéricas
  • Helpers: Contains, Map, Filter, Reduce (cuando no uses slices / maps)
  • APIs: genéricos para mantener tipado fuerte sin duplicar código

🔗 Para saber más

Punteros

¿Qué es?

Un puntero almacena la dirección de memoria de un valor. Go tiene punteros pero no aritmética de punteros (a diferencia de C). Es como una nota adhesiva que te dice dónde está el objeto, no el objeto en sí.

🔗 Go dev — Pointer types

Sintaxis básica

var x int = 42
var p *int = &x  // p es un puntero a int, &x obtiene la dirección

fmt.Println(*p)  // 42 — desreferenciación
*p = 100         // Modifica x a través del puntero
fmt.Println(x)   // 100

¿Cuándo usar punteros vs valores?

EscenarioRecomendación
Struct grande (>64 bytes)Puntero — evita copiar
Método que modifica el receptorPuntero — func (u *Usuario) SetNombre()
Campo nil permitidoPuntero — indica ausencia
Canal, slice, mapSiempre referencia (ya son punteros internamente)
Enteros, booleanos, flagsValor — los punteros añaden overhead
StringsValor — son inmutables y ligeras

Escape analysis (compilador decide heap vs stack):

El compilador de Go decide automáticamente si una variable va al stack o al heap. No necesitas new vs malloc — Go lo hace por ti.

func crearUsuario() *Usuario {
    u := Usuario{Nombre: "Ana"} // ¿stack o heap?
    return &u                    // ❗ Escapa al heap — se usa fuera de la función
}

func procesar() {
    var u Usuario               // Stack — no escapa
    u.Nombre = "Ana"
    _ = u.Nombre
}

Puedes ver las decisiones del compilador:

go build -gcflags='-m' ./...

Si el compilador ve que una variable debe sobrevivir a la función que la crea, la asigna al heap. Si no, la asigna al stack — mucho más rápido.

🧪 Cómo probarlo

func TestPunteros(t *testing.T) {
    x := 42
    p := &x
    if *p != 42 { t.Errorf("esperado 42, got %d", *p) }
    *p = 100
    if x != 100 { t.Errorf("esperado 100, got %d", x) }
}

func TestEscapeAnalysis(t *testing.T) {
    u := crearUsuario()
    if u.Nombre != "Ana" { t.Errorf("esperado Ana, got %s", u.Nombre) }
    // crearUsuario aloca en heap porque devuelve puntero
}

💡 Memoria y rendimiento

  • Los punteros ocupan 8 bytes en x86-64 (una dirección de memoria)
  • Stack es rápido (asignación barata); heap requiere GC
  • Escape analysis decide automáticamente stack vs heap
  • go build -gcflags='-m' para inspeccionar decisiones
  • Pasar structs grandes por puntero es más barato que copiarlos
  • Enteros pequeños por valor suelen ser más rápidos que por puntero

✅ Buenas prácticas

  • ✅ Usa punteros para structs grandes y receptores mutables
  • ✅ Usa valores para enteros, booleanos, strings, y structs pequeños
  • ⚠️ No uses punteros para evitar copias prematuras — el compilador optimiza
  • ❌ No hay null pointer safety — siempre checkea nil antes de dereferenciar
  • ❌ No compartas punteros entre goroutines sin sincronización

🏗️ Metodología

  • Receptores: pointer receiver si mutas, value receiver si no
  • Campos opcionales: puntero indica nil = ausencia
  • Constructores: devuelven *T para indicar ownership
  • Caches y pools: punteros evitando copias innecesarias

🔗 Para saber más

Errores

¿Qué es?

Go trata los errores como valores, no como excepciones. No hay try/catch. Es como un semáforo: en vez de lanzar una sirena cuando algo falla, simplemente devuelves una señal (error) que quien te llama debe revisar.

🔗 Go dev — Errors (blog)

Sintaxis básica

// error es una interfaz predefinida
type error interface {
    Error() string
}

// Función que puede fallar — devuelve error
func Dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("división por cero")
    }
    return a / b, nil // nil = no error
}

// Uso idiomático
resultado, err := Dividir(10, 0)
if err != nil {
    fmt.Println("Error:", err)
    return
}
fmt.Println(resultado)

Wrapping errores (Go 1.13+):

import "errors"
import "fmt"

// Envolver un error con contexto
if err != nil {
    return fmt.Errorf("leer archivo %s: %w", filename, err)
}

// Desenvolver con errors.Is y errors.As
if errors.Is(err, os.ErrNotExist) {
    fmt.Println("El archivo no existe")
}

var pathError *os.PathError
if errors.As(err, &pathError) {
    fmt.Println("Path:", pathError.Path)
}

fmt.Errorf("%w") envuelve el error para que errors.Is y errors.As puedan atravesar la cadena. Usa %v en lugar de %w si no necesitas desenvolver.

errors.Join (Go 1.20+):

var errs error
for _, op := range operaciones {
    if err := op(); err != nil {
        errs = errors.Join(errs, err) // acumula errores
    }
}
return errs // devuelve todos los errores combinados

Errores personalizados:

type ErrorValidacion struct {
    Campo string
    Valor any
    Motivo string
}

func (e *ErrorValidacion) Error() string {
    return fmt.Sprintf("validación: %s con valor %v — %s", e.Campo, e.Valor, e.Motivo)
}

// Uso
return nil, &ErrorValidacion{Campo: "email", Valor: "", Motivo: "no puede estar vacío"}

Sentinel errors vs error types:

PatrónEjemploCuándo
Sentinelvar ErrNotFound = errors.New("not found")Error simple y conocido
Custom typetype ErrorValidacion structLleva datos adicionales
Opaque errorSolo err != nilAl llamante no le importa el detalle

🧪 Cómo probarlo

func TestErrorBasico(t *testing.T) {
    _, err := Dividir(10, 0)
    if err == nil { t.Fatal("debería haber error") }
    if err.Error() != "división por cero" {
        t.Errorf("mensaje incorrecto: %s", err.Error())
    }
    res, err := Dividir(10, 2)
    if err != nil { t.Fatal("no debería haber error") }
    if res != 5 { t.Errorf("esperado 5, got %f", res) }
}

func TestErrorWrapping(t *testing.T) {
    base := errors.New("error base")
    envuelto := fmt.Errorf("contexto: %w", base)
    if !errors.Is(envuelto, base) {
        t.Error("errors.Is debería encontrar el error envuelto")
    }
}

func TestErrorJoin(t *testing.T) {
    e1 := errors.New("error 1")
    e2 := errors.New("error 2")
    joined := errors.Join(e1, e2)
    if !errors.Is(joined, e1) || !errors.Is(joined, e2) {
        t.Error("errors.Join debería contener ambos errores")
    }
}

💡 Memoria y rendimiento

  • error es una interfaz de 16 bytes (itable + puntero)
  • errors.New aloca una vez — reutiliza la misma string
  • fmt.Errorf("%w") aloca un nuevo wrapping error cada vez
  • errors.Is y errors.As recorren la cadena — O(n) en número de wraps
  • errors.Join evita anidar múltiples niveles — eficiente para batches

✅ Buenas prácticas

  • ✅ Siempre checkea err != nil — es idiomático en Go
  • ✅ Usa %w para wrapping con intención de desenvolver después
  • ✅ Usa errors.Is para sentinel errors, errors.As para custom types
  • ⚠️ No uses _ = err para ignorar errores — al menos loguea
  • ❌ No uses panic como sustituto de errores — panics son para bugs, no para validación

🏗️ Metodología

  • APIs: devuelve (resultado, error) como último valor de retorno
  • Validación: errores personalizados con datos estructurados
  • Fallos de sistema: errors.New para errores simples y conocidos
  • Capas: cada capa añade contexto con %w sin duplicar información

🔗 Para saber más

Defer, panic y recover

¿Qué es?

defer programa una función para ejecutarse al salir del ámbito actual (LIFO). panic detiene el flujo normal (error irrecuperable). recover captura un panic dentro de un defer. Es como una red de seguridad: el defer es el arnés que siempre se activa al saltar, el panic es la alarma, y recover la red que atrapa la caída.

🔗 Go dev — Defer

Sintaxis básica

defer — ejecuta una función al salir del ámbito actual:

func leerArchivo(nombre string) error {
    f, err := os.Open(nombre)
    if err != nil {
        return err
    }
    defer f.Close() // se ejecuta cuando leerArchivo termina

    // procesar archivo...
    return nil
}

Los defer se ejecutan en orden LIFO (last in, first out):

func ejemplo() {
    defer fmt.Println("tercero")
    defer fmt.Println("segundo")
    fmt.Println("primero")
}
// Output:
// primero
// segundo
// tercero

panic — error irrecuperable (equivalente a lanzar excepción):

func init() {
    if config == nil {
        panic("config no inicializada — el programa no puede continuar")
    }
}

Usa panic solo para casos que no deberían ocurrir (bugs, estado imposible). No uses panic para errores normales — para eso está error.

recover — captura un panic (como un catch):

func proteger() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recuperado de panic:", r)
            // Log, cleanup, etc.
        }
    }()
    // código que podría paniquear
    panic("algo salió mal")
}

recover solo funciona dentro de un defer. Si llamas recover fuera de un defer, devuelve nil.

Cuándo usar panic/recover:

  • Panic: inicialización que falla, errores de programación (nil dereference), casos imposibles
  • Error: todo lo demás (IO, validación, red, BD)
  • Recover: en el borde del programa (middleware HTTP, worker supervisor, entrada de goroutine)
// Middleware HTTP que captura panics
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if r := recover(); r != nil {
                log.Printf("panic capturado: %v", r)
                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}

🧪 Cómo probarlo

func TestDeferLIFO(t *testing.T) {
    var orden []string
    func() {
        defer func() { orden = append(orden, "segundo") }()
        defer func() { orden = append(orden, "primero") }()
    }()
    if len(orden) != 2 || orden[0] != "primero" || orden[1] != "segundo" {
        t.Errorf("orden LIFO incorrecto: %v", orden)
    }
}

func TestRecoverPanic(t *testing.T) {
    func() {
        defer func() {
            if r := recover(); r == nil {
                t.Error("debería haber capturado el panic")
            }
        }()
        panic("test panic")
    }()
}

💡 Memoria y rendimiento

  • defer tiene overhead pequeño (~50ns por llamada en Go 1.14+)
  • defer en un closure captura variables por referencia — cuidado con bucles
  • panic/recover tienen costo alto (~1µs) — no uses para control de flujo
  • Los defer se evalúan en el momento del registro (argumentos se copian), no al ejecutarse

✅ Buenas prácticas

  • ✅ Usa defer para cleanup (cerrar archivos, liberar mutexes, cerrar conexiones)
  • ✅ Usa panic solo para bugs o estados imposibles
  • ✅ Usa recover en el borde del programa (middleware HTTP, goroutines main)
  • ⚠️ No uses panic para errores esperables (IO, validación, red)
  • ❌ No uses recover para control de flujo normal

🏗️ Metodología

  • Cleanup: defer f.Close() justo después de os.Open
  • Middleware HTTP: recover en el handler más externo para evitar crashes
  • Workers: recover en cada goroutine para aislar fallos
  • Inicialización: panic si el programa no puede continuar sin configuración válida

🔗 Para saber más

Goroutines y canales

¿Qué es?

Las goroutines son hilos ligeros gestionados por el runtime de Go (~4KB de stack vs ~1MB de thread SO). Los canales son tuberías tipadas para comunicación entre goroutines. Es como tener 1000 mensajeros (goroutines) que se pasan notas (canales) sin pisarse.

🔗 Go dev — Goroutines

Sintaxis básica

func decir(texto string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(texto)
    }
}

func main() {
    go decir("hola")   // goroutine — se ejecuta concurrentemente
    go decir("mundo")  // otra goroutine
    time.Sleep(1 * time.Second) // espera a que terminen (mala práctica)
}

Canales — tuberías tipadas para comunicación entre goroutines:

// Crear canal
ch := make(chan int)       // unbuffered — bloquea hasta que alguien recibe
buf := make(chan int, 10)  // buffered — buffer de 10 elementos

// Enviar
ch <- 42

// Recibir
valor := <-ch
valor, ok := <-ch  // ok=false si el canal está cerrado

// Cerrar canal
close(ch)

Canales unbuffered (sincrónicos):

func main() {
    ch := make(chan string)

    // Goroutine que envía
    go func() {
        fmt.Println("Goroutine: enviando...")
        ch <- "mensaje"  // Bloquea hasta que main recibe
        fmt.Println("Goroutine: enviado!")
    }()

    time.Sleep(500 * time.Millisecond)
    fmt.Println("Main: recibiendo...")
    msg := <-ch          // Bloquea hasta que la goroutine envía
    fmt.Println("Main:", msg)
}

Canales buffered (asíncronos):

ch := make(chan int, 3)  // buffer de 3

ch <- 1  // No bloquea (buffer no está lleno)
ch <- 2  // No bloquea
ch <- 3  // No bloquea
// ch <- 4 // ❌ Bloquearía — buffer lleno

fmt.Println(<-ch)  // 1
fmt.Println(<-ch)  // 2
fmt.Println(<-ch)  // 3

range sobre canales:

func main() {
    ch := make(chan string)

    go func() {
        defer close(ch) // importante: cerrar el canal
        for _, m := range []string{"a", "b", "c"} {
            ch <- m
        }
    }()

    for msg := range ch { // itera hasta que el canal se cierra
        fmt.Println(msg)
    }
}

select — espera múltiples canales (como switch pero para canales):

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "uno"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "dos"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("Recibido de ch1:", msg)
    case msg := <-ch2:
        fmt.Println("Recibido de ch2:", msg)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("Timeout") // si ninguno responde
    default:
        fmt.Println("Ningún canal listo") // select no bloqueante
    }
}

Patrón fan-out/fan-in (distribuir trabajo):

// Fan-out: múltiples workers leen del mismo canal
func worker(id int, jobs <-chan int, results chan<- int) {
    for job := range jobs {
        results <- job * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Lanzar 3 workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Enviar 9 jobs
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // Recibir resultados
    for r := 1; r <= 9; r++ {
        <-results
    }
}

🧪 Cómo probarlo

func TestCanalUnbuffered(t *testing.T) {
    ch := make(chan string)
    go func() {
        ch <- "hola"
    }()
    msg := <-ch
    if msg != "hola" { t.Errorf("esperado hola, got %s", msg) }
}

func TestCanalBuffered(t *testing.T) {
    ch := make(chan int, 3)
    ch <- 1; ch <- 2; ch <- 3
    if <-ch != 1 { t.Error("esperado 1") }
    if <-ch != 2 { t.Error("esperado 2") }
    if <-ch != 3 { t.Error("esperado 3") }
}

func TestCanalCerrado(t *testing.T) {
    ch := make(chan int)
    close(ch)
    val, ok := <-ch
    if ok { t.Error("canal cerrado debería dar ok=false") }
    if val != 0 { t.Error("canal cerrado debería dar zero value") }
}

func TestSelect(t *testing.T) {
    ch := make(chan string)
    go func() {
        ch <- "mensaje"
    }()
    select {
    case msg := <-ch:
        if msg != "mensaje" { t.Errorf("esperado mensaje, got %s", msg) }
    case <-time.After(time.Second):
        t.Error("timeout")
    }
}

💡 Memoria y rendimiento

  • Las goroutines empiezan con ~4KB de stack, crecen y se reutilizan
  • El scheduler M:N mapea N goroutines a M OS threads (GOMAXPROCS)
  • Canales unbuffered sincronizan --- buffered desacoplan
  • select con default es no bloqueante
  • Cerrar un canal libera a los receivers bloqueados en range
  • Los canales tienen overhead pequeño --- pero enviar valores grandes por puntero es más rápido

✅ Buenas prácticas

  • ✅ Usa go func() en main para tareas concurrentes
  • ✅ Siempre cierra los canales desde el emisor, no desde el receptor
  • ✅ Usa range para consumir canales hasta que se cierren
  • ✅ Usa select con time.After para timeouts
  • ⚠️ No uses time.Sleep para sincronizar --- usa canales o sync.WaitGroup
  • ⚠️ Las goroutines filtradas (leak) no se recolectan --- asegúrate de que terminen
  • ❌ No compartas memoria entre goroutines --- usa canales para comunicar

🏗️ Metodología

  • Pipeline: generator → worker → collector con canales
  • Workers: fan-out con workers fijos, fan-in con resultados
  • Timeouts: select con time.After en operaciones de red
  • Señales: canal chan struct{} como señal ligera (zero bytes)
  • Tareas programadas: time.Ticker + select

🔗 Para saber más

Context

¿Qué es?

context.Context lleva deadlines, cancelaciones y valores a través de la cadena de llamadas. Es el mecanismo estándar para controlar el ciclo de vida de operaciones concurrentes. Es como un walkie-talkie: desde la base (main) le dices a cada explorador (goroutine) “vuelve en 5 segundos” o “cancelado, todos a casa”.

🔗 Go dev — Context (blog)

Sintaxis básica

import "context"

// Función que acepta context y respeta cancelación
func operacionLarga(ctx context.Context) error {
    select {
    case <-time.After(2 * time.Second):
        return nil
    case <-ctx.Done():
        return ctx.Err() // context.Canceled o context.DeadlineExceeded
    }
}

Contextos padre/hijo:

// Con cancelación manual
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // importante: siempre llama a cancel para liberar recursos

go func() {
    time.Sleep(100 * time.Millisecond)
    cancel() // cancela todas las operaciones que usen ctx
}()

err := operacionLarga(ctx)

// Con timeout automático
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// Con deadline absoluto
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

// Con valores (tipo map, scope limitado al contexto)
ctx = context.WithValue(ctx, "requestID", "abc-123")
reqID := ctx.Value("requestID").(string) // type assertion necesaria

Buenas prácticas con context:

  1. El context es siempre el primer parámetro de una función
  2. Nunca almacenes un context en un struct — pásalo como argumento
  3. Siempre llama a cancel() cuando crees un context cancelable
  4. Usa context.Background() para el root, context.TODO() cuando aún no sabes qué usar
  5. No uses context.WithValue para cosas que deberían ser parámetros
func Handler(ctx context.Context, req *Request) (*Response, error) {
    ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
    defer cancel()

    data, err := fetchFromDB(ctx, req.ID)
    if err != nil {
        return nil, err
    }
    return &Response{Data: data}, nil
}

🧪 Cómo probarlo

func TestContextCancel(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    cancel() // cancelación inmediata

    select {
    case <-ctx.Done():
        if ctx.Err() != context.Canceled {
            t.Errorf("esperado Canceled, got %v", ctx.Err())
        }
    case <-time.After(time.Second):
        t.Error("timeout")
    }
}

func TestContextTimeout(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
    defer cancel()

    <-ctx.Done()
    if ctx.Err() != context.DeadlineExceeded {
        t.Errorf("esperado DeadlineExceeded, got %v", ctx.Err())
    }
}

func TestContextValue(t *testing.T) {
    ctx := context.WithValue(context.Background(), "key", "value")
    if v := ctx.Value("key").(string); v != "value" {
        t.Errorf("esperado value, got %s", v)
    }
}

💡 Memoria y rendimiento

  • context.Background() y context.TODO() son singletons — zero alloc
  • WithCancel/WithTimeout alocan un nuevo contexto cada vez
  • WithValue usa una linked list interna — O(n) en búsqueda
  • ctx.Done() devuelve el mismo canal cada vez — no aloca
  • Llamar cancel() más de una vez es seguro (no-op en la segunda)

✅ Buenas prácticas

  • ✅ El context es siempre el primer parámetro — convención del lenguaje
  • ✅ Siempre llama a cancel() con defer para evitar leaks
  • ✅ Usa context.WithTimeout para operaciones con límite de tiempo
  • ⚠️ No almacenes context en structs — pásalo como argumento
  • ⚠️ No uses WithValue como sustituto de parámetros normales
  • ❌ No pases context.Background() a funciones — pasa el context que recibes

🏗️ Metodología

  • HTTP handlers: context.WithTimeout(ctx, 10s) para cada request
  • DB queries: pasa context a db.QueryContext(ctx, ...)
  • Goroutines: pasa context a cada goroutine lanzada
  • Propagación: extiende deadlines, nunca los ignores
  • Cleanup: cancel() libera resources del contexto padre

🔗 Para saber más

Sincronización (sync)

¿Qué es?

El paquete sync proporciona primitivas de sincronización básicas: WaitGroup para esperar goroutines, Mutex/RWMutex para exclusión mutua, Once para inicialización única, y Map para maps concurrentes. Es como un semáforo en una intersección: coordina quién pasa y quién espera.

🔗 Go dev — sync

Sintaxis básica

sync.WaitGroup — esperar a que un grupo de goroutines termine:

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1) // incrementa contador
        go func(id int) {
            defer wg.Done() // decrementa contador
            fmt.Printf("Worker %d terminado\n", id)
        }(i)
    }

    wg.Wait() // bloquea hasta que contador llegue a 0
    fmt.Println("Todos los workers terminaron")
}

sync.Mutex — exclusión mutua:

type Contador struct {
    mu    sync.Mutex
    valor int
}

func (c *Contador) Incrementar() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.valor++
}

func (c *Contador) Valor() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.valor
}

sync.RWMutex — lectores concurrentes, escritor exclusivo:

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock() // múltiples lecturas concurrentes
    defer c.mu.RUnlock()
    v, ok := c.data[key]
    return v, ok
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock() // escritura exclusiva
    defer c.mu.Unlock()
    c.data[key] = value
}

sync.Once — ejecuta algo exactamente una vez (singletons, inicialización):

var (
    config   *Config
    configMu sync.Once
)

func GetConfig() *Config {
    configMu.Do(func() {
        fmt.Println("Inicializando configuración (solo una vez)")
        config = &Config{
            DBUrl:  os.Getenv("DB_URL"),
            Puerto: 8080,
        }
    })
    return config
}

sync.Map — map concurrente optimizado (cuando las claves se escriben una vez y se leen muchas):

var m sync.Map

m.Store("key", "value")
val, ok := m.Load("key")
m.LoadOrStore("key", "default") // si existe, devuelve el existente
m.Delete("key")

m.Range(func(key, value any) bool {
    fmt.Println(key, value)
    return true // continuar iterando
})

⚠️ sync.Map está optimizado para patrones específicos (escritura una vez, lectura frecuente). Para la mayoría de casos, un map con sync.RWMutex es mejor opción.

🧪 Cómo probarlo

func TestWaitGroup(t *testing.T) {
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
        }()
    }
    wg.Wait()
    // si llegamos aquí sin deadlock, el test pasa
}

func TestMutex(t *testing.T) {
    c := Contador{}
    var wg sync.WaitGroup
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            c.Incrementar()
        }()
    }
    wg.Wait()
    if c.Valor() != 100 { t.Errorf("esperado 100, got %d", c.Valor()) }
}

func TestOnce(t *testing.T) {
    var count int
    var o sync.Once
    for i := 0; i < 10; i++ {
        o.Do(func() { count++ })
    }
    if count != 1 { t.Errorf("Once debería ejecutarse 1 vez, count=%d", count) }
}

💡 Memoria y rendimiento

  • sync.Mutex es ligero (~8 bytes), sync.RWMutex un poco más
  • Mutex.Lock() con contención hace system calls — caro pero necesario
  • sync.Once usa atomic operations internamente — overhead mínimo
  • sync.WaitGroup usa atomics + futex — eficiente para cientos de goroutines
  • sync.Map tiene overhead interno pero brilla en lecturas sin contención
  • Siempre usa defer para unlock — previene deadlocks si hay panic

✅ Buenas prácticas

  • ✅ Usa defer c.mu.Unlock() justo después de Lock()
  • ✅ Prefiere sync.RWMutex cuando hay más lecturas que escrituras
  • ✅ Usa sync.Once para inicialización perezosa
  • ⚠️ No copies un sync.Mutex — pasa siempre por puntero
  • ⚠️ No uses sync.Map para casos generales — map + RWMutex suele ser mejor
  • ❌ No mezcles canales y mutexes en la misma goroutine — difícil de razonar

🏗️ Metodología

  • WaitGroup: esperar que N goroutines terminen
  • Mutex: proteger estado compartido con acceso concurrente
  • RWMutex: caches, configs (más lecturas que escrituras)
  • Once: singletons, inicialización perezosa
  • Pool: sync.Pool para reutilizar objetos alocados frecuentemente

🔗 Para saber más

io.Reader y io.Writer

¿Qué es?

io.Reader y io.Writer son las interfaces más ubicuas de Go después de error. Representan fuentes y destinos de datos en streaming. Es como una manguera: por un lado entra agua (Reader), por el otro sale (Writer), y puedes conectar mangueras entre sí.

🔗 Go dev — io

Sintaxis básica

type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

Composición de readers/writers:

// Leer todo de un reader
data, _ := io.ReadAll(reader)

// Copiar de reader a writer
io.Copy(writer, reader)

// TeeReader: escribe lo que lee a otro writer (como `tee` en Unix)
io.Copy(os.Stdout, io.TeeReader(file, os.Stderr))

// LimitedReader: lee solo N bytes
limited := io.LimitReader(reader, 1024)

// MultiReader: concatenar múltiples readers
combined := io.MultiReader(file1, file2)

// MultiWriter: escribir a múltiples writers a la vez
writers := io.MultiWriter(file, os.Stdout)

// PipeReader/PipeWriter: conexión síncrona entre goroutines
pr, pw := io.Pipe()

Ejemplo práctico — servidor de archivos con io.Copy:

func serveFile(w http.ResponseWriter, path string) {
    f, err := os.Open(path)
    if err != nil {
        http.Error(w, err.Error(), http.StatusNotFound)
        return
    }
    defer f.Close()

    // Detecta Content-Type automáticamente
    buf := make([]byte, 512)
    f.Read(buf)
    w.Header().Set("Content-Type", http.DetectContentType(buf))

    f.Seek(0, io.SeekStart) // vuelve al principio
    io.Copy(w, f)           // copia eficiente del archivo a la response
}

🧪 Cómo probarlo

func TestReaderWriter(t *testing.T) {
    var buf bytes.Buffer
    writer := &buf
    n, err := writer.Write([]byte("hola"))
    if err != nil { t.Fatal(err) }
    if n != 4 { t.Errorf("esperado 4 bytes escritos, got %d", n) }

    reader := &buf
    data, err := io.ReadAll(reader)
    if err != nil { t.Fatal(err) }
    if string(data) != "hola" { t.Errorf("esperado hola, got %s", string(data)) }
}

func TestCopy(t *testing.T) {
    var src bytes.Buffer
    var dst bytes.Buffer
    src.WriteString("test data")
    n, err := io.Copy(&dst, &src)
    if err != nil { t.Fatal(err) }
    if n != 9 { t.Errorf("esperado 9 bytes copiados, got %d", n) }
    if dst.String() != "test data" { t.Errorf("esperado 'test data', got '%s'", dst.String()) }
}

func TestLimitedReader(t *testing.T) {
    src := strings.NewReader("contenido largo")
    limited := io.LimitReader(src, 5)
    data, _ := io.ReadAll(limited)
    if string(data) != "conte" { t.Errorf("esperado 'conte', got '%s'", string(data)) }
}

💡 Memoria y rendimiento

  • io.Copy usa un buffer interno de 32KB — copia eficiente entre streams
  • bytes.Buffer crece dinámicamente como un slice de bytes
  • strings.NewReader no aloca — es un wrapper sobre un string existente
  • io.Pipe es síncrono — bloquea hasta que el otro lado lee/escribe
  • bufio.Reader/bufio.Writer añaden buffering para reducir system calls

✅ Buenas prácticas

  • ✅ Usa io.Copy en vez de leer todo a memoria y luego escribir
  • ✅ Usa io.ReadAll solo cuando necesitas todo el contenido en memoria
  • ✅ Usa io.LimitReader para prevenir lecturas ilimitadas de fuentes no confiables
  • ⚠️ Siempre verifica el error de Read (especialmente io.EOF)
  • ❗ Si Read devuelve n > 0 con err == io.EOF, los n bytes son válidos

🏗️ Metodología

  • Streaming: io.Copy para transferencias grandes entre reader y writer
  • Middleware: io.TeeReader para loggear mientras se procesa
  • Testing: bytes.Buffer como writer/reader en tests
  • HTTP: http.ResponseWriter es un io.Writer, r.Body es un io.Reader
  • Compresión: gzip.Reader/gzip.Writer envuelven readers/writers existentes

🔗 Para saber más

encoding/json

¿Qué es?

encoding/json serializa (Marshal) y deserializa (Unmarshal) datos entre Go structs y JSON. Usa struct tags para mapear campos. Es como un traductor simultáneo: conviertes structs Go ↔ JSON sin esfuerzo manual.

🔗 Go dev — encoding/json

Sintaxis básica

type Usuario struct {
    ID        int       `json:"id"`
    Nombre    string    `json:"nombre"`
    Email     string    `json:"email,omitempty"` // omitir si está vacío
    CreatedAt time.Time `json:"created_at"`
    Password  string    `json:"-"` // nunca se serializa
}

Marshaling (struct → JSON):

u := Usuario{ID: 1, Nombre: "Ana", Email: "ana@email.com", CreatedAt: time.Now()}

bytes, err := json.Marshal(u)          // []byte
bytes, err := json.MarshalIndent(u, "", "  ") // pretty print

// Escribir directamente a un writer
json.NewEncoder(w).Encode(u)

Unmarshaling (JSON → struct):

jsonData := `{"id":1,"nombre":"Ana","email":"ana@email.com"}`
var u Usuario
err := json.Unmarshal([]byte(jsonData), &u)

// Leer desde un reader
json.NewDecoder(r).Decode(&u)

JSON dinámico (raw message + map):

var data map[string]any
json.Unmarshal(bytes, &data)
fmt.Println(data["nombre"]) // "Ana"

// RawMessage para decodificación parcial
type Flexible struct {
    Type string          `json:"type"`
    Data json.RawMessage `json:"data"` // decodifica después
}

JSON streams (NDJSON):

dec := json.NewDecoder(file)
for {
    var u Usuario
    if err := dec.Decode(&u); err == io.EOF {
        break
    } else if err != nil {
        log.Fatal(err)
    }
    fmt.Println(u.Nombre)
}

🧪 Cómo probarlo

func TestMarshalUnmarshal(t *testing.T) {
    u := Usuario{ID: 1, Nombre: "Ana"}
    data, err := json.Marshal(u)
    if err != nil { t.Fatal(err) }

    var u2 Usuario
    if err := json.Unmarshal(data, &u2); err != nil { t.Fatal(err) }
    if u2.Nombre != "Ana" { t.Errorf("esperado Ana, got %s", u2.Nombre) }
}

func TestOmitEmpty(t *testing.T) {
    u := Usuario{ID: 1, Nombre: "Ana"}
    data, _ := json.Marshal(u)
    if bytes.Contains(data, []byte("email")) {
        t.Error("email vacío no debería aparecer con omitempty")
    }
}

func TestJSONStream(t *testing.T) {
    var buf bytes.Buffer
    enc := json.NewEncoder(&buf)
    for i := 0; i < 3; i++ {
        enc.Encode(Usuario{ID: i, Nombre: "User"})
    }

    dec := json.NewDecoder(&buf)
    count := 0
    for {
        var u Usuario
        if err := dec.Decode(&u); err == io.EOF { break }
        count++
    }
    if count != 3 { t.Errorf("esperado 3, got %d", count) }
}

💡 Memoria y rendimiento

  • json.Marshal usa reflection — más lento que serialización manual pero más cómodo
  • json.Decoder es eficiente para streams (lee en chunks, no carga todo)
  • json.RawMessage evita re-serializar datos que ya están en JSON
  • json.Encoder/Decoder son más eficientes que Marshal/Unmarshal para streams grandes
  • Los struct tags son strings estáticas — sin overhead en runtime

✅ Buenas prácticas

  • ✅ Usa json:"campo" tags en todos los campos exportados
  • ✅ Usa json:"-" para campos que no deben serializarse (passwords, secrets)
  • ✅ Usa json:"campo,omitempty" para omitir valores zero
  • ✅ Usa json.NewDecoder/json.NewEncoder para streams y HTTP
  • ⚠️ No serialices campos con tipos no exportados (privados) — causan error
  • ❌ No confíes en el orden de keys en un map[string]any — no está garantizado

🏗️ Metodología

  • APIs REST: json.NewEncoder(w).Encode(resp) para responses
  • Request body: json.NewDecoder(r.Body).Decode(&req)
  • Config: json.MarshalIndent para archivos de configuración legibles
  • Flexible: json.RawMessage para estructuras con campos variables
  • Logging: marshal estructuras para logs estructurados

🔗 Para saber más

database/sql

¿Qué es?

database/sql es una interfaz genérica para bases de datos SQL. Incluye pooling de conexiones, prepared statements, y transacciones. Los drivers específicos (PostgreSQL, MySQL, SQLite) implementan la interfaz. Es como un conector universal: el estándar es el enchufe, cada base de datos es un adaptador diferente.

🔗 Go dev — database/sql

Sintaxis básica

import (
    "database/sql"
    _ "github.com/lib/pq" // driver PostgreSQL — el _ importa sin usar
)

// Abrir conexión (no conecta, solo prepara)
db, err := sql.Open("postgres", "postgres://user:pass@localhost/dbname?sslmode=disable")
if err != nil {
    log.Fatal(err)
}
defer db.Close()

// Configurar pool
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)

Operaciones básicas:

// Query (SELECT)
rows, err := db.Query("SELECT id, nombre FROM usuarios WHERE activo = $1", true)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
    var id int
    var nombre string
    if err := rows.Scan(&id, &nombre); err != nil {
        log.Fatal(err)
    }
    fmt.Println(id, nombre)
}
if err := rows.Err(); err != nil {
    log.Fatal(err)
}

// QueryRow (una fila)
var nombre string
err := db.QueryRow("SELECT nombre FROM usuarios WHERE id = $1", 1).Scan(&nombre)

// Exec (INSERT, UPDATE, DELETE)
result, err := db.Exec("INSERT INTO usuarios (nombre, email) VALUES ($1, $2)", "Ana", "ana@email.com")
id, _ := result.LastInsertId()    // no soportado por todos los drivers
n, _ := result.RowsAffected()     // filas afectadas

// Prepared statements
stmt, err := db.Prepare("INSERT INTO usuarios (nombre) VALUES ($1) RETURNING id")
defer stmt.Close()
var newID int
stmt.QueryRow("Pedro").Scan(&newID)

// Transacciones
tx, err := db.Begin()
tx.Exec("UPDATE cuentas SET saldo = saldo - $1 WHERE id = $2", 100, 1)
tx.Exec("UPDATE cuentas SET saldo = saldo + $1 WHERE id = $2", 100, 2)
tx.Commit() // o tx.Rollback()

Nullables:

var apellido sql.NullString  // String que puede ser NULL
if err := rows.Scan(&apellido); err != nil {
    log.Fatal(err)
}
if apellido.Valid {
    fmt.Println(apellido.String)
} else {
    fmt.Println("NULL")
}
// También: sql.NullInt64, sql.NullFloat64, sql.NullBool, sql.NullTime

🧪 Cómo probarlo

// Usando un driver SQLite en memoria para tests sin infraestructura
func TestDB(t *testing.T) {
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil { t.Fatal(err) }
    defer db.Close()

    db.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)")
    db.Exec("INSERT INTO test (name) VALUES (?)", "Ana")

    var name string
    db.QueryRow("SELECT name FROM test WHERE id = ?", 1).Scan(&name)
    if name != "Ana" { t.Errorf("esperado Ana, got %s", name) }
}

💡 Memoria y rendimiento

  • El pool de conexiones reutiliza conexiones — evita el overhead de abrir/cerrar
  • sql.Open no conecta inmediatamente — db.Ping() fuerza la conexión
  • SetMaxOpenConns limita conexiones simultáneas a la BD
  • Prepared statements se cachean por conexión
  • Las transacciones ocupan una conexión del pool hasta Commit/Rollback

✅ Buenas prácticas

  • ✅ Siempre llama a rows.Close() incluso si iteras todas las filas
  • ✅ Siempre verifica rows.Err() después del bucle
  • ✅ Usa db.SetMaxOpenConns y db.SetMaxIdleConns apropiadamente
  • ✅ Usa $1, $2 (PostgreSQL) o ? (MySQL/SQLite) para parámetros — nunca interpolación
  • ⚠️ No olvides el _ en el import del driver para que init() se ejecute
  • ⚠️ LastInsertId() no es soportado por todos los drivers (PostgreSQL usa RETURNING)
  • ❌ No compartas *sql.DB entre goroutines sin sincronización — el pool ya es thread-safe

🏗️ Metodología

  • Queries: QueryContext con context para cancelación
  • Transacciones cortas: mantenlas breves para no bloquear el pool
  • Migrations: librerías como golang-migrate o pressly/goose
  • Testing: SQLite en memoria o testcontainers para integración real
  • ORM: GORM o sqlx para proyectos complejos; database/sql para control fino

🔗 Para saber más

Testing

¿Qué es?

Go incluye testing en la stdlib: go test, subtests, table-driven tests, benchmarks, testdata, y cobertura. No necesitas librerías externas para tests unitarios. Es como tener un laboratorio portátil: pipetas, probetas y microscopio incluidos en la mochila.

🔗 Go dev — Testing

Sintaxis básica

// main_test.go
import "testing"

func TestSuma(t *testing.T) {
    result := Suma(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("Suma(2,3) = %d; esperado %d", result, expected)
    }
}

Ejecutar:

go test                    # tests del paquete actual
go test ./...             # todos los paquetes
go test -v                # verbose
go test -run TestSuma     # test específico
go test -count=1          # sin caché
go test -race             # race detector

Subtests (t.Run):

func TestUsuario(t *testing.T) {
    t.Run("crear usuario válido", func(t *testing.T) {
        u := NuevoUsuario("Ana", "ana@email.com")
        if u.Nombre != "Ana" {
            t.Errorf("nombre incorrecto")
        }
    })

    t.Run("crear usuario sin email", func(t *testing.T) {
        _, err := NuevoUsuario("Ana", "")
        if err == nil {
            t.Error("se esperaba error")
        }
    })
}

Table-driven tests (el patrón estándar en Go):

func TestDividir(t *testing.T) {
    tests := []struct {
        nombre   string
        a, b     float64
        esperado float64
        err      bool
    }{
        {nombre: "división normal", a: 10, b: 2, esperado: 5, err: false},
        {nombre: "división exacta", a: 9, b: 3, esperado: 3, err: false},
        {nombre: "división por cero", a: 10, b: 0, esperado: 0, err: true},
        {nombre: "números negativos", a: -10, b: 2, esperado: -5, err: false},
    }

    for _, tt := range tests {
        t.Run(tt.nombre, func(t *testing.T) {
            result, err := Dividir(tt.a, tt.b)
            if tt.err {
                if err == nil {
                    t.Error("se esperaba un error")
                }
                return
            }
            if result != tt.esperado {
                t.Errorf("Dividir(%f, %f) = %f; esperado %f", tt.a, tt.b, result, tt.esperado)
            }
        })
    }
}

Benchmarks:

func BenchmarkSuma(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Suma(1000, 2000)
    }
}
go test -bench=.
go test -bench=. -benchmem  # también muestra alocaciones de memoria

Test helpers:

func setupTest(t *testing.T) *sql.DB {
    t.Helper() // marca como helper — el error se reporta en la línea del caller
    db, err := sql.Open("sqlite3", ":memory:")
    if err != nil {
        t.Fatal(err)
    }
    return db
}

func TestConBD(t *testing.T) {
    db := setupTest(t) // si falla, el error apunta aquí, no a setupTest
    defer db.Close()
    // ...
}

Test fixtures con testdata:

func TestLeerArchivo(t *testing.T) {
    data, err := os.ReadFile("testdata/input.json")
    // testdata/ es un directorio especial que Go ignora en builds
}

Testing HTTP (net/http/httptest):

func TestHandler(t *testing.T) {
    req := httptest.NewRequest(http.MethodGet, "/api/usuarios", nil)
    w := httptest.NewRecorder()

    miHandler(w, req)

    resp := w.Result()
    body, _ := io.ReadAll(resp.Body)

    if resp.StatusCode != http.StatusOK {
        t.Errorf("status = %d; esperado %d", resp.StatusCode, http.StatusOK)
    }
    // assert JSON body...
}

🧪 Cómo probarlo (meta)

func TestTestingItself(t *testing.T) {
    t.Run("table-driven", func(t *testing.T) {
        tests := []struct{ a, b, expected int }{
            {1, 1, 2}, {0, 0, 0}, {-1, 1, 0},
        }
        for _, tt := range tests {
            if got := Suma(tt.a, tt.b); got != tt.expected {
                t.Errorf("Suma(%d,%d) = %d", tt.a, tt.b, got)
            }
        }
    })
}

💡 Memoria y rendimiento

  • go test compila cada paquete y ejecuta los tests en binarios separados
  • -count=1 desactiva caché de tests
  • -race añade overhead (~5-10x más lento) pero detecta data races
  • -benchmem muestra allocs por operación — útil para optimización
  • Los tests se ejecutan secuencialmente por defecto; t.Parallel() para concurrencia

✅ Buenas prácticas

  • ✅ Nombra archivos *_test.go — Go los reconoce automáticamente
  • ✅ Usa table-driven tests para múltiples casos de entrada
  • ✅ Usa t.Helper() en funciones auxiliares para mejor stack traces
  • ✅ Usa testdata/ para archivos de fixture
  • ✅ Usa httptest.NewServer/NewRecorder para tests HTTP
  • ⚠️ No uses testing en código de producción
  • ⚠️ t.Fatal/t.FailNow detienen el test actual — úsalos con cuidado

🏗️ Metodología

  • Unitarios: table-driven con subtests
  • HTTP: httptest.NewRecorder + httptest.NewRequest
  • BD: SQLite en memoria o testcontainers
  • Mocking: interfaces pequeñas → mocks fáciles (gomock, mockery)
  • Cobertura: go test -coverprofile=coverage.out
  • Tests de integración: build tags (//go:build integration)

🔗 Para saber más


Sistema de archivos

os — operaciones del sistema

¿Qué es?

El paquete os proporciona una interfaz multiplataforma para operaciones del sistema operativo: archivos, directorios, permisos, variables de entorno y procesos. Es como tener un mando a distancia del sistema de archivos: puedes abrir, crear, mover y eliminar archivos sin importar el SO subyacente.

🔗 Go dev — os

Sintaxis básica

import "os"

// Archivos
f, err := os.Open("archivo.txt")       // solo lectura
f, err := os.Create("archivo.txt")      // crea o trunca
data, err := os.ReadFile("archivo.txt") // leer completo (~ioutil.ReadFile)
err := os.WriteFile("archivo.txt", []byte("hola"), 0644)
info, err := os.Stat("archivo.txt")     // metadatos
err := os.Mkdir("dir", 0755)
err := os.MkdirAll("a/b/c", 0755)      // crea directorios intermedios
err := os.Remove("archivo.txt")
err := os.RemoveAll("dir")             // rm -rf
err := os.Rename("old.txt", "new.txt")
entries, err := os.ReadDir(".")        // lista directorio (Go 1.16+)

os.File — operaciones avanzadas:

f, _ := os.Open("archivo.txt")
defer f.Close()

buf := make([]byte, 1024)
n, err := f.Read(buf)        // leer hasta len(buf) bytes
n, err := f.Write([]byte("datos"))
pos, err := f.Seek(0, io.SeekStart) // ir al principio
f.Close()

🧪 Cómo probarlo

func TestFileOperations(t *testing.T) {
    // Usar t.TempDir() para directorio temporal automático
    path := filepath.Join(t.TempDir(), "test.txt")
    
    err := os.WriteFile(path, []byte("hola mundo"), 0644)
    if err != nil { t.Fatal(err) }
    
    data, err := os.ReadFile(path)
    if err != nil { t.Fatal(err) }
    if string(data) != "hola mundo" {
        t.Errorf("esperado 'hola mundo', got %s", data)
    }
    
    info, err := os.Stat(path)
    if err != nil { t.Fatal(err) }
    if info.Size() != 10 { t.Errorf("size debe ser 10, got %d", info.Size()) }
}

func TestTempDirRemoves(t *testing.T) {
    dir := t.TempDir()
    path := filepath.Join(dir, "tmp.txt")
    os.WriteFile(path, []byte("data"), 0644)
    // t.TempDir() se borra automáticamente al terminar el test
}

💡 Memoria y rendimiento

  • os.ReadFile carga todo el archivo en memoria — peligroso para archivos >100 MB
  • os.File.Read(buf) con buffer propio permite streaming controlado
  • os.ReadDir es más eficiente que ioutil.ReadDir (obsoleto) — no lista archivos uno a uno
  • Las operaciones de archivo son llamadas al sistema (syscalls) — hay overhead por cada llamada

✅ Buenas prácticas

  • ✅ Usa defer f.Close() inmediatamente después de os.Open/os.Create
  • ✅ Usa os.ReadFile/os.WriteFile para archivos pequeños (menos de 10 MB)
  • ⚠️ No uses ioutil (obsoleto desde Go 1.16) — usa os y io
  • ❌ No ignores errores de f.Close() — al menos loguea

🏗️ Metodología

  • Archivos pequeños: os.ReadFile/os.WriteFile (una llamada, simple)
  • Archivos grandes: os.Open + bufio.Scanner (streaming línea por línea)
  • Directorios: os.ReadDir para listar, filepath.Walk para recorrer árbol
  • Temporales: t.TempDir() en tests, os.CreateTemp en producción

🔗 Para saber más

bufio — I/O con buffer

¿Qué es?

bufio envuelve un io.Reader o io.Writer con un buffer interno para reducir llamadas al sistema. Es como tener un depósito de agua: en lugar de ir al pozo por cada gota, llenas un cubo y bebes de él. El Scanner es la forma idiomática de leer línea por línea.

🔗 Go dev — bufio

Sintaxis básica

import "bufio"

f, _ := os.Open("archivo.txt")
defer f.Close()

// Scanner — leer línea por línea (idiomático)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
    log.Fatal(err)
}

// Reader con buffer
r := bufio.NewReader(f)
line, err := r.ReadString('\n')

// Writer con buffer
w := bufio.NewWriter(os.Stdout)
w.WriteString("hola")
w.Flush() // vaciar buffer al writer subyacente

🧪 Cómo probarlo

func TestBufioScanner(t *testing.T) {
    content := "línea 1\nlínea 2\nlínea 3\n"
    r := strings.NewReader(content)
    scanner := bufio.NewScanner(r)
    
    var lines []string
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        t.Fatal(err)
    }
    if len(lines) != 3 { t.Errorf("esperado 3 líneas, got %d", len(lines)) }
    if lines[0] != "línea 1" { t.Errorf("esperado 'línea 1', got %s", lines[0]) }
}

func TestBufioWriter(t *testing.T) {
    var buf bytes.Buffer
    w := bufio.NewWriter(&buf)
    w.WriteString("hola ")
    w.WriteString("mundo")
    w.Flush()
    if buf.String() != "hola mundo" {
        t.Errorf("esperado 'hola mundo', got %s", buf.String())
    }
}

💡 Memoria y rendimiento

  • Buffer por defecto: 4096 bytes — óptimo para la mayoría de casos
  • bufio.Scanner limita línea a 64KB por defecto (ajustable con Scanner.Buffer())
  • Sin buffer: cada Read de 1 byte es una syscall — con buffer se reduce 4096x
  • bufio.Writer acumula escrituras hasta Flush() — mejora rendimiento en archivos

✅ Buenas prácticas

  • ✅ Usa bufio.Scanner para leer archivos línea por línea (es lo más idiomático)
  • ✅ Usa bufio.NewWriter para escritura intensiva (reduce syscalls)
  • ⚠️ No olvides Flush() — el writer con buffer no vacía automáticamente
  • ❌ No uses ReadString('\n') mezclado con Scanner en el mismo archivo

🏗️ Metodología

  • Archivos de texto: bufio.Scanner (línea por línea)
  • Archivos binarios: bufio.NewReader con Read de fragmentos
  • Logging intensivo: bufio.NewWriter con Flush periódico
  • Red: bufio.NewReader/NewWriter para protocolos basados en líneas

🔗 Para saber más

io — interfaces fundamentales

¿Qué es?

io.Reader y io.Writer son las interfaces más ubicuas de Go. Cualquier cosa que produzca bytes implementa Reader; cualquier cosa que consuma bytes implementa Writer. Es como el USB: todo lo que se conecta habla el mismo protocolo, independientemente de lo que sea.

🔗 Go dev — io

Sintaxis básica

import "io"

// Interfaces base
type Reader interface {
    Read(p []byte) (n int, err error)
}
type Writer interface {
    Write(p []byte) (n int, err error)
}
type Closer interface {
    Close() error
}

// Utilidades
data, _ := io.ReadAll(r)       // leer todo
n, _ := io.Copy(w, r)          // copiar de reader a writer
io.CopyN(w, r, 1024)           // copiar N bytes
combined := io.MultiReader(r1, r2) // concatenar readers
writers := io.MultiWriter(w1, w2)  // escribir a múltiples writers

🧪 Cómo probarlo

func TestIoCopy(t *testing.T) {
    var buf bytes.Buffer
    r := strings.NewReader("hola mundo")
    n, err := io.Copy(&buf, r)
    if err != nil { t.Fatal(err) }
    if n != 10 { t.Errorf("esperado 10 bytes, got %d", n) }
    if buf.String() != "hola mundo" {
        t.Errorf("esperado 'hola mundo', got %s", buf.String())
    }
}

func TestMultiWriter(t *testing.T) {
    var buf1, buf2 bytes.Buffer
    mw := io.MultiWriter(&buf1, &buf2)
    mw.Write([]byte("test"))
    if buf1.String() != "test" { t.Error("buf1 falló") }
    if buf2.String() != "test" { t.Error("buf2 falló") }
}

💡 Memoria y rendimiento

  • io.Copy usa un buffer interno de 32KB — eficiente para copias grandes
  • io.ReadAll carga todo en memoria — O(n) en RAM
  • io.MultiWriter escribe secuencialmente — el más lento determina la velocidad
  • Las interfaces Reader/Writer no tienen overhead de reflection — son tipos concretos

✅ Buenas prácticas

  • ✅ Escribe funciones que acepten io.Reader/io.Writer en lugar de *os.File
  • ✅ Usa io.Copy en lugar de leer todo y luego escribir
  • ⚠️ No uses io.ReadAll para archivos grandes
  • ❌ No implementes Read/Write manualmente sin manejar io.EOF

🏗️ Metodología

  • Funciones reutilizables: acepta io.Reader/io.Writer, no tipos concretos
  • Tests: usa strings.NewReader o bytes.Buffer como mock de archivos
  • Streaming: io.Copy para tuberías de datos
  • Logging: io.MultiWriter para escribir a archivo y stdout simultáneamente

🔗 Para saber más

io/fs — sistema de archivos virtual (Go 1.16+)

¿Qué es?

io/fs define la interfaz fs.FS, un sistema de archivos virtual que puede ser implementado por cualquier tipo: directorios reales, archivos embebidos, sistemas remotos, ZIP. Es como un adaptador universal para sistemas de archivos: da igual si es disco duro, memoria o red, la API es la misma.

🔗 Go dev — io/fs

Sintaxis básica

import "io/fs"

// FS interface — cualquier sistema de archivos
type FS interface {
    Open(name string) (File, error)
}

// Recorrer directorios
fs.WalkDir(os.DirFS("."), ".", func(path string, d fs.DirEntry, err error) error {
    fmt.Println(path)
    return nil
})

// Leer archivo desde un FS
data, _ := fs.ReadFile(os.DirFS("."), "archivo.txt")

🧪 Cómo probarlo

func TestFSInterface(t *testing.T) {
    // Crear un FS virtual en memoria
    mfs := fstest.MapFS{
        "hola.txt":   &fstest.MapFile{Data: []byte("mundo")},
        "dir/a.txt":  &fstest.MapFile{Data: []byte("a")},
    }
    
    data, err := fs.ReadFile(mfs, "hola.txt")
    if err != nil { t.Fatal(err) }
    if string(data) != "mundo" { t.Error("contenido incorrecto") }
    
    err = fs.WalkDir(mfs, ".", func(path string, d fs.DirEntry, err error) error {
        if d.IsDir() && path != "." {
            t.Logf("dir: %s", path)
        }
        return nil
    })
    if err != nil { t.Fatal(err) }
}

💡 Memoria y rendimiento

  • fs.FS es una interfaz ligera — implementarla es barato
  • os.DirFS añade overhead mínimo sobre operaciones directas de disco
  • fstest.MapFS opera en RAM — ideal para tests rápidos
  • fs.WalkDir es más eficiente que filepath.Walk (evita stat innecesarios)

✅ Buenas prácticas

  • ✅ Usa fs.FS como tipo de parámetro para aceptar cualquier origen de archivos
  • ✅ Usa fstest.MapFS para tests — evita archivos temporales
  • ⚠️ No implementes fs.FS manualmente si solo lees archivos reales
  • ❌ No asumas que todos los FS tienen directorios — algunos son planos (embed)

🏗️ Metodología

  • Tests: fstest.MapFS para simular cualquier estructura
  • Producción: os.DirFS para archivos reales, embed.FS para estáticos
  • Librerías: acepta fs.FS en lugar de rutas de archivo para máxima flexibilidad

🔗 Para saber más

filepath — manipulación de rutas

¿Qué es?

filepath proporciona funciones para manipular rutas de archivo de forma multiplataforma. Linux usa /, Windows usa \ — filepath normaliza automáticamente. Es como un GPS para rutas: siempre sabes dónde estás y cómo llegar, sin importar el mapa (SO).

🔗 Go dev — filepath

Sintaxis básica

import "path/filepath"

path := filepath.Join("dir", "sub", "archivo.txt") // dir/sub/archivo.txt
dir := filepath.Dir("/a/b/c.txt")   // /a/b
base := filepath.Base("/a/b/c.txt") // c.txt
ext := filepath.Ext("archivo.txt")  // .txt

filepath.Walk("dir", func(path string, info os.FileInfo, err error) error {
    fmt.Println(path)
    return nil
})

matches, _ := filepath.Glob("*.txt")  // todos los .txt del directorio

🧪 Cómo probarlo

func TestFilepath(t *testing.T) {
    p := filepath.Join("a", "b", "c.txt")
    if filepath.Dir(p) != filepath.FromSlash("a/b") {
        t.Errorf("dir incorrecto: %s", filepath.Dir(p))
    }
    if filepath.Base(p) != "c.txt" { t.Error("base incorrecta") }
    if filepath.Ext(p) != ".txt" { t.Error("extensión incorrecta") }
    
    matches, err := filepath.Glob("*.go")
    if err != nil { t.Fatal(err) }
    if len(matches) == 0 { t.Log("no hay archivos .go en el directorio") }
}

func TestWalk(t *testing.T) {
    dir := t.TempDir()
    os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)
    os.Mkdir(filepath.Join(dir, "sub"), 0755)
    os.WriteFile(filepath.Join(dir, "sub", "b.txt"), []byte("b"), 0644)
    
    var files []string
    filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
        if !info.IsDir() { files = append(files, info.Name()) }
        return nil
    })
    if len(files) != 2 { t.Errorf("esperado 2 archivos, got %d", len(files)) }
}

💡 Memoria y rendimiento

  • filepath.Join es O(n) — construye la ruta sin allocations extra
  • filepath.Walk usa callbacks — no carga el árbol completo en memoria
  • filepath.Glob usa el mismo matching que la shell — O(n) en el directorio
  • Clean paths: filepath.Clean normaliza ../ y // sin tocar el disco

✅ Buenas prácticas

  • ✅ Siempre usa filepath.Join en lugar de concatenar strings con /
  • ✅ Usa filepath.Walk en lugar de os.ReadDir recursivo manual
  • ⚠️ No uses filepath.Walk si el árbol es muy profundo (>1000 niveles)
  • ❌ No asumas / como separador — filepath.Join es multiplataforma

🏗️ Metodología

  • Rutas simples: filepath.Join + filepath.Dir/Base/Ext
  • Recorrer árbol: filepath.Walk para procesar todos los archivos
  • Búsqueda por patrón: filepath.Glob para encontrar archivos por extensión
  • Rutas relativas/absolutas: filepath.Abs + filepath.Rel

🔗 Para saber más

embed — incrustar archivos en binario (Go 1.16+)

¿Qué es?

embed permite incrustar archivos estáticos (HTML, CSS, imágenes) dentro del binario Go en tiempo de compilación. El resultado es un solo binario sin dependencias externas. Es como si el archivo se imprimiera en el ADN del programa: viaja con él a donde vaya.

🔗 Go dev — embed

Sintaxis básica

import "embed"

//go:embed templates/*.html
//go:embed static/css/*.css
var assets embed.FS

func main() {
    data, _ := assets.ReadFile("templates/index.html")
    fmt.Println(string(data))
    http.Handle("/static/", http.FileServer(http.FS(assets)))
}

🧪 Cómo probarlo

func TestEmbed(t *testing.T) {
    data, err := assets.ReadFile("templates/index.html")
    if err != nil { t.Fatal(err) }
    if len(data) == 0 { t.Error("el archivo embebido está vacío") }
}

func TestEmbedFileServer(t *testing.T) {
    ts := httptest.NewServer(http.FileServer(http.FS(assets)))
    defer ts.Close()
    
    resp, err := http.Get(ts.URL + "/templates/index.html")
    if err != nil { t.Fatal(err) }
    defer resp.Body.Close()
    
    if resp.StatusCode != http.StatusOK {
        t.Errorf("status %d, esperado 200", resp.StatusCode)
    }
}

💡 Memoria y rendimiento

  • Los archivos se incrustan en el binario como []byte — aumentan el tamaño del binario
  • El contenido está en memoria mapeada — acceso O(1) sin I/O de disco
  • Los patrones //go:embed se validan en compilación — errores tempranos
  • Sirviendo con http.FS no hay syscalls de disco — extremadamente rápido

✅ Buenas prácticas

  • ✅ Úsalo para templates, archivos estáticos, configuraciones por defecto
  • ✅ Especifica patrones precisos en //go:embed para no incluir archivos grandes
  • ⚠️ No incrustes archivos grandes (>10 MB) — el binario se hace muy pesado
  • ❌ No uses embed para archivos que cambian frecuentemente — requieren recompilación

🏗️ Metodología

  • CLI tools: incrusta templates, ayuda, defaults en el binario
  • Servidores web: incrusta archivos estáticos para despliegue con un solo binario
  • Migraciones SQL: incrusta archivos .sql para migraciones sin path externo
  • Tests: usa embed.FS como implementación de fs.FS en tests de integración

🔗 Para saber más

encoding/csv

¿Qué es?

El paquete encoding/csv lee y escribe archivos CSV (Comma-Separated Values). Soporta comillas, escapes y diferentes delimitadores. Es el formato estándar para intercambio de datos tabulares.

🔗 Go dev — csv

Sintaxis básica

import "encoding/csv"

f, _ := os.Create("datos.csv")
w := csv.NewWriter(f)
w.Write([]string{"Nombre", "Email", "Edad"})
w.Write([]string{"Ana", "ana@mail.com", "30"})
w.Flush()

f, _ = os.Open("datos.csv")
r := csv.NewReader(f)
records, _ := r.ReadAll() // [][]string

🧪 Cómo probarlo

func TestCSVReadWrite(t *testing.T) {
    var buf bytes.Buffer
    w := csv.NewWriter(&buf)
    w.Write([]string{"nombre", "edad"})
    w.Write([]string{"Ana", "30"})
    w.Write([]string{"Luis", "25"})
    w.Flush()
    if err := w.Error(); err != nil { t.Fatal(err) }
    
    r := csv.NewReader(&buf)
    records, err := r.ReadAll()
    if err != nil { t.Fatal(err) }
    if len(records) != 3 { t.Errorf("esperado 3 filas, got %d", len(records)) }
    if records[1][0] != "Ana" { t.Errorf("esperado Ana, got %s", records[1][0]) }
}

func TestCSVReaderOptions(t *testing.T) {
    // CSV con punto y coma
    data := "a;1\nb;2\n"
    r := csv.NewReader(strings.NewReader(data))
    r.Comma = ';'
    records, _ := r.ReadAll()
    if records[0][0] != "a" { t.Error("coma personalizada falló") }
}

💡 Memoria y rendimiento

  • ReadAll() carga todo el archivo en memoria — O(n) en RAM
  • Read() fila por fila para streaming — O(1) memoria por iteración
  • csv.NewReader con FieldsPerRecord = -1 permite número variable de columnas
  • El parser maneja RFC 4180 — comillas, escapes, saltos de línea en campos

✅ Buenas prácticas

  • ✅ Siempre verifica w.Error() después de Flush() — los errores de escritura son diferidos
  • ✅ Usa Read() en bucle para archivos grandes (>100 MB)
  • ⚠️ No asumas que todas las filas tienen el mismo número de columnas
  • ❌ No uses CSV para datos anidados o complejos — usa JSON

🏗️ Metodología

  • Exportación de datos: CSV para hojas de cálculo y Excel
  • ETL simple: CSV para intercambio entre sistemas legacy
  • Grandes volúmenes: streaming con Read() fila por fila
  • Validación: parsea con csv.NewReader, valida campo por campo

🔗 Para saber más

encoding/json

¿Qué es?

encoding/json serializa (marshal) y deserializa (unmarshal) datos Go a/desde JSON. Usa struct tags para mapear campos. Es la forma estándar de intercambiar datos en APIs REST.

🔗 Go dev — json

Sintaxis básica

import "encoding/json"

type Usuario struct {
    Nombre string `json:"nombre"`
    Email  string `json:"email,omitempty"`
}

u := Usuario{Nombre: "Ana", Email: "ana@mail.com"}
bytes, _ := json.Marshal(u)
json.Unmarshal(bytes, &u)

// Streaming
json.NewEncoder(w).Encode(u)
json.NewDecoder(r).Decode(&u)

🧪 Cómo probarlo

func TestJSONMarshalUnmarshal(t *testing.T) {
    type Item struct {
        ID    int    `json:"id"`
        Name  string `json:"name"`
    }
    original := Item{ID: 1, Name: "test"}
    
    data, err := json.Marshal(original)
    if err != nil { t.Fatal(err) }
    if !strings.Contains(string(data), `"id":1`) {
        t.Error("JSON malformado")
    }
    
    var decoded Item
    err = json.Unmarshal(data, &decoded)
    if err != nil { t.Fatal(err) }
    if decoded != original {
        t.Errorf("decoded %+v != original %+v", decoded, original)
    }
}

func TestJSONOmitEmpty(t *testing.T) {
    type User struct {
        Name  string `json:"name"`
        Email string `json:"email,omitempty"`
    }
    u := User{Name: "Ana"}
    data, _ := json.Marshal(u)
    if strings.Contains(string(data), "email") {
        t.Error("omitempty debería omitir email vacío")
    }
}

💡 Memoria y rendimiento

  • json.Marshal/Unmarshal usa reflection — más lento que serialización manual
  • Los decoders/encoders con streaming son más eficientes en memoria
  • json.RawMessage permite decodificación perezosa (lazy)
  • La reutilización de buffers con json.NewEncoder reduce allocs

✅ Buenas prácticas

  • ✅ Usa struct tags json:"campo" para controlar la serialización
  • ✅ Usa json:",omitempty" para omitir campos cero/vacíos
  • ⚠️ No serialices campos con valores zero si no deben aparecer — usa omitempty
  • ❌ No uses json.Marshal/Unmarshal con any sin type assertion

🏗️ Metodología

  • APIs REST: json.NewEncoder/json.NewDecoder para streaming HTTP
  • Archivos: json.Marshal/Unmarshal para archivos completos
  • Config: json.Decoder con os.Open para leer config desde archivo
  • NDJSON: json.NewDecoder en bucle para múltiples objetos por archivo

🔗 Para saber más

archive/zip y compress/gzip

¿Qué es?

archive/zip lee y escribe archivos ZIP. compress/gzip comprime/descomprime en formato gzip. ZIP agrupa múltiples archivos, gzip comprime un solo flujo de datos. Son como mochilas (ZIP guarda varias cosas organizadas) vs bolsas de vacío (gzip comprime una cosa al máximo).

🔗 Go dev — archive/zip, Go dev — compress/gzip

Sintaxis básica

import "archive/zip"
import "compress/gzip"

// ZIP
w := zip.NewWriter(file)
f, _ := w.Create("archivo.txt")
f.Write([]byte("contenido"))
w.Close()

// GZIP
gw := gzip.NewWriter(file)
gw.Write([]byte("datos comprimidos"))
gw.Close()

gr, _ := gzip.NewReader(file)
data, _ := io.ReadAll(gr)
gr.Close()

🧪 Cómo probarlo

func TestZipRoundtrip(t *testing.T) {
    var buf bytes.Buffer
    zw := zip.NewWriter(&buf)
    f, _ := zw.Create("test.txt")
    f.Write([]byte("contenido"))
    zw.Close()
    
    zr, err := zip.NewReader(bytes.NewReader(buf.Bytes()), int64(buf.Len()))
    if err != nil { t.Fatal(err) }
    if len(zr.File) != 1 { t.Errorf("esperado 1 archivo, got %d", len(zr.File)) }
    
    rc, _ := zr.File[0].Open()
    data, _ := io.ReadAll(rc)
    rc.Close()
    if string(data) != "contenido" { t.Error("contenido del zip incorrecto") }
}

func TestGzipRoundtrip(t *testing.T) {
    var buf bytes.Buffer
    gw := gzip.NewWriter(&buf)
    gw.Write([]byte("hola mundo"))
    gw.Close()
    
    gr, _ := gzip.NewReader(&buf)
    data, _ := io.ReadAll(gr)
    gr.Close()
    if string(data) != "hola mundo" { t.Error("contenido gzip incorrecto") }
}

💡 Memoria y rendimiento

  • ZIP usa compresión DEFLATE por defecto — ratio ~3:1 en texto
  • GZIP es más rápido que ZIP para un solo archivo (menos overhead de cabeceras)
  • compress/gzip tiene niveles 1-9 (velocidad vs compresión)
  • archive/zip lista el índice al abrir — O(n) para archivos con muchos archivos

✅ Buenas prácticas

  • ✅ Usa ZIP para empaquetar múltiples archivos con estructura
  • ✅ Usa GZIP para comprimir streams individuales (logs, respuestas HTTP)
  • ⚠️ Siempre cierra el writer para finalizar el archivo ZIP/GZIP
  • ❌ No comprimas archivos ya comprimidos (JPEG, MP4) — no ganas espacio

🏗️ Metodología

  • Backups: ZIP para agrupar múltiples archivos
  • Logs: GZIP para comprimir archivos de log rotados
  • HTTP: gzip.NewWriter como middleware de compresión
  • Distribución: ZIP para distribuir múltiples archivos empaquetados

🔗 Para saber más


Algoritmos y estructuras de datos

sort — ordenación y búsqueda

¿Qué es?

El paquete sort implementa ordenación primitiva para tipos built-in y genérica con sort.Slice. También incluye búsqueda binaria con sort.Search. Es como tener un asistente de ordenación: dile cómo comparar elementos y él los organiza.

🔗 Go dev — sort

Sintaxis básica

import "sort"

// Ordenar slice de enteros
nums := []int{3, 1, 4, 1, 5}
sort.Ints(nums)

// Ordenar slice genérico con función de comparación
sort.Slice(nums, func(i, j int) bool {
    return nums[i] < nums[j]
})

// Búsqueda binaria (slice debe estar ordenado)
idx := sort.Search(len(nums), func(i int) bool {
    return nums[i] >= 4
}) // devuelve el primer índice donde nums[i] >= 4

🧪 Cómo probarlo

func TestSort(t *testing.T) {
    nums := []int{3, 1, 4, 1, 5}
    sort.Ints(nums)
    if !sort.IntsAreSorted(nums) {
        t.Errorf("esperado ordenado, got %v", nums)
    }
    if nums[0] != 1 || nums[4] != 5 {
        t.Errorf("orden incorrecto: %v", nums)
    }
}

func TestSortSlice(t *testing.T) {
    items := []struct { Name string; Age int }{
        {"Ana", 30}, {"Luis", 25}, {"Carlos", 35},
    }
    sort.Slice(items, func(i, j int) bool {
        return items[i].Age < items[j].Age
    })
    if items[0].Name != "Luis" {
        t.Errorf("el más joven debería ser Luis, got %s", items[0].Name)
    }
}

func TestSearch(t *testing.T) {
    nums := []int{1, 3, 5, 7, 9}
    idx := sort.Search(len(nums), func(i int) bool { return nums[i] >= 5 })
    if idx != 2 { t.Errorf("esperado índice 2, got %d", idx) }
}

💡 Memoria y rendimiento

  • sort.Ints usa QuickSort (variante) — O(n log n) promedio, O(n²) peor caso
  • sort.Slice es O(n log n) pero más lento que sort.Ints (usa reflection)
  • sort.Search es O(log n) — búsqueda binaria clásica
  • El ordenamiento es in-place — O(1) memoria extra

✅ Buenas prácticas

  • ✅ Usa sort.Ints/sort.Strings para tipos nativos (más rápido que Slice)
  • ✅ Usa sort.Slice para structs con comparadores personalizados
  • ⚠️ No llames a sort.Search en un slice desordenado — resultados indefinidos
  • ❌ No implementes sort.Interface manualmente si sort.Slice te sirve

🏗️ Metodología

  • Tipos nativos: sort.Ints/sort.Strings/sort.Float64s
  • Structs: sort.Slice con función de comparación
  • Múltiples criterios: sort.SliceStable para ordenación estable
  • Búsqueda: sort.Search después de ordenar

🔗 Para saber más

slices — utilidades genéricas (Go 1.21+)

¿Qué es?

El paquete slices proporciona funciones genéricas para manipular slices: ordenación, búsqueda, inserción, eliminación, comparación. Es la evolución moderna de sort para slices, disponible desde Go 1.21.

🔗 Go dev — slices

Sintaxis básica

import "slices"

nums := []int{3, 1, 4, 1, 5}

slices.Sort(nums)                         // ordenación
idx, found := slices.BinarySearch(nums, 4) // búsqueda binaria
nums = slices.Delete(nums, 1, 3)           // eliminar elementos [1,3)
nums = slices.Insert(nums, 0, 0)           // insertar
slices.Reverse(nums)                       // invertir
slices.Compact(nums)                       // eliminar duplicados adyacentes
slices.Contains(nums, 3)                   // contiene
slices.Index(nums, 4)                      // primer índice
slices.Equal(nums, []int{1, 2, 3})         // igualdad

🧪 Cómo probarlo

func TestSlicesSort(t *testing.T) {
    nums := []int{3, 1, 4}
    slices.Sort(nums)
    if !slices.IsSorted(nums) { t.Error("no ordenado") }
}

func TestSlicesBinarySearch(t *testing.T) {
    nums := []int{1, 3, 5, 7}
    idx, found := slices.BinarySearch(nums, 5)
    if !found || idx != 2 {
        t.Errorf("esperado encontrado en 2, got %d, %v", idx, found)
    }
    _, found = slices.BinarySearch(nums, 4)
    if found { t.Error("4 no debería estar en el slice") }
}

func TestSlicesDelete(t *testing.T) {
    s := []int{1, 2, 3, 4, 5}
    s = slices.Delete(s, 1, 3)
    if !slices.Equal(s, []int{1, 4, 5}) {
        t.Errorf("esperado [1,4,5], got %v", s)
    }
}

func TestSlicesCompact(t *testing.T) {
    s := []int{1, 1, 2, 2, 3}
    s = slices.Compact(s)
    if !slices.Equal(s, []int{1, 2, 3}) {
        t.Errorf("esperado [1,2,3], got %v", s)
    }
}

💡 Memoria y rendimiento

  • slices.Sort es un algoritmo híbrido (pdqsort) — O(n log n) promedio
  • slices.BinarySearch es O(log n)
  • slices.Delete/slices.Insert son O(n) — mueven elementos
  • slices.Compact es O(n) — elimina duplicados adyacentes en una pasada
  • slices.Equal es O(n) pero sale rápido en la primera diferencia

✅ Buenas prácticas

  • ✅ Prefiere slices sobre sort para código moderno (Go 1.21+)
  • ✅ Usa slices.Contains en lugar de búsqueda manual con for
  • ⚠️ slices.Compact solo elimina duplicados adyacentes — ordena primero
  • ❌ No uses slices.Equal con slices de tipos no comparables

🏗️ Metodología

  • Ordenar: slices.Sort / slices.SortFunc (con comparador personalizado)
  • Buscar: slices.BinarySearch (datos ordenados) / slices.Index (lineal)
  • Modificar: slices.Delete, slices.Insert, slices.Replace
  • Comparar: slices.Equal / slices.Compare

🔗 Para saber más

maps — utilidades genéricas (Go 1.21+)

¿Qué es?

El paquete maps proporciona funciones genéricas para manipular maps: clonar, copiar, obtener claves/valores, eliminar por condición. Es el complemento moderno para trabajar con maps de forma funcional.

🔗 Go dev — maps

Sintaxis básica

import "maps"

m1 := map[string]int{"a": 1, "b": 2}
m2 := maps.Clone(m1)           // copia superficial
maps.Copy(m2, m1)              // copia m1 sobre m2 (sobrescribe claves)
keys := maps.Keys(m1)          // slice de claves (orden no determinado)
vals := maps.Values(m1)        // slice de valores
maps.DeleteFunc(m1, func(k string, v int) bool {
    return v == 1              // eliminar donde v == 1
})

🧪 Cómo probarlo

func TestMapsClone(t *testing.T) {
    original := map[string]int{"a": 1, "b": 2}
    clone := maps.Clone(original)
    if !maps.Equal(original, clone) {
        t.Error("clone debería ser igual al original")
    }
    clone["c"] = 3
    if _, ok := original["c"]; ok {
        t.Error("modificar clone no debería afectar al original")
    }
}

func TestMapsKeysValues(t *testing.T) {
    m := map[string]int{"a": 1, "b": 2}
    keys := maps.Keys(m)
    vals := maps.Values(m)
    if len(keys) != 2 || len(vals) != 2 {
        t.Error("debería tener 2 keys y 2 values")
    }
}

func TestMapsDeleteFunc(t *testing.T) {
    m := map[string]int{"a": 1, "b": 2, "c": 3}
    maps.DeleteFunc(m, func(k string, v int) bool { return v > 1 })
    if len(m) != 1 || m["a"] != 1 {
        t.Errorf("esperado solo 'a', got %v", m)
    }
}

💡 Memoria y rendimiento

  • maps.Clone es O(n) — copia superficial, los valores no se clonan
  • maps.Copy es O(n) — sobrescribe claves existentes
  • maps.Keys/maps.Values son O(n) — crean un nuevo slice
  • maps.DeleteFunc es O(n) — itera todo el map
  • maps.Equal es O(n) — compara clave a clave

✅ Buenas prácticas

  • ✅ Usa maps.Clone en lugar de crear un map nuevo y copiar manualmente
  • ✅ Usa maps.DeleteFunc para limpieza condicional en lugar de for...delete
  • ⚠️ maps.Keys no garantiza orden — ordena el resultado si es necesario
  • ❌ No uses maps.Copy esperando merge profundo — solo es shallow

🏗️ Metodología

  • Clonar: maps.Clone para evitar mutación accidental
  • Fusionar: maps.Copy para combinar maps
  • Limpiar: maps.DeleteFunc para eliminar elementos por condición
  • Serializar: maps.Keys/maps.Values para extraer datos

🔗 Para saber más

container/heap — cola de prioridad

¿Qué es?

container/heap implementa una cola de prioridad (min-heap) genérica. Cualquier tipo que implemente heap.Interface puede usarse como heap. Es como una fila VIP: el elemento con mayor prioridad sale primero, siempre.

🔗 Go dev — heap

Sintaxis básica

import "container/heap"

type MinHeap []int

func (h MinHeap) Len() int            { return len(h) }
func (h MinHeap) Less(i, j int) bool  { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }

func (h *MinHeap) Push(x any) {
    *h = append(*h, x.(int))
}

func (h *MinHeap) Pop() any {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[:n-1]
    return x
}

func main() {
    h := &MinHeap{3, 1, 4}
    heap.Init(h)
    heap.Push(h, 2)
    fmt.Println(heap.Pop(h).(int)) // 1 (mínimo)
}

🧪 Cómo probarlo

func TestMinHeap(t *testing.T) {
    h := &MinHeap{5, 3, 7, 1}
    heap.Init(h)
    
    expected := []int{1, 3, 5, 7}
    for _, exp := range expected {
        if val := heap.Pop(h).(int); val != exp {
            t.Errorf("esperado %d, got %d", exp, val)
        }
    }
}

func TestHeapSort(t *testing.T) {
    h := &MinHeap{}
    for _, v := range []int{9, 3, 7, 1, 5} {
        heap.Push(h, v)
    }
    var sorted []int
    for h.Len() > 0 {
        sorted = append(sorted, heap.Pop(h).(int))
    }
    if !slices.IsSorted(sorted) {
        t.Errorf("heap debería producir elementos ordenados, got %v", sorted)
    }
}

💡 Memoria y rendimiento

  • heap.Init es O(n) — heapify linear
  • heap.Push/heap.Pop son O(log n)
  • heap.Fix es O(log n) — útil cuando cambia la prioridad de un elemento
  • heap.Remove es O(log n) — eliminar elemento arbitrario por índice

✅ Buenas prácticas

  • ✅ Implementa heap.Interface para cualquier tipo que necesite prioridad
  • ✅ Usa heap.Init en lugar de Push repetido (O(n) vs O(n log n))
  • ⚠️ No accedas al slice subyacente directamente — usa las funciones del heap
  • ❌ No confundas Push/Pop del heap con los métodos del slice

🏗️ Metodología

  • Cola de prioridad: tareas urgentes primero, eventos temporizados
  • Top-K: mantener los K elementos más grandes/pequeños
  • Merge de streams: combinar múltiples fuentes ordenadas
  • Algoritmos: Dijkstra, Huffman, A* necesitan colas de prioridad

🔗 Para saber más

container/list — lista enlazada doble

¿Qué es?

container/list implementa una lista enlazada doble. Cada elemento tiene punteros al siguiente y anterior. Es útil para inserciones/eliminaciones frecuentes en posiciones arbitrarias, pero en Go los slices suelen ser más eficientes.

🔗 Go dev — list

Sintaxis básica

import "container/list"

l := list.New()
l.PushBack(1)
l.PushFront(0)
elem := l.PushBack(2)
l.Remove(elem)

for e := l.Front(); e != nil; e = e.Next() {
    fmt.Println(e.Value)
}

🧪 Cómo probarlo

func TestList(t *testing.T) {
    l := list.New()
    l.PushBack(1)
    l.PushFront(0)
    l.PushBack(2)
    
    if l.Len() != 3 { t.Errorf("esperado 3, got %d", l.Len()) }
    
    var vals []int
    for e := l.Front(); e != nil; e = e.Next() {
        vals = append(vals, e.Value.(int))
    }
    if !slices.Equal(vals, []int{0, 1, 2}) {
        t.Errorf("esperado [0,1,2], got %v", vals)
    }
}

func TestListRemove(t *testing.T) {
    l := list.New()
    e1 := l.PushBack(1)
    l.PushBack(2)
    e3 := l.PushBack(3)
    l.Remove(e1)
    l.Remove(e3)
    
    if l.Len() != 1 { t.Errorf("esperado 1, got %d", l.Len()) }
    if l.Front().Value.(int) != 2 {
        t.Errorf("esperado 2, got %d", l.Front().Value.(int))
    }
}

💡 Memoria y rendimiento

  • Cada elemento tiene overhead de 2 punteros (anterior, siguiente) + interfaz any
  • Acceso: O(n) — no hay acceso por índice
  • Inserción/eliminación con referencia al elemento: O(1)
  • list.List no es genérico — los valores son any (type assertion necesaria)

✅ Buenas prácticas

  • ✅ Úsalo para implementar colas FIFO o LRU cache simples
  • ⚠️ En la mayoría de casos, un slice es más eficiente que una lista enlazada
  • ❌ No lo uses para acceso frecuente por índice — el slice es O(1)

🏗️ Metodología

  • LRU cache: lista enlazada + map para acceso O(1) al elemento
  • Cola FIFO: PushBack + Remove(Front()) — pero container/list añade overhead
  • Alternativa: casi siempre prefiere slice o ring (menos overhead)

🔗 Para saber más

container/ring — lista circular

¿Qué es?

container/ring implementa una lista circular enlazada. El último elemento apunta al primero. Es útil para buffers circulares, turnos rotativos, ventanas deslizantes.

🔗 Go dev — ring

Sintaxis básica

import "container/ring"

r := ring.New(3)
for i := 0; i < r.Len(); i++ {
    r.Value = i
    r = r.Next()
}

r.Do(func(v any) {
    fmt.Println(v)
})

🧪 Cómo probarlo

func TestRing(t *testing.T) {
    r := ring.New(3)
    for i := 1; i <= 3; i++ {
        r.Value = i
        r = r.Next()
    }
    
    sum := 0
    r.Do(func(v any) { sum += v.(int) })
    if sum != 6 { t.Errorf("esperado 6, got %d", sum) }
}

func TestRingCircular(t *testing.T) {
    r := ring.New(5)
    // Avanzar más que la longitud — debe volver al inicio
    for i := 0; i < 12; i++ {
        r = r.Next()
    }
    if r.Len() != 5 { t.Error("Len no debería cambiar") }
}

💡 Memoria y rendimiento

  • ring.New(n) pre-asigna n elementos — O(n) memoria fija
  • r.Do(f) es O(n) — recorre todos los elementos
  • r.Next()/r.Prev() son O(1) — solo mueve punteros
  • r.Link()/r.Unlink() son O(1) — concatenación/división de anillos

✅ Buenas prácticas

  • ✅ Úsalo para buffers circulares de tamaño fijo
  • ⚠️ No lo uses si necesitas acceso por índice — prefiere slice
  • ❌ No modifiques el ring mientras iteras con Do — resultados impredecibles

🏗️ Metodología

  • Buffer circular: almacenar últimas N lecturas de un sensor
  • Round-robin: turnos entre N workers
  • Ventana deslizante: mantener los últimos N elementos para promediar
  • Pool de recursos: rotación entre conexiones/cachés

🔗 Para saber más

Big O de operaciones comunes en Go

EstructuraAccesoInserciónEliminaciónBúsquedaNotas
Slice (append)O(1)O(1) amortizadoO(n)O(n)append reasigna al doble cuando lleno
Slice (índice)O(1)———acceso directo por índice
Slice (búsqueda)———O(n)lineal; con sort+Search O(log n)
MapO(1) promedioO(1) promedioO(1) promedioO(1) promediotabla hash, colisiones posibles
Lista enlazadaO(n)O(1)O(1) (con elem)O(n)container/list
Heap (Push/Pop)—O(log n)O(log n)—container/heap
RingO(n)O(1)O(1)O(n)container/ring

🔗 Para saber más


POO en Go (no hay clases)

Comparativa OOP → Go

¿Qué es?

Go no tiene clases ni herencia. En su lugar usa tipos (structs), métodos (receiver functions), interfaces y composición (embedding). Es como tener un juego de Lego en vez de figuras pre-moldeadas: tienes las piezas básicas y construyes lo que necesitas.

Concepto OOPEn Go
Clasestruct con métodos
ConstructorFunción que devuelve *T (ej: NewUsuario())
HerenciaEmbedding de structs (composición)
PolimorfismoInterfaces (implícitas, estructurales)
EncapsulaciónMayúscula/minúscula (exportado/no exportado)
Métodos estáticosFunciones de paquete
this/selfReceptor explícito (u *Usuario)
superLlamar al método del struct embebido
Abstract classInterfaz + constructor que devuelve la interfaz
OverloadingNo existe — nombres de función únicos
Excepcioneserror interface + panic/recover
GenéricosType parameters (Go 1.18+)

Ejemplo completo: sistema de medios

¿Qué es?

Un sistema de reproducción de medios que demuestra los conceptos OO de Go: interfaces (implícitas), embedding (composición), constructores, encapsulación y polimorfismo.

Sintaxis básica

package media

import (
    "fmt"
    "time"
)

// ── Tipos base ──

type FormatoVideo int

const (
    MP4 FormatoVideo = iota
    WEBM
    AVI
    MKV
)

type Metadata struct {
    Titulo         string
    DuracionSegs   int
    FechaCreacion  time.Time
    Tags           []string
}

func (m Metadata) DuracionFormateada() string {
    mins := m.DuracionSegs / 60
    segs := m.DuracionSegs % 60
    return fmt.Sprintf("%d:%02d", mins, segs)
}

func (m Metadata) String() string {
    return fmt.Sprintf("%s (%s)", m.Titulo, m.DuracionFormateada())
}

// ── Interfaz base ──

type Contenido interface {
    Reproducir() string
    Info() string
    Metadata() Metadata
    Reproducciones() int
}

// ── Implementación base embebible ──

type contenidoBase struct {
    meta       Metadata
    reproducciones int
}

func (c *contenidoBase) Reproducir() string {
    c.reproducciones++
    return "▶️ Reproduciendo " + c.meta.Titulo + "..."
}

func (c *contenidoBase) Metadata() Metadata {
    return c.meta
}

func (c *contenidoBase) Reproducciones() int {
    return c.reproducciones
}

// ── Tipos concretos con embedding ──

type Audio struct {
    *contenidoBase          // embedding
    BitrateKbps int
}

func NewAudio(meta Metadata, bitrate int) *Audio {
    return &Audio{
        contenidoBase: &contenidoBase{meta: meta},
        BitrateKbps:   bitrate,
    }
}

func (a *Audio) Info() string {
    return fmt.Sprintf("🎵 %s | %d kbps | %d reproducciones",
        a.meta.Titulo, a.BitrateKbps, a.reproducciones)
}

type Video struct {
    *contenidoBase
    Resolucion string
    Formato    FormatoVideo
    subtitulos bool
}

func NewVideo(meta Metadata, resolucion string, formato FormatoVideo) *Video {
    return &Video{
        contenidoBase: &contenidoBase{meta: meta},
        Resolucion:    resolucion,
        Formato:       formato,
    }
}

func (v *Video) Info() string {
    subs := "Sin subtítulos"
    if v.subtitulos {
        subs = "CC"
    }
    return fmt.Sprintf("🎬 %s | %s | %v | %s",
        v.meta.Titulo, v.Resolucion, v.Formato, subs)
}

func (v *Video) SetSubtitulos(activo bool) {
    v.subtitulos = activo
}

// ── Composición: lista de reproducción ──

type Playlist struct {
    Nombre string
    items  []Contenido
}

func NewPlaylist(nombre string) *Playlist {
    return &Playlist{Nombre: nombre, items: make([]Contenido, 0)}
}

func (p *Playlist) Agregar(item Contenido) {
    p.items = append(p.items, item)
}

func (p *Playlist) ReproducirTodo() []string {
    result := make([]string, len(p.items))
    for i, item := range p.items {
        result[i] = item.Reproducir()
    }
    return result
}

func (p *Playlist) InfoCompleta() []string {
    result := make([]string, len(p.items))
    for i, item := range p.items {
        result[i] = item.Info()
    }
    return result
}

// ── Interface de emisión (tipo estructural) ──

type Emisible interface {
    Reproducir() string
    Metadata() Metadata
}

func Emitir(c Emisible) {
    meta := c.Metadata()
    fmt.Printf("📡 Emitiendo: %s → %s\n", c.Reproducir(), meta.Titulo)
}

// ── Uso ──

func main() {
    cancion := NewAudio(
        Metadata{Titulo: "Bohemian Rhapsody", DuracionSegs: 354, Tags: []string{"rock", "clásico"}},
        320,
    )

    pelicula := NewVideo(
        Metadata{Titulo: "Inception", DuracionSegs: 8880, Tags: []string{"ciencia ficción"}},
        "4K", MKV,
    )
    pelicula.SetSubtitulos(true)

    // Polimorfismo: Info() cambia según tipo concreto
    for _, c := range []Contenido{cancion, pelicula} {
        fmt.Println(c.Info())
    }

    // Composición
    lista := NewPlaylist("Favoritos")
    lista.Agregar(cancion)
    lista.Agregar(pelicula)
    fmt.Println(lista.InfoCompleta())

    // Interfaz estructural
    Emitir(cancion)
}

Conceptos demostrados:

ConceptoCómo aparece
InterfazContenido, Emisible
Implementación implícita*Audio implementa Contenido sin declaración
Embedding*contenidoBase dentro de Audio y Video
ComposicionPlaylist contiene []Contenido
ConstructorNewAudio(), NewVideo()
Encapsulaciónsubtitulos minúscula, SetSubtitulos() mayúscula
PolimorfismoInfo() distinto comportamiento según tipo
MétodosReceiver (a *Audio)
Interfaz vacíaNo se usa — se prefieren interfaces específicas

🧪 Cómo probarlo

func TestSistemaMedios(t *testing.T) {
    cancion := NewAudio(
        Metadata{Titulo: "Test", DuracionSegs: 100}, 320,
    )
    if cancion.Info() == "" { t.Error("Info() no debería estar vacía") }

    pelicula := NewVideo(
        Metadata{Titulo: "Film", DuracionSegs: 5000}, "1080p", MP4,
    )
    if pelicula.Info() == "" { t.Error("Info() no debería estar vacía") }

    // Polimorfismo
    var c Contenido = cancion
    if _, ok := c.(*Audio); !ok { t.Error("debería ser *Audio") }

    // Playlist
    lista := NewPlaylist("Test")
    lista.Agregar(cancion)
    lista.Agregar(pelicula)
    if len(lista.ReproducirTodo()) != 2 { t.Error("debería tener 2 items") }
}

💡 Memoria y rendimiento

  • Embedding no añade overhead en memoria — es solo promoción de campos
  • Interfaces son 16 bytes (itable + puntero) — costo fijo
  • Constructores devuelven punteros para evitar copias
  • *contenidoBase embebido como puntero permite compartir estado

✅ Buenas prácticas

  • ✅ Usa interfaces para definir contratos de comportamiento
  • ✅ Usa embedding para reutilizar implementación
  • ✅ Constructores devuelven *T no T para consistency
  • ⚠️ No abuses de embedding profundo (>2 niveles)
  • ⚠️ El embedding no es herencia — no esperes polimorfismo de subtipos

🏗️ Metodología

  • Modelado OO en Go: interfaces → struct base embebido → structs concretos
  • Polimorfismo: las interfaces permiten tratar Audio y Video como Contenido
  • Composición: Playlist contiene Contenido — “tiene un” vs “es un”

🔗 Para saber más


Patrones de diseño con ejemplos

Strategy (interfaces)

¿Qué es?

El patrón Strategy permite cambiar el algoritmo de un objeto en tiempo de ejecución. En Go se implementa con interfaces: cada estrategia es un tipo que implementa la interfaz.

Sintaxis básica

type EstrategiaOrdenacion interface {
    Ordenar([]int) []int
}

type Ascendente struct{}
func (Ascendente) Ordenar(datos []int) []int {
    result := make([]int, len(datos))
    copy(result, datos)
    sort.Ints(result)
    return result
}

type Descendente struct{}
func (Descendente) Ordenar(datos []int) []int {
    result := make([]int, len(datos))
    copy(result, datos)
    sort.Sort(sort.Reverse(sort.IntSlice(result)))
    return result
}

type Mitades struct{}
func (Mitades) Ordenar(datos []int) []int {
    if len(datos) < 2 {
        return datos
    }
    mid := len(datos) / 2
    izq := make([]int, mid)
    der := make([]int, len(datos)-mid)
    copy(izq, datos[:mid])
    copy(der, datos[mid:])
    sort.Ints(izq)
    sort.Sort(sort.Reverse(sort.IntSlice(der)))
    return append(izq, der...)
}

type Analizador struct {
    estrategia EstrategiaOrdenacion
}

func (a *Analizador) Analizar(datos []int) []int {
    return a.estrategia.Ordenar(datos)
}

// Uso
a := Analizador{estrategia: Descendente{}}
fmt.Println(a.Analizar([]int{3, 1, 4, 1, 5, 9})) // [9, 5, 4, 3, 1, 1]

Builder (functional options)

¿Qué es?

El patrón Builder en Go se implementa idiomáticamente con functional options: cada opción es una función que modifica el struct. Es el estándar en librerías como gRPC, HTTP, etc.

Sintaxis básica

type Servidor struct {
    host    string
    port    int
    timeout time.Duration
    tls     bool
    logger  *log.Logger
}

type Option func(*Servidor)

func WithHost(host string) Option {
    return func(s *Servidor) { s.host = host }
}

func WithPort(port int) Option {
    return func(s *Servidor) { s.port = port }
}

func WithTimeout(t time.Duration) Option {
    return func(s *Servidor) { s.timeout = t }
}

func WithTLS() Option {
    return func(s *Servidor) { s.tls = true }
}

func NuevoServidor(opts ...Option) *Servidor {
    s := &Servidor{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
    }
    for _, opt := range opts {
        opt(s)
    }
    return s
}

// Uso
s := NuevoServidor(
    WithHost("example.com"),
    WithPort(443),
    WithTLS(),
    WithTimeout(10*time.Second),
)

Es el patrón estándar en librerías como gRPC, HTTP, etc.

Observer (canales)

¿Qué es?

El patrón Observer permite notificar a múltiples suscriptores cuando ocurre un evento. En Go se implementa con canales: los observers reciben notificaciones a través de un canal.

Sintaxis básica

type Observable struct {
    observers []chan<- any
    mu        sync.RWMutex
}

func (o *Observable) Conectar() (<-chan any, func()) {
    ch := make(chan any, 10)
    o.mu.Lock()
    o.observers = append(o.observers, ch)
    o.mu.Unlock()

    disconnect := func() {
        o.mu.Lock()
        defer o.mu.Unlock()
        for i, obs := range o.observers {
            if obs == ch {
                o.observers = append(o.observers[:i], o.observers[i+1:]...)
                break
            }
        }
        close(ch)
    }

    return ch, disconnect
}

func (o *Observable) Notificar(dato any) {
    o.mu.RLock()
    defer o.mu.RUnlock()
    for _, obs := range o.observers {
        select {
        case obs <- dato:
        default:
            // buffer lleno, descartar (non-blocking send)
        }
    }
}

// Uso
sensor := &Observable{}
ch, disconnect := sensor.Conectar()
defer disconnect()

go func() {
    for dato := range ch {
        fmt.Println("Recibido:", dato)
    }
}()

sensor.Notificar("lectura sensor: 23.5°C")

Factory (constructores)

¿Qué es?

El patrón Factory crea objetos sin exponer la lógica de creación al cliente. En Go se implementa con funciones constructoras que devuelven interfaces.

Sintaxis básica

type Parser interface {
    Parse(data []byte) (any, error)
}

type JSONParser struct{}
func (JSONParser) Parse(data []byte) (any, error) {
    var result any
    err := json.Unmarshal(data, &result)
    return result, err
}

type XMLParser struct{}
func (XMLParser) Parse(data []byte) (any, error) {
    var result any
    err := xml.Unmarshal(data, &result)
    return result, err
}

// Factory
func NewParser(format string) (Parser, error) {
    switch format {
    case "json":
        return JSONParser{}, nil
    case "xml":
        return XMLParser{}, nil
    default:
        return nil, fmt.Errorf("formato no soportado: %s", format)
    }
}

Factory con registro (más flexible):

var parsers = make(map[string]func() Parser)

func RegisterParser(format string, factory func() Parser) {
    parsers[format] = factory
}

func NewParser(format string) (Parser, error) {
    factory, ok := parsers[format]
    if !ok {
        return nil, fmt.Errorf("formato no registrado: %s", format)
    }
    return factory(), nil
}

func init() {
    RegisterParser("json", func() Parser { return JSONParser{} })
    RegisterParser("xml", func() Parser { return XMLParser{} })
}

Singleton (sync.Once)

¿Qué es?

El patrón Singleton asegura que una estructura se inicialice una sola vez. En Go se implementa con sync.Once.

Sintaxis básica

type Config struct {
    DBUrl  string
    Puerto int
}

var (
    config *Config
    once   sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        config = &Config{
            DBUrl:  os.Getenv("DATABASE_URL"),
            Puerto: 8080,
        }
    })
    return config
}

Worker pool (goroutines + canales)

¿Qué es?

El patrón Worker Pool mantiene un grupo fijo de goroutines que procesan trabajos desde una cola. Controla la concurrencia máxima y reutiliza goroutines.

Sintaxis básica

type Pool struct {
    jobs    chan func()
    wg      sync.WaitGroup
}

func NewPool(size int) *Pool {
    p := &Pool{
        jobs: make(chan func(), 100),
    }
    for i := 0; i < size; i++ {
        p.wg.Add(1)
        go p.worker(i)
    }
    return p
}

func (p *Pool) worker(id int) {
    defer p.wg.Done()
    for job := range p.jobs {
        log.Printf("Worker %d ejecutando job", id)
        job()
    }
}

func (p *Pool) Submit(job func()) {
    p.jobs <- job
}

func (p *Pool) Shutdown() {
    close(p.jobs)
    p.wg.Wait()
}

Middleware (http.Handler)

¿Qué es?

El patrón Middleware envuelve un http.Handler con funcionalidad adicional (logging, auth, rate limiting). En Go se implementa como una función que recibe y devuelve un http.Handler.

Sintaxis básica

type Middleware func(http.Handler) http.Handler

// Logging middleware
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// Autenticación middleware
func Autenticar(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        // validar token...
        next.ServeHTTP(w, r)
    })
}

// Chain (componer middlewares)
func Chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

// Uso
handler := Chain(
    miHandler,
    Logging,
    Autenticar,
    RateLimit,
)

Polimorfismo en detalle

Go tiene dos formas de polimorfismo: interfaces estructurales (duck typing en compilación) y genéricos (Go 1.18+).

1. Polimorfismo por interfaces

¿Qué es?

Cualquier tipo que implemente los métodos de una interfaz la satisface automáticamente. El dispatch es dinámico (vtable en runtime).

Sintaxis básica

type Animal interface {
    Sonido() string
}

type Perro struct{ Nombre string }
func (p Perro) Sonido() string { return "Guau" }

type Gato struct{ Nombre string }
func (g Gato) Sonido() string { return "Miau" }

type Coche struct{ Marca string }
func (c Coche) Sonido() string { return "Brum" }

func HacerSonido(a Animal) {
    fmt.Println(a.Sonido())
}

func main() {
    HacerSonido(Perro{}) // Guau
    HacerSonido(Gato{})  // Miau
    HacerSonido(Coche{}) // Brum — no es animal, pero implementa la interfaz
}

Type switch para polimorfismo condicional:

func Describir(a any) string {
    switch v := a.(type) {
    case Perro:
        return fmt.Sprintf("Perro llamado %s", v.Nombre)
    case Gato:
        return fmt.Sprintf("Gato llamado %s", v.Nombre)
    case Coche:
        return fmt.Sprintf("Coche marca %s", v.Marca)
    default:
        return fmt.Sprintf("Tipo desconocido: %T", v)
    }
}

2. Polimorfismo con genéricos (Go 1.18+)

¿Qué es?

Los genéricos permiten escribir código polimórfico en compilación (monomorfización) — el compilador genera código especializado por tipo.

Sintaxis básica

type Sumable interface {
    ~int | ~float64 | ~string
}

func Suma[S Sumable](vals ...S) S {
    var total S
    for _, v := range vals {
        total += v
    }
    return total
}

func main() {
    fmt.Println(Suma(1, 2, 3))                    // 6
    fmt.Println(Suma(1.5, 2.5, 3.0))              // 7.0
    fmt.Println(Suma("a", "b", "c"))               // "abc"
}

Cuándo usar cada uno:

SituaciónInterfacesGenéricos
Diferentes tipos, mismo comportamiento✅❌ (no necesitas)
Mismo código para cualquier tipo❌✅
Quieres almacenar tipos en una variable✅ var a Animal = Perro{}❌
Type switch sobre el tipo concreto✅ switch v.(type)❌
Funciones sobre slices/maps genéricos❌✅
Rendimiento crítico❌ (dynamic dispatch)✅ (monomorfización)

Multimedia e imágenes

Go no tiene una stdlib multimedia potente como Python (Pillow) o Node (Sharp). Sin embargo, cubre lo básico con image y hay librerías externas excelentes.

image — decodificar y codificar imágenes

¿Qué es?

El paquete image de la stdlib permite decodificar y codificar imágenes en formatos comunes (PNG, JPEG, GIF) mediante un sistema de drivers registrados.

Sintaxis básica

import "image"
import _ "image/png"  // registro del decoder PNG
import _ "image/jpeg" // registro del decoder JPEG
import _ "image/gif"  // registro del decoder GIF

f, _ := os.Open("imagen.png")
defer f.Close()

img, format, err := image.Decode(f) // format = "png", "jpeg", "gif"
fmt.Println("Formato:", format)
bounds := img.Bounds()
fmt.Printf("Dimensiones: %dx%d", bounds.Dx(), bounds.Dy())

image/color — colores

Sintaxis básica

import "image/color"

c := color.RGBA{R: 255, G: 0, B: 0, A: 255} // rojo opaco
// También: color.NRGBA (pre-multiplicado), color.Gray, color.CMYK

image/draw — dibujar sobre imágenes

¿Qué es?

El paquete image/draw permite componer imágenes: dibujar una imagen sobre otra, rellenar regiones, aplicar máscaras.

Sintaxis básica

import "image/draw"
import "image/png"

// Crear imagen en blanco
dst := image.NewRGBA(image.Rect(0, 0, 800, 600))
draw.Draw(dst, dst.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)

// Dibujar una imagen sobre otra
srcFile, _ := os.Open("logo.png")
src, _, _ := image.Decode(srcFile)
srcFile.Close()

offset := image.Pt(100, 50)
draw.Draw(dst, src.Bounds().Add(offset), src, image.Point{}, draw.Over)

// Guardar
out, _ := os.Create("resultado.png")
png.Encode(out, dst)
out.Close()

image/gif — crear GIFs animados

¿Qué es?

El paquete image/gif permite codificar y decodificar GIFs, incluyendo animaciones con múltiples frames y delays.

Sintaxis básica

import "image/gif"

var frames []*image.Paletted
var delays []int

for i := 0; i < 10; i++ {
    // crear frame...
    frames = append(frames, frame)
    delays = append(delays, 10) // 10 centésimas de segundo
}

f, _ := os.Create("animacion.gif")
gif.EncodeAll(f, &gif.GIF{
    Image: frames,
    Delay: delays,
})
f.Close()

golang.org/x/image — extensiones

Paquetes oficiales fuera de la stdlib pero mantenidos por el equipo Go:

  • golang.org/x/image/tiff — TIFF
  • golang.org/x/image/bmp — BMP
  • golang.org/x/image/webp — WebP decode
  • golang.org/x/image/font — renderizado de fuentes
  • golang.org/x/image/vector — gráficos vectoriales
go get golang.org/x/image

Librerías externas populares

LibreríaPara quéEnlace
ggRenderizado 2D (líneas, formas, texto, gradientes)github.com/fogleman/gg
go-ffmpegBindings para FFmpeg (procesamiento de video)github.com/m1k1o/go-ffmpeg
beepReproducción y procesamiento de audiogithub.com/gopxl/beep
otoAudio multiplataforma (sin dependencias externas)github.com/hajimehoshi/oto
gocvOpenCV bindings (visión artificial)github.com/hybridgroup/gocv
gographics/imagickImageMagick bindingsgithub.com/gographics/imagick
disintegration/imagingProcesamiento de imágenes de alto nivelgithub.com/disintegration/imaging
// gg — ejemplo de renderizado 2D
import "github.com/fogleman/gg"

dc := gg.NewContext(400, 300)
dc.SetRGB(1, 1, 1)
dc.Clear()
dc.SetRGB(0, 0, 0)
dc.SetLineWidth(4)
dc.DrawCircle(200, 150, 50)
dc.Stroke()
dc.SavePNG("circulo.png")

Fuente: Go dev — image, Go dev — image/png, Go dev — image/gif, golang.org/x/image, gg GitHub, beep GitHub, gocv GitHub


Bases de datos

SQL

¿Qué es?

Go tiene varias opciones para bases de datos SQL, desde la stdlib (database/sql) hasta ORMs completos (GORM) y drivers nativos (pgx).

LibreríaAsyncORMPara qué
database/sql (stdlib)NoNoDriver estándar, funciona con PostgreSQL, MySQL, SQLite
pgxSíNoDriver PostgreSQL nativo en Go. 5x más rápido que lib/pq
sqlxNoNoExtensión de database/sql con struct scanning, named params
GORMNoORM completoORM más popular. Migraciones automáticas, callbacks, preloading
bunSíORM ligeroSQLite/PostgreSQL/MySQL. Query builder fluido
jetNoQuery builderGenera tipos desde la BD, type-safe queries

Sintaxis básica

// sqlx — el punto medio entre stdlib y ORM
type Usuario struct {
    ID    int    `db:"id"`
    Nombre string `db:"nombre"`
    Email  string `db:"email"`
}

db, err := sqlx.Connect("postgres", "postgres://user:pass@localhost/db?sslmode=disable")

// Select — mapea automáticamente a struct
var usuarios []Usuario
db.Select(&usuarios, "SELECT * FROM usuarios")

// Get — una fila
var u Usuario
db.Get(&u, "SELECT * FROM usuarios WHERE id = $1", 1)

// Named queries
db.NamedExec("INSERT INTO usuarios (nombre, email) VALUES (:nombre, :email)", &u)

// In — queries con IN dinámico
query, args, _ := sqlx.In("SELECT * FROM usuarios WHERE id IN (?)", []int{1, 2, 3})
query = db.Rebind(query) // adapta $1, $2 según driver
db.Select(&usuarios, query, args...)
// pgx — PostgreSQL nativo, moderno y rápido
import "github.com/jackc/pgx/v5"

conn, err := pgx.Connect(ctx, "postgres://user:pass@localhost/db")

// Query simple
rows, err := conn.Query(ctx, "SELECT id, nombre FROM usuarios")
for rows.Next() {
    var id int
    var nombre string
    rows.Scan(&id, &nombre)
}

// pgxpool — connection pooling
pool, err := pgxpool.New(ctx, "postgres://user:pass@localhost/db")
pool.Config().MaxConns = 20

// Con pgx la interfaz database/sql también funciona:
db, err := sql.Open("pgx", "postgres://user:pass@localhost/db")
// GORM — ORM completo
import "gorm.io/gorm"
import "gorm.io/driver/postgres"

type Usuario struct {
    gorm.Model
    Nombre string `gorm:"size:100;not null"`
    Email  string `gorm:"uniqueIndex"`
    Edad   int
    Posts  []Post // HasMany
}

type Post struct {
    gorm.Model
    Titulo    string
    UsuarioID uint
    Usuario   Usuario // BelongsTo
}

db, _ := gorm.Open(postgres.Open("postgres://user:pass@localhost/db"), &gorm.Config{})

// AutoMigrate — crea/actualiza tablas
db.AutoMigrate(&Usuario{}, &Post{})

// CRUD
db.Create(&Usuario{Nombre: "Ana", Email: "ana@email.com"})
var u Usuario
db.First(&u, 1)                       // por ID
db.Where("nombre = ?", "Ana").First(&u)
db.Model(&u).Update("edad", 30)
db.Delete(&u)

// Preloading (eager loading)
var usuarios []Usuario
db.Preload("Posts").Find(&usuarios)

Migraciones

¿Qué es?

Gestionan cambios evolutivos del esquema de BD. golang-migrate/migrate es el estándar de facto.

Sintaxis básica

// golang-migrate/migrate — estándar de facto
import "github.com/golang-migrate/migrate/v4"
import _ "github.com/golang-migrate/migrate/v4/database/postgres"
import _ "github.com/golang-migrate/migrate/v4/source/file"
# CLI (instalar con brew o go install)
migrate create -ext sql -dir migrations -seq crear_usuarios

# migrations/000001_crear_usuarios.up.sql
CREATE TABLE usuarios (
    id SERIAL PRIMARY KEY,
    nombre TEXT NOT NULL,
    email TEXT UNIQUE NOT NULL
);

# migrations/000001_crear_usuarios.down.sql
DROP TABLE IF EXISTS usuarios;

# Ejecutar migraciones
migrate -path migrations -database "postgres://user:pass@localhost/db?sslmode=disable" up

Alternativas:

  • pressly/goose: migraciones en SQL o Go, soporte para embedding
  • golang-migrate/migrate: el más usado, soporta múltiples drivers y fuentes (archivos, S3, GitHub)

NoSQL

¿Qué es?

Go tiene drivers excelentes para bases de datos NoSQL: Redis y MongoDB son los más comunes.

Sintaxis básica

// Redis — go-redis/redis
import "github.com/redis/go-redis/v9"

rdb := redis.NewClient(&redis.Options{
    Addr:     "localhost:6379",
    Password: "",
    DB:       0,
})

// Set/Get
rdb.Set(ctx, "key", "value", 10*time.Minute)
val, _ := rdb.Get(ctx, "key").Result()

// Estructuras
rdb.HSet(ctx, "user:1", "nombre", "Ana", "email", "ana@email.com")
rdb.LPush(ctx, "queue", "job1")
rdb.SAdd(ctx, "tags", "golang", "redis")
rdb.ZAdd(ctx, "leaderboard", redis.Z{Score: 100, Member: "Ana"})

// Pub/Sub
pubsub := rdb.Subscribe(ctx, "canal")
for msg := range pubsub.Channel() {
    fmt.Println(msg.Payload)
}
// MongoDB — mongo-go-driver
import "go.mongodb.org/mongo-driver/mongo"
import "go.mongodb.org/mongo-driver/bson"

client, _ := mongo.Connect(ctx, options.Client().ApplyURI("mongodb://localhost:27017"))
db := client.Database("app")
coll := db.Collection("usuarios")

// Insert
coll.InsertOne(ctx, bson.M{"nombre": "Ana", "email": "ana@email.com"})

// Find
cursor, _ := coll.Find(ctx, bson.M{"nombre": "Ana"})
var usuarios []bson.M
cursor.All(ctx, &usuarios)

// FindOne + decode a struct
var u struct {
    Nombre string `bson:"nombre"`
    Email  string `bson:"email"`
}
coll.FindOne(ctx, bson.M{"_id": objectID}).Decode(&u)

Fuente: pgx docs, GORM docs, sqlx docs, go-redis docs, MongoDB Go Driver


WebSockets

gorilla/websocket

¿Qué es?

WebSockets permiten comunicación bidireccional en tiempo real entre cliente y servidor. gorilla/websocket es la librería más usada, madura y bien documentada.

LibreríaPara qué
gorilla/websocketLa más usada, madura y bien documentada
nhooyr.io/websocketModerna, más simple, mejor soporte para context
MelodyFramework sobre gorilla, sesiones, broadcast

Sintaxis básica

import "github.com/gorilla/websocket"

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool { return true }, // solo dev
}

type Hub struct {
    clients    map[*websocket.Conn]bool
    broadcast  chan []byte
    register   chan *websocket.Conn
    unregister chan *websocket.Conn
    mu         sync.RWMutex
}

func (h *Hub) Run() {
    for {
        select {
        case client := <-h.register:
            h.mu.Lock()
            h.clients[client] = true
            h.mu.Unlock()

        case client := <-h.unregister:
            h.mu.Lock()
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                client.Close()
            }
            h.mu.Unlock()

        case message := <-h.broadcast:
            h.mu.RLock()
            for client := range h.clients {
                client.WriteMessage(websocket.TextMessage, message)
            }
            h.mu.RUnlock()
        }
    }
}

func handleWebSocket(hub *Hub, w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }

    hub.register <- conn

    defer func() {
        hub.unregister <- conn
    }()

    for {
        _, msg, err := conn.ReadMessage()
        if err != nil {
            if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway) {
                log.Printf("error: %v", err)
            }
            break
        }
        hub.broadcast <- msg
    }
}

// En main()
hub := &Hub{
    clients:    make(map[*websocket.Conn]bool),
    broadcast:  make(chan []byte),
    register:   make(chan *websocket.Conn),
    unregister: make(chan *websocket.Conn),
}
go hub.Run()

http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
    handleWebSocket(hub, w, r)
})

nhooyr.io/websocket (moderna)

Sintaxis básica

import "nhooyr.io/websocket"

func handleWS(w http.ResponseWriter, r *http.Request) {
    conn, err := websocket.Accept(w, r, &websocket.AcceptOptions{
        InsecureSkipVerify: true,
    })
    if err != nil {
        log.Println(err)
        return
    }
    defer conn.Close(websocket.StatusInternalError, "connection closed")

    for {
        msgType, msg, err := conn.Read(r.Context())
        if err != nil {
            break
        }
        conn.Write(r.Context(), msgType, msg) // echo
    }
}

Fuente: gorilla/websocket docs, nhooyr.io/websocket


Concurrencia en detalle

Go tiene un modelo de concurrencia único: goroutines (hilos ligeros del runtime) + canales (comunicación tipada entre goroutines). La filosofía es: “no comuniques memoria compartida, comparte memoria comunicándote”.

Tabla de escenarios

¿Qué es?

Cada problema de concurrencia tiene una solución idiomática en Go. Esta tabla mapea escenarios comunes a su implementación recomendada.

EscenarioSolución Go
Lanzar N tareas independientesgo func() + sync.WaitGroup
Productor-consumidorCanal buffered
Fan-out (1 productor, N workers)Canal compartido, N goroutines
Fan-in (N productores, 1 consumidor)select o merge de canales
Timeout en operacióncontext.WithTimeout
Rate limitingCanal ticker o rate.Limiter
Pipeline de procesamientoCanales encadenados
Broadcast a N listenersCanal cerrado (todos reciben zero value)
Shared state protegidosync.Mutex o sync.RWMutex
Una sola inicializaciónsync.Once

Pipeline (canales encadenados)

¿Qué es?

Cada etapa del pipeline es una goroutine que recibe de un canal y envía a otro. Los datos fluyen como en una cadena de montaje.

Sintaxis básica

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

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

func main() {
    // Pipeline: nums → sq → sq → print
    for n := range sq(sq(gen(2, 3, 4))) {
        fmt.Println(n) // 16, 81, 256
    }
}

Fan-out / Fan-in con error handling

¿Qué es?

Distribuir trabajo entre múltiples workers (fan-out) y recolectar resultados (fan-in), con manejo de errores por job.

Sintaxis básica

type Result struct {
    Value int
    Err   error
}

func worker(ctx context.Context, id int, jobs <-chan int, results chan<- Result) {
    for job := range jobs {
        select {
        case <-ctx.Done():
            return
        default:
            // Simular trabajo que puede fallar
            if job == 42 {
                results <- Result{Err: fmt.Errorf("worker %d: job %d es prohibido", id, job)}
            } else {
                results <- Result{Value: job * 2}
            }
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    jobs := make(chan int, 10)
    results := make(chan Result, 10)

    // Fan-out: 3 workers
    for w := 1; w <= 3; w++ {
        go worker(ctx, w, jobs, results)
    }

    // Enviar jobs
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // Recibir resultados
    for r := 1; r <= 9; r++ {
        result := <-results
        if result.Err != nil {
            fmt.Println("Error:", result.Err)
        } else {
            fmt.Println("Resultado:", result.Value)
        }
    }
}

errgroup (cancelación al primer error)

¿Qué es?

errgroup extiende sync.WaitGroup con propagación de errores y cancelación: si una goroutine falla, todas las demás se cancelan automáticamente.

Sintaxis básica

import "golang.org/x/sync/errgroup"

g, ctx := errgroup.WithContext(context.Background())

urls := []string{
    "https://httpbin.org/delay/1",
    "https://httpbin.org/delay/2",
    "https://httpbin.org/status/500", // este falla
}

results := make([]string, len(urls))
for i, url := range urls {
    i, url := i, url // capturar variables del bucle
    g.Go(func() error {
        req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        resp, err := http.DefaultClient.Do(req)
        if err != nil {
            return err
        }
        defer resp.Body.Close()
        if resp.StatusCode >= 400 {
            return fmt.Errorf("URL %s: status %d", url, resp.StatusCode)
        }
        body, _ := io.ReadAll(resp.Body)
        results[i] = string(body)
        return nil
    })
}

// Espera a todas las goroutines — si una falla, cancela las demás
if err := g.Wait(); err != nil {
    fmt.Println("Error en goroutine:", err)
}

Ticker (rate limiting)

¿Qué es?

time.Ticker emite ticks a intervalos regulares, permitiendo limitar la tasa de operaciones.

Sintaxis básica

limiter := time.NewTicker(200 * time.Millisecond)
defer limiter.Stop()

for _, req := range requests {
    <-limiter.C // espera el tick
    go process(req)
}

Worker pool con control de errores

¿Qué es?

Un pool de workers fijo que procesa jobs con cancelación temprana al primer error.

Sintaxis básica

func ProcessJobs(ctx context.Context, jobs []Job, workers int) []Result {
    jobCh := make(chan Job, len(jobs))
    resultCh := make(chan Result, len(jobs))
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // Workers
    var wg sync.WaitGroup
    for i := 0; i < workers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobCh {
                select {
                case <-ctx.Done():
                    return
                default:
                    resultCh <- processJob(ctx, job)
                }
            }
        }()
    }

    // Enviar jobs
    for _, job := range jobs {
        jobCh <- job
    }
    close(jobCh)

    // Cerrar resultCh cuando todos terminen
    go func() {
        wg.Wait()
        close(resultCh)
    }()

    // Recoger resultados
    var results []Result
    for result := range resultCh {
        results = append(results, result)
        if result.Err != nil {
            cancel() // cancelar workers restantes
        }
    }
    return results
}

Concurrencia vs paralelismo

ConceptoConcurrenciaParalelismo
DefiniciónMúltiples tareas progresan al mismo tiempoMúltiples tareas se ejecutan simultáneamente
En GoGoroutines multiplexadas en threads del SOVarias goroutines ejecutándose en distintos cores
NúcleosNo necesita múltiples coresNecesita múltiples cores
GOMAXPROCSControla cuántos threads del SO usa el runtimeruntime.GOMAXPROCS(runtime.NumCPU())
// Paralelismo real: usa todos los cores
import "runtime"

func main() {
    runtime.GOMAXPROCS(runtime.NumCPU()) // por defecto ya lo hace Go
    // Ahora las goroutines pueden ejecutarse en paralelo real
}

Fuente: Go dev — Concurrency, Go Concurrency Patterns (Rob Pike), errgroup docs


Testing y calidad

testing package y table-driven tests

¿Qué es?

Go tiene el paquete testing en la stdlib. Los tests se escriben en archivos *_test.go con funciones TestXxx(t *testing.T). Go promueve table-driven tests (TDT) como patrón estándar.

🔗 Go dev — testing package

Sintaxis básica

// archivo: calculadora_test.go
package main

import "testing"

func TestSuma(t *testing.T) {
    result := suma(2, 3)
    expected := 5
    if result != expected {
        t.Errorf("suma(2, 3) = %d; esperado %d", result, expected)
    }
}

// Table-driven test
func TestDivide(t *testing.T) {
    tests := []struct {
        name     string
        a, b     float64
        expected float64
        wantErr  bool
    }{
        {"división exacta", 10, 2, 5, false},
        {"división inexacta", 7, 2, 3.5, false},
        {"división por cero", 10, 0, 0, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            result, err := divide(tt.a, tt.b)
            if (err != nil) != tt.wantErr {
                t.Errorf("error = %v, wantErr = %v", err, tt.wantErr)
            }
            if result != tt.expected {
                t.Errorf("result = %v, esperado %v", result, tt.expected)
            }
        })
    }
}

🧪 Cómo probarlo

go test ./...                    # Todos los tests
go test -v ./...                 # Verboso
go test -run TestSuma ./...      # Filtra por nombre
go test -bench . ./...           # Benchmarks
go test -cover ./...             # Cobertura

💡 Memoria y rendimiento

  • go test compila y ejecuta en el mismo proceso — rápido
  • -bench ejecuta benchmarks con b.N iteraciones automáticas
  • Subtests (t.Run) se ejecutan en paralelo si usas t.Parallel()

✅ Buenas prácticas

  • ✅ Usa table-driven tests para múltiples casos
  • ✅ Los nombres de archivos test deben terminar en _test.go
  • ⚠️ No uses if para errores de test — usa t.Error/t.Fatal
  • ❌ No dependas del orden de ejecución — cada test es independiente

🏗️ Metodología

  • Unit tests: un archivo _test.go por cada archivo .go
  • Integration tests: usa build tag //go:build integration
  • Fuzzing: go test -fuzz (Go 1.18+)
  • Coverage: go test -coverprofile=coverage.out

🔗 Para saber más

Testify (assertions)

¿Qué es?

Testify es la librería de assertions más popular para Go. Proporciona assert y require con mensajes descriptivos y suite para tests agrupados.

🔗 Testify GitHub

Sintaxis básica

import "github.com/stretchr/testify/assert"

func TestConTestify(t *testing.T) {
    result := suma(2, 3)
    assert.Equal(t, 5, result, "suma(2, 3) debe ser 5")
    assert.NotNil(t, result)
    assert.Greater(t, result, 0)
}

// require detiene el test si falla
import "github.com/stretchr/testify/require"

func TestConRequire(t *testing.T) {
    result, err := divide(10, 0)
    require.Error(t, err)  // Detiene aquí si no hay error
    assert.Equal(t, 0.0, result)
}

🧪 Cómo probarlo

func TestTestify(t *testing.T) {
    assert.Equal(t, 4, suma(2, 2))
    assert.NotEqual(t, 5, suma(2, 2))
    assert.True(t, suma(2, 2) > 0)
}

💡 Memoria y rendimiento

  • Testify usa reflection para comparar — ligero overhead frente a if manual
  • require llama a t.FailNow() internamente — detiene el goroutine actual
  • Las suites (testify/suite) añaden estructura pero no overhead significativo

✅ Buenas prácticas

  • ✅ Usa assert para la mayoría de casos (no detiene el test)
  • ✅ Usa require cuando no tenga sentido continuar tras el fallo
  • ⚠️ No mezcles t.Error con testify — elige uno

🏗️ Metodología

  • Assertions simples: assert.Equal, assert.NotNil, assert.Error
  • Colecciones: assert.ElementsMatch, assert.Subset
  • HTTP: assert.HTTPStatusCode, assert.HTTPBody
  • Suites: testify/suite para agrupar tests con Setup/Teardown

🔗 Para saber más

Cobertura y bench

¿Qué es?

Go incluye herramientas para medir cobertura de código y rendimiento de benchmarks sin librerías externas.

Sintaxis básica

# Cobertura con HTML
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out

# Benchmarks
go test -bench=. -benchmem ./...

💡 Memoria y rendimiento

  • -cover instrumenta el código añadiendo contadores — overhead ~5-10% en tiempo de test
  • -benchmem mide alocaciones por operación (allocations/op)
  • Los resultados de bench son estables con -count=5 para promediar

✅ Buenas prácticas

  • ✅ Apunta a >80% de cobertura en código crítico
  • ✅ Benchmarks con b.ResetTimer() si hay setup costoso
  • ⚠️ La cobertura no mide calidad — solo qué código se ejecutó

🏗️ Metodología

  • Cobertura: go test -coverprofile=coverage.out → CI gate
  • Benchmarks: compara versiones con benchstat (golang.org/x/perf)
  • Profiling: go test -cpuprofile=cpu.out -memprofile=mem.out

Conceptos avanzados

go:generate

go:generate ejecuta comandos arbitrarios desde comentarios en el código. Se usa para generar código, protobuf, mocks, etc.

//go:generate protoc --go_out=. ./api/proto/*.proto
//go:generate stringer -type=Status
//go:generate mockgen -source=repo.go -destination=mock_repo.go

// Ejecutar:
// go generate ./...

Pprof (profiling)

Go tiene profiling integrado con pprof. Ideal para diagnosticar CPU, memoria, goroutines y bloqueos.

import _ "net/http/pprof"

func main() {
    go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
    // Abrir: http://localhost:6060/debug/pprof/
}

// Línea de comandos:
// go tool pprof http://localhost:6060/debug/pprof/heap
// top, list, web

Cgo

cgo permite llamar a código C desde Go. Útil para usar librerías nativas, pero añade complejidad y penalización de rendimiento.

/*
#include <stdlib.h>
*/
import "C"
import "unsafe"

func main() {
    cstr := C.CString("Hola desde C")
    defer C.free(unsafe.Pointer(cstr))
}

WASM

Go compila a WebAssembly. Ideal para aplicaciones web que necesitan lógica Go en el navegador.

GOOS=wasip1 GOARCH=wasm go build -o main.wasm main.go

Govulncheck

Detecta vulnerabilidades conocidas en dependencias Go.

go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./...

Proyecto final integrador

CLI de gestión de tareas

¿Qué es?

Una CLI que combina archivos (os, encoding/json), flags (flag), structs y tests. Integra todos los conceptos de la guía en un proyecto práctico.

🧪 Cómo probarlo

go mod init taskcli
go test ./...

Sintaxis básica

package main

import (
    "encoding/json"
    "flag"
    "fmt"
    "os"
    "time"
)

type Task struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Done      bool      `json:"done"`
    CreatedAt time.Time `json:"created_at"`
}

type TaskManager struct {
    tasks  []Task
    nextID int
    file   string
}

func NewTaskManager(file string) *TaskManager {
    tm := &TaskManager{file: file, nextID: 1}
    data, _ := os.ReadFile(file)
    json.Unmarshal(data, &tm.tasks)
    if len(tm.tasks) > 0 {
        tm.nextID = tm.tasks[len(tm.tasks)-1].ID + 1
    }
    return tm
}

func (tm *TaskManager) Add(title string) Task {
    t := Task{ID: tm.nextID, Title: title, CreatedAt: time.Now()}
    tm.nextID++
    tm.tasks = append(tm.tasks, t)
    tm.save()
    return t
}

func (tm *TaskManager) save() {
    data, _ := json.MarshalIndent(tm.tasks, "", "  ")
    os.WriteFile(tm.file, data, 0644)
}

func main() {
    add := flag.String("add", "", "Título de la tarea")
    flag.Parse()

    tm := NewTaskManager("tasks.json")

    if *add != "" {
        task := tm.Add(*add)
        fmt.Printf("✅ Tarea #%d creada: %s\n", task.ID, task.Title)
    }
}

Despliegue a producción

Build para producción

go build -ldflags="-s -w" -o app ./cmd/app

El binario resultante pesa ~15 MB y no requiere dependencias externas.

Docker multi-stage (scratch)

FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o app ./cmd/app

FROM scratch
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]

La imagen final ocupa ~15 MB — solo el binario estático.

Hosting

  • Fly.io — compatible con Dockerfile, fly launch
  • Railway — detecta Go automáticamente
  • DigitalOcean — App Platform con Docker o binario

CI/CD (GitHub Actions)

name: Deploy
on:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: "1.23"
      - run: go test ./...
  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions@1.5
        with:
          args: "deploy"

Hacks y tips de productividad

1. go generate — generación de código

//go:generate stringer -type=Estado
type Estado int

const (
    Pendiente Estado = iota
    Activo
    Inactivo
)
go install golang.org/x/tools/cmd/stringer@latest
go generate ./...  # genera estado_string.go con método String()

2. //go:embed — incrustar archivos en el binario (Go 1.16+)

import "embed"

//go:embed templates/*.html
//go:embed static/*
var assets embed.FS

func main() {
    data, _ := assets.ReadFile("templates/index.html")
    fmt.Println(string(data))

    // Servir archivos embedded
    http.Handle("/static/", http.FileServer(http.FS(assets)))
}

3. slog — logging estructurado nativo (Go 1.21+)

import "log/slog"

func main() {
    // JSON logger
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }))
    slog.SetDefault(logger)

    slog.Info("servidor iniciado",
        "puerto", 8080,
        "env", "production",
    )
    slog.Error("error de conexión",
        "error", err,
        "intento", 3,
    )
}

4. Profiling con pprof

import _ "net/http/pprof" // registra handlers en /debug/pprof/

func main() {
    go func() {
        http.ListenAndServe("localhost:6060", nil) // servidor de profiling
    }()
    // ... tu aplicación
}
go tool pprof http://localhost:6060/debug/pprof/heap  # heap profile
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30  # CPU profile
go tool pprof -http=:8081 ~/pprof/pprof.samples.cpu.001.pb.gz  # UI web

5. Race detector

go test -race ./...      # detecta data races en tests
go build -race .         # compila con race detector

El race detector es una herramienta de desarrollo. Los binarios con -race son más lentos y usan más memoria. Nunca en producción.

6. Fuzzing (Go 1.18+)

func FuzzDividir(f *testing.F) {
    testcases := []struct {
        a, b float64
    }{
        {10, 2},
        {0, 5},
        {-1, 3},
    }
    for _, tc := range testcases {
        f.Add(tc.a, tc.b)
    }

    f.Fuzz(func(t *testing.T, a, b float64) {
        result, err := Dividir(a, b)
        if err == nil && b == 0 {
            t.Errorf("se esperaba error para división por cero")
        }
        if err != nil && b != 0 {
            t.Errorf("error inesperado para %f / %f: %v", a, b, err)
        }
        if err == nil && !math.IsInf(result, 0) {
            _ = result
        }
    })
}
go test -fuzz=FuzzDividir -fuzztime=30s

7. any vs interface{}

Son exactamente lo mismo. any es un alias introducido en Go 1.18. Usa any en código nuevo (más corto, más legible).

// Antes (Go < 1.18)
var x interface{}

// Después (Go 1.18+)
var x any

8. net/http/httptest para tests HTTP

func TestHandler(t *testing.T) {
    ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hola")
    }))
    defer ts.Close()

    resp, err := http.Get(ts.URL)
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    if string(body) != "Hola\n" {
        t.Errorf("esperado 'Hola\\n', got %q", body)
    }
}

9. singleflight — evitar duplicación de peticiones

import "golang.org/x/sync/singleflight"

var sf singleflight.Group

func fetchData(key string) (any, error) {
    result, err, _ := sf.Do(key, func() (any, error) {
        // Solo una goroutine ejecuta esto
        return fetchFromDB(key)
    })
    return result, err
}

10. golangci-lint — linter multi-herramienta

# Instalación
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Ejecutar
golangci-lint run ./...

# .golangci.yml
linters:
  enable:
    - errcheck
    - govet
    - staticcheck
    - gosec
    - revive

11. Cross-compile inteligente

# Compilar para cualquier plataforma desde cualquier máquina
GOOS=linux GOARCH=arm64 go build -o app-linux-arm64 .
GOOS=windows GOARCH=amd64 go build -o app-windows-amd64.exe .
GOOS=darwin GOARCH=amd64 go build -o app-darwin-amd64 .

Go hace cross-compile nativo. No necesitas Docker ni VMs. CGO_ENABLED=0 para binarios estáticos sin dependencias C.

Fuente: Go dev — blog, Go dev — pprof, Go dev — fuzzing


Canales y recursos en español

YouTube

  • Midulive — Go, desarrollo backend, Docker, Kubernetes. Explicaciones claras y proyectos prácticos. Canal
  • MoureDev — Go, Rust, automatización, proyectos fullstack con explicaciones detalladas. También hace directos en Twitch. Canal
  • Fazt — Go desde cero, APIs REST, Gin, MongoDB. Tutoriales cortos y directos. Canal
  • Código Kotlin — Canal variado con Go, Rust, Kotlin y desarrollo backend. Buenas explicaciones de concurrencia. Canal
  • OpenWebinars — Cursos formales de Go con certificados. Desde fundamentos hasta despliegue. OpenWebinars Go

Comunidades

  • r/golang (Reddit) — Comunidad oficial de Go en inglés, muy activa, con threads de preguntas y proyectos
  • Gophers en Español — Grupos de Telegram y Discord para hablar de Go en español
  • Go Spain (gospain.org) — Comunidad española de Go, meetups, eventos
  • Stack Overflow en español — Etiqueta go para dudas técnicas

Repositorios de código

Blogs y newsletters

Fuente: búsqueda en YouTube y Reddit, listas curadas de Awesome Go


Referencias y documentación oficial

Fuentes de esta guía

Esta guía se ha elaborado a partir de las siguientes fuentes consultadas durante su desarrollo:


⚠️ Errores comunes

Estos son los errores que más se repiten al escribir tus primeros programas en Go. Casi todos tienen una causa concreta y una solución rápida.

ErrorCausaSolución
undefined: fmt / ./main.go:7:2: undefined: fmtUsas fmt.Println pero no lo importasteAñade import "fmt" entre los imports
undefined: XLa función o variable X no existe en ese ámbito o paqueteComprueba el nombre y que esté exportada (mayúscula) o disponible
cannot use X (type string) as type int in argumentPasa un string a una función que espera un intConvierte el valor: int(variable) o strconv.Atoi(). Go no hace coerción implícita
go: go.mod file not foundEjecutas go get, go run o go build fuera de un móduloCrea el módulo con go mod init <nombre> en la raíz del proyecto
command-line-arguments con go runEjecutas go run sobre un paquete sin func main o con variasCrea o localiza el fichero con package main y func main()
dial tcp: lookup ...: no such host al descargar dependenciasProblema de red o DNS al go get / go mod downloadRevisa la conexión, go env -w GO111MODULE=on y prueba GOPROXY o el proxy por defecto proxy.golang.org
GO111MODULE obsoleto después de go runVariables de entorno de versiones antiguasgo env -w GO111MODULE=on (o auto) y reapunta GOPATH
linker 'cc' not found en Windows/CGOFalta un compilador C para enlazarInstala un compilador C (MinGW/GCC) o desactiva CGO con CGO_ENABLED=0

🔗 Guías relacionadas


🏆 Retos Relacionados

Pon a prueba lo aprendido con estos desafíos:

COMPARTIR:
ETIQUETADO EN:
COMENTARIOS:

📋 Contenido