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)
gofmtcomo 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
- Descarga el instalador MSI desde go.dev/dl
- El instalador añade Go al PATH automáticamente
- Verifica en PowerShell:
go version - 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 buildogo run. Tipado estático: no puedes sumarintyfloat64sin 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 deferpara 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).
appendpuede reasignar el slice si no cabe. Los maps devuelven zero value si la clave no existe — usaval, ok := m["key"]. Los strings son inmutables y UTF-8.rangesobre 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:
errorinterface,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()functionsencoding/json: marshal/unmarshal structs, tagsjson:- 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).
nilen una interfaz no es lo mismo que un puntero nil. La interfaz vacíainterface{}(oany) es el “tope” del sistema de tipos. Go no tienetry/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.Helpergo test -benchbenchmarksgo test -racedetector de data racesgo mod,go mod tidy,go get, versionado semánticonet/http: handlers,http.HandlerFunc,http.ServeMux, middlewarecontext.Context: WithCancel, WithTimeout, WithDeadline, WithValuetime:time.Now,time.After,time.Ticker,time.Sleepsync: 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 unhttp.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,godotenvoviper 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.Contextoecho.Contextque 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.NotifyContextpara graceful shutdown.errgrouppara 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).GOMAXPROCSpor 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-gooconfluent-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
pluginpackage 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
| Aspecto | GOPATH (obsoleto) | Módulos (actual) |
|---|---|---|
| Ubicación | $GOPATH/src/... | Cualquier directorio |
| Dependencias | go get global | Versionadas en go.mod |
| Versiones | master siempre | SemVer fijo, reproducible |
| Herramienta | dep/govendor | go mod nativo |
| Reproducibilidad | No | Sí (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
| Ausencia | Consecuencia |
|---|---|
| Herencia | No hay extends. Se usa composición con embedding |
| Excepciones | No hay try/catch. Los errores son valores (error interface) |
| Genéricos (hasta 1.18) | Había que usar interface{} / any o generar código |
| Sobrecarga | No puedes tener dos funciones con el mismo nombre |
| Operadores personalizados | No hay sobrecarga de operadores |
| Herencia múltiple | Ni falta que hace con interfaces implícitas |
| Aritmética de punteros | Los 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.
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/uintson tamaño de palabra nativa (64 bits en x86-64) — óptimo para la CPUbyteyruneocupan 1 y 4 bytes respectivamentestringes un descriptor de 16 bytes (puntero + longitud) — el contenido está en heapnilen slices/maps/channels evita alocar hasta que se usan
✅ Buenas prácticas
- ✅ Usa
intpor defecto para enteros (noint8/int16a menos que ahorres memoria) - ✅ Usa
float64por defecto para flotantes - ⚠️ No confundas
byte(uint8) conrune(int32) - ❌ No accedas a campos de un
*structque esnil— panic
🏗️ Metodología
- APIs: usa
intyfloat64para valores numéricos - Serialización:
int/float64están optimizados en encoding/json - Juegos/embebido: usa
int8/int16/float32para 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.
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
varpara 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.
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
anyimplica 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
anya menos que sea estrictamente necesario — pierdes type safety - ❌ No compares slices directamente con
==(solo connil) — usareflect.DeepEqualoslices.Equal
🏗️ Metodología
- Datos inmutables: structs por valor
- Colecciones dinámicas: slices (no arrays de tamaño fijo)
- Lookups: maps con
okidiom - Errores: siempre devuelve
errorcomo ú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.
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
rangepara iterar strings (itera por runa, no por byte) - ✅ Usa
strings.Builderpara 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.
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.Poolreusa 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.
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
defertiene overhead mínimo (~50ns) — usa el patrón de cleanup siemprefor rangesobre slices es tan rápido como indexación manualswitchcompila a jump table cuando los casos son constantespanic/recoverno deben usarse para flujo normal — son costosos
✅ Buenas prácticas
- ✅ Usa
deferpara todo cleanup (cerrar archivos, unlock mutex, cerrar BD) - ✅ Usa
for rangepara iterar slices, maps, canales y strings - ⚠️ No uses
panicpara errores ordinarios — usaerror - ❌ No abuses de
recover— solo en servidores HTTP para evitar caídas
🏗️ Metodología
- Errores predecibles:
if err != nilidiom - Errores irrecuperables:
log.Fatalen main,panicen init - Cleanup: siempre
deferinmediatamente después de abrir recurso - Múltiples condiciones:
switchsin 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.
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
errorcomo ú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.
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, nousersouser_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
utilsocommon
🔗 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.
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 confunc 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.mody se cachean internalno 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
mainno es importable por otros paquetes
✅ Buenas prácticas
- ✅ Nombra paquetes en singular y cortos (
user, nousersouser_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
utilsocommon
🏗️ 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 mainmí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.
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
intes tamaño de palabra nativa (64 bits en x86-64) — óptimo para la CPUbyteyruneocupan 1 y 4 bytes respectivamentestringes un descriptor de 16 bytes (puntero + longitud) — contenido en heapnilen slices/maps/channels evita alocar hasta que se usaniotase evalúa en compilación — zero overhead en runtime
✅ Buenas prácticas
- ✅ Usa
:=dentro de funciones para código conciso - ✅ Usa
varpara valores zero explícitos (var i int) - ⚠️ No confundas
byte(uint8) conrune(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:
iotapara secuencias, bitsets con1 << iota - APIs: usa
intyfloat64para 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.
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 (
<64bytes) - ⚠️ 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/envpara 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.
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() stringerror: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{}/anytiene el mismo costo que cualquier otra interfaz- Las type assertions con
okevitan 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
anypara valores genéricos no tipados (JSON, logging) - ⚠️ No declares interfaces en el paquete que las consume — decláralas donde se usan
- ⚠️ Type assertion sin
okpuede 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.ReadWritercombinaio.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.
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:
- No puedes usar operadores en tipos genéricos sin constraints (excepto
==y!=concomparable) - No puedes hacer
switch v.(type)directamente (necesitas reflexión) - Type inference no siempre funciona con tipos complejos
- 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
anyen genéricos evita boxing (a diferencia deinterface{})- 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
comparablepara funciones que necesitan==/!= - ✅ Usa
anycomo 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 usesslices/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í.
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?
| Escenario | Recomendación |
|---|---|
| Struct grande (>64 bytes) | Puntero — evita copiar |
| Método que modifica el receptor | Puntero — func (u *Usuario) SetNombre() |
| Campo nil permitido | Puntero — indica ausencia |
| Canal, slice, map | Siempre referencia (ya son punteros internamente) |
| Enteros, booleanos, flags | Valor — los punteros añaden overhead |
| Strings | Valor — 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
nilantes 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
*Tpara 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.
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ón | Ejemplo | Cuándo |
|---|---|---|
| Sentinel | var ErrNotFound = errors.New("not found") | Error simple y conocido |
| Custom type | type ErrorValidacion struct | Lleva datos adicionales |
| Opaque error | Solo err != nil | Al 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
errores una interfaz de 16 bytes (itable + puntero)errors.Newaloca una vez — reutiliza la misma stringfmt.Errorf("%w")aloca un nuevo wrapping error cada vezerrors.Isyerrors.Asrecorren la cadena — O(n) en número de wrapserrors.Joinevita anidar múltiples niveles — eficiente para batches
✅ Buenas prácticas
- ✅ Siempre checkea
err != nil— es idiomático en Go - ✅ Usa
%wpara wrapping con intención de desenvolver después - ✅ Usa
errors.Ispara sentinel errors,errors.Aspara custom types - ⚠️ No uses
_ = errpara 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.Newpara errores simples y conocidos - Capas: cada capa añade contexto con
%wsin 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.
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
defertiene overhead pequeño (~50ns por llamada en Go 1.14+)deferen un closure captura variables por referencia — cuidado con buclespanic/recovertienen 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
deferpara cleanup (cerrar archivos, liberar mutexes, cerrar conexiones) - ✅ Usa
panicsolo para bugs o estados imposibles - ✅ Usa
recoveren el borde del programa (middleware HTTP, goroutines main) - ⚠️ No uses
panicpara errores esperables (IO, validación, red) - ❌ No uses
recoverpara control de flujo normal
🏗️ Metodología
- Cleanup:
defer f.Close()justo después deos.Open - Middleware HTTP:
recoveren el handler más externo para evitar crashes - Workers:
recoveren cada goroutine para aislar fallos - Inicialización:
panicsi 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.
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
selectcondefaultes 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
rangepara consumir canales hasta que se cierren - ✅ Usa
selectcontime.Afterpara timeouts - ⚠️ No uses
time.Sleeppara sincronizar --- usa canales osync.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 → collectorcon canales - Workers: fan-out con workers fijos, fan-in con resultados
- Timeouts:
selectcontime.Afteren 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”.
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:
- El context es siempre el primer parámetro de una función
- Nunca almacenes un context en un struct — pásalo como argumento
- Siempre llama a
cancel()cuando crees un context cancelable - Usa
context.Background()para el root,context.TODO()cuando aún no sabes qué usar - No uses
context.WithValuepara 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()ycontext.TODO()son singletons — zero allocWithCancel/WithTimeoutalocan un nuevo contexto cada vezWithValueusa una linked list interna — O(n) en búsquedactx.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()condeferpara evitar leaks - ✅ Usa
context.WithTimeoutpara operaciones con límite de tiempo - ⚠️ No almacenes context en structs — pásalo como argumento
- ⚠️ No uses
WithValuecomo 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.
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.Mutexes ligero (~8 bytes),sync.RWMutexun poco másMutex.Lock()con contención hace system calls — caro pero necesariosync.Onceusa atomic operations internamente — overhead mínimosync.WaitGroupusa atomics + futex — eficiente para cientos de goroutinessync.Maptiene overhead interno pero brilla en lecturas sin contención- Siempre usa
deferpara unlock — previene deadlocks si hay panic
✅ Buenas prácticas
- ✅ Usa
defer c.mu.Unlock()justo después deLock() - ✅ Prefiere
sync.RWMutexcuando hay más lecturas que escrituras - ✅ Usa
sync.Oncepara inicialización perezosa - ⚠️ No copies un
sync.Mutex— pasa siempre por puntero - ⚠️ No uses
sync.Mappara casos generales —map + RWMutexsuele 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.Poolpara 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í.
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.Copyusa un buffer interno de 32KB — copia eficiente entre streamsbytes.Buffercrece dinámicamente como un slice de bytesstrings.NewReaderno aloca — es un wrapper sobre un string existenteio.Pipees síncrono — bloquea hasta que el otro lado lee/escribebufio.Reader/bufio.Writerañaden buffering para reducir system calls
✅ Buenas prácticas
- ✅ Usa
io.Copyen vez de leer todo a memoria y luego escribir - ✅ Usa
io.ReadAllsolo cuando necesitas todo el contenido en memoria - ✅ Usa
io.LimitReaderpara prevenir lecturas ilimitadas de fuentes no confiables - ⚠️ Siempre verifica el error de
Read(especialmenteio.EOF) - ❗ Si
Readdevuelven > 0conerr == io.EOF, losnbytes son válidos
🏗️ Metodología
- Streaming:
io.Copypara transferencias grandes entre reader y writer - Middleware:
io.TeeReaderpara loggear mientras se procesa - Testing:
bytes.Buffercomo writer/reader en tests - HTTP:
http.ResponseWriteres unio.Writer,r.Bodyes unio.Reader - Compresión:
gzip.Reader/gzip.Writerenvuelven 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.
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.Marshalusa reflection — más lento que serialización manual pero más cómodojson.Decoderes eficiente para streams (lee en chunks, no carga todo)json.RawMessageevita re-serializar datos que ya están en JSONjson.Encoder/Decoderson 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.NewEncoderpara 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.MarshalIndentpara archivos de configuración legibles - Flexible:
json.RawMessagepara 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.
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.Openno conecta inmediatamente —db.Ping()fuerza la conexiónSetMaxOpenConnslimita 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.SetMaxOpenConnsydb.SetMaxIdleConnsapropiadamente - ✅ Usa
$1,$2(PostgreSQL) o?(MySQL/SQLite) para parámetros — nunca interpolación - ⚠️ No olvides el
_en el import del driver para queinit()se ejecute - ⚠️
LastInsertId()no es soportado por todos los drivers (PostgreSQL usaRETURNING) - ❌ No compartas
*sql.DBentre goroutines sin sincronización — el pool ya es thread-safe
🏗️ Metodología
- Queries:
QueryContextcon context para cancelación - Transacciones cortas: mantenlas breves para no bloquear el pool
- Migrations: librerías como
golang-migrateopressly/goose - Testing: SQLite en memoria o
testcontainerspara integración real - ORM: GORM o sqlx para proyectos complejos;
database/sqlpara 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.
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 testcompila cada paquete y ejecuta los tests en binarios separados-count=1desactiva caché de tests-raceañade overhead (~5-10x más lento) pero detecta data races-benchmemmuestra 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/NewRecorderpara tests HTTP - ⚠️ No uses
testingen código de producción - ⚠️
t.Fatal/t.FailNowdetienen 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.
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.ReadFilecarga todo el archivo en memoria — peligroso para archivos >100 MBos.File.Read(buf)con buffer propio permite streaming controladoos.ReadDires más eficiente queioutil.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 deos.Open/os.Create - ✅ Usa
os.ReadFile/os.WriteFilepara archivos pequeños (menos de 10 MB) - ⚠️ No uses
ioutil(obsoleto desde Go 1.16) — usaosyio - ❌ 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.ReadDirpara listar,filepath.Walkpara recorrer árbol - Temporales:
t.TempDir()en tests,os.CreateTempen 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.
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.Scannerlimita línea a 64KB por defecto (ajustable conScanner.Buffer())- Sin buffer: cada
Readde 1 byte es una syscall — con buffer se reduce 4096x bufio.Writeracumula escrituras hastaFlush()— mejora rendimiento en archivos
✅ Buenas prácticas
- ✅ Usa
bufio.Scannerpara leer archivos línea por línea (es lo más idiomático) - ✅ Usa
bufio.NewWriterpara escritura intensiva (reduce syscalls) - ⚠️ No olvides
Flush()— el writer con buffer no vacía automáticamente - ❌ No uses
ReadString('\n')mezclado conScanneren el mismo archivo
🏗️ Metodología
- Archivos de texto:
bufio.Scanner(línea por línea) - Archivos binarios:
bufio.NewReaderconReadde fragmentos - Logging intensivo:
bufio.NewWriterconFlushperiódico - Red:
bufio.NewReader/NewWriterpara 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.
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.Copyusa un buffer interno de 32KB — eficiente para copias grandesio.ReadAllcarga todo en memoria — O(n) en RAMio.MultiWriterescribe secuencialmente — el más lento determina la velocidad- Las interfaces
Reader/Writerno tienen overhead de reflection — son tipos concretos
✅ Buenas prácticas
- ✅ Escribe funciones que acepten
io.Reader/io.Writeren lugar de*os.File - ✅ Usa
io.Copyen lugar de leer todo y luego escribir - ⚠️ No uses
io.ReadAllpara archivos grandes - ❌ No implementes
Read/Writemanualmente sin manejario.EOF
🏗️ Metodología
- Funciones reutilizables: acepta
io.Reader/io.Writer, no tipos concretos - Tests: usa
strings.NewReaderobytes.Buffercomo mock de archivos - Streaming:
io.Copypara tuberías de datos - Logging:
io.MultiWriterpara 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.
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.FSes una interfaz ligera — implementarla es baratoos.DirFSañade overhead mínimo sobre operaciones directas de discofstest.MapFSopera en RAM — ideal para tests rápidosfs.WalkDires más eficiente quefilepath.Walk(evita stat innecesarios)
✅ Buenas prácticas
- ✅ Usa
fs.FScomo tipo de parámetro para aceptar cualquier origen de archivos - ✅ Usa
fstest.MapFSpara tests — evita archivos temporales - ⚠️ No implementes
fs.FSmanualmente si solo lees archivos reales - ❌ No asumas que todos los FS tienen directorios — algunos son planos (embed)
🏗️ Metodología
- Tests:
fstest.MapFSpara simular cualquier estructura - Producción:
os.DirFSpara archivos reales,embed.FSpara estáticos - Librerías: acepta
fs.FSen 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).
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.Joines O(n) — construye la ruta sin allocations extrafilepath.Walkusa callbacks — no carga el árbol completo en memoriafilepath.Globusa el mismo matching que la shell — O(n) en el directorio- Clean paths:
filepath.Cleannormaliza../y//sin tocar el disco
✅ Buenas prácticas
- ✅ Siempre usa
filepath.Joinen lugar de concatenar strings con/ - ✅ Usa
filepath.Walken lugar deos.ReadDirrecursivo manual - ⚠️ No uses
filepath.Walksi el árbol es muy profundo (>1000 niveles) - ❌ No asumas
/como separador —filepath.Joines multiplataforma
🏗️ Metodología
- Rutas simples:
filepath.Join+filepath.Dir/Base/Ext - Recorrer árbol:
filepath.Walkpara procesar todos los archivos - Búsqueda por patrón:
filepath.Globpara 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.
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:embedse validan en compilación — errores tempranos - Sirviendo con
http.FSno hay syscalls de disco — extremadamente rápido
✅ Buenas prácticas
- ✅ Úsalo para templates, archivos estáticos, configuraciones por defecto
- ✅ Especifica patrones precisos en
//go:embedpara no incluir archivos grandes - ⚠️ No incrustes archivos grandes (>10 MB) — el binario se hace muy pesado
- ❌ No uses
embedpara 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
.sqlpara migraciones sin path externo - Tests: usa
embed.FScomo implementación defs.FSen 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.
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 RAMRead()fila por fila para streaming — O(1) memoria por iteracióncsv.NewReaderconFieldsPerRecord = -1permite 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 deFlush()— 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.
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/Unmarshalusa reflection — más lento que serialización manual- Los decoders/encoders con streaming son más eficientes en memoria
json.RawMessagepermite decodificación perezosa (lazy)- La reutilización de buffers con
json.NewEncoderreduce 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/Unmarshalconanysin type assertion
🏗️ Metodología
- APIs REST:
json.NewEncoder/json.NewDecoderpara streaming HTTP - Archivos:
json.Marshal/Unmarshalpara archivos completos - Config:
json.Decoderconos.Openpara leer config desde archivo - NDJSON:
json.NewDecoderen 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/gziptiene niveles 1-9 (velocidad vs compresión)archive/ziplista 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.NewWritercomo 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.
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.Intsusa QuickSort (variante) — O(n log n) promedio, O(n²) peor casosort.Slicees O(n log n) pero más lento quesort.Ints(usa reflection)sort.Searches O(log n) — búsqueda binaria clásica- El ordenamiento es in-place — O(1) memoria extra
✅ Buenas prácticas
- ✅ Usa
sort.Ints/sort.Stringspara tipos nativos (más rápido que Slice) - ✅ Usa
sort.Slicepara structs con comparadores personalizados - ⚠️ No llames a
sort.Searchen un slice desordenado — resultados indefinidos - ❌ No implementes
sort.Interfacemanualmente sisort.Slicete sirve
🏗️ Metodología
- Tipos nativos:
sort.Ints/sort.Strings/sort.Float64s - Structs:
sort.Slicecon función de comparación - Múltiples criterios:
sort.SliceStablepara ordenación estable - Búsqueda:
sort.Searchdespué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.
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.Sortes un algoritmo híbrido (pdqsort) — O(n log n) promedioslices.BinarySearches O(log n)slices.Delete/slices.Insertson O(n) — mueven elementosslices.Compactes O(n) — elimina duplicados adyacentes en una pasadaslices.Equales O(n) pero sale rápido en la primera diferencia
✅ Buenas prácticas
- ✅ Prefiere
slicessobresortpara código moderno (Go 1.21+) - ✅ Usa
slices.Containsen lugar de búsqueda manual confor - ⚠️
slices.Compactsolo elimina duplicados adyacentes — ordena primero - ❌ No uses
slices.Equalcon 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.
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.Clonees O(n) — copia superficial, los valores no se clonanmaps.Copyes O(n) — sobrescribe claves existentesmaps.Keys/maps.Valuesson O(n) — crean un nuevo slicemaps.DeleteFunces O(n) — itera todo el mapmaps.Equales O(n) — compara clave a clave
✅ Buenas prácticas
- ✅ Usa
maps.Cloneen lugar de crear un map nuevo y copiar manualmente - ✅ Usa
maps.DeleteFuncpara limpieza condicional en lugar defor...delete - ⚠️
maps.Keysno garantiza orden — ordena el resultado si es necesario - ❌ No uses
maps.Copyesperando merge profundo — solo es shallow
🏗️ Metodología
- Clonar:
maps.Clonepara evitar mutación accidental - Fusionar:
maps.Copypara combinar maps - Limpiar:
maps.DeleteFuncpara eliminar elementos por condición - Serializar:
maps.Keys/maps.Valuespara 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.
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.Inites O(n) — heapify linearheap.Push/heap.Popson O(log n)heap.Fixes O(log n) — útil cuando cambia la prioridad de un elementoheap.Removees O(log n) — eliminar elemento arbitrario por índice
✅ Buenas prácticas
- ✅ Implementa
heap.Interfacepara cualquier tipo que necesite prioridad - ✅ Usa
heap.Initen lugar dePushrepetido (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.
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.Listno es genérico — los valores sonany(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())— perocontainer/listañ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.
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 fijar.Do(f)es O(n) — recorre todos los elementosr.Next()/r.Prev()son O(1) — solo mueve punterosr.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
| Estructura | Acceso | Inserción | Eliminación | Búsqueda | Notas |
|---|---|---|---|---|---|
| Slice (append) | O(1) | O(1) amortizado | O(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) |
| Map | O(1) promedio | O(1) promedio | O(1) promedio | O(1) promedio | tabla hash, colisiones posibles |
| Lista enlazada | O(n) | O(1) | O(1) (con elem) | O(n) | container/list |
| Heap (Push/Pop) | — | O(log n) | O(log n) | — | container/heap |
| Ring | O(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 OOP | En Go |
|---|---|
| Clase | struct con métodos |
| Constructor | Función que devuelve *T (ej: NewUsuario()) |
| Herencia | Embedding de structs (composición) |
| Polimorfismo | Interfaces (implícitas, estructurales) |
| Encapsulación | Mayúscula/minúscula (exportado/no exportado) |
| Métodos estáticos | Funciones de paquete |
| this/self | Receptor explícito (u *Usuario) |
| super | Llamar al método del struct embebido |
| Abstract class | Interfaz + constructor que devuelve la interfaz |
| Overloading | No existe — nombres de función únicos |
| Excepciones | error interface + panic/recover |
| Genéricos | Type 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:
| Concepto | Cómo aparece |
|---|---|
| Interfaz | Contenido, Emisible |
| Implementación implícita | *Audio implementa Contenido sin declaración |
| Embedding | *contenidoBase dentro de Audio y Video |
| Composicion | Playlist contiene []Contenido |
| Constructor | NewAudio(), NewVideo() |
| Encapsulación | subtitulos minúscula, SetSubtitulos() mayúscula |
| Polimorfismo | Info() distinto comportamiento según tipo |
| Métodos | Receiver (a *Audio) |
| Interfaz vacía | No 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
*contenidoBaseembebido como puntero permite compartir estado
✅ Buenas prácticas
- ✅ Usa interfaces para definir contratos de comportamiento
- ✅ Usa embedding para reutilizar implementación
- ✅ Constructores devuelven
*TnoTpara 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ón | Interfaces | Gené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— TIFFgolang.org/x/image/bmp— BMPgolang.org/x/image/webp— WebP decodegolang.org/x/image/font— renderizado de fuentesgolang.org/x/image/vector— gráficos vectoriales
go get golang.org/x/image
Librerías externas populares
| Librería | Para qué | Enlace |
|---|---|---|
| gg | Renderizado 2D (líneas, formas, texto, gradientes) | github.com/fogleman/gg |
| go-ffmpeg | Bindings para FFmpeg (procesamiento de video) | github.com/m1k1o/go-ffmpeg |
| beep | Reproducción y procesamiento de audio | github.com/gopxl/beep |
| oto | Audio multiplataforma (sin dependencias externas) | github.com/hajimehoshi/oto |
| gocv | OpenCV bindings (visión artificial) | github.com/hybridgroup/gocv |
| gographics/imagick | ImageMagick bindings | github.com/gographics/imagick |
| disintegration/imaging | Procesamiento de imágenes de alto nivel | github.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ía | Async | ORM | Para qué |
|---|---|---|---|
| database/sql (stdlib) | No | No | Driver estándar, funciona con PostgreSQL, MySQL, SQLite |
| pgx | Sí | No | Driver PostgreSQL nativo en Go. 5x más rápido que lib/pq |
| sqlx | No | No | Extensión de database/sql con struct scanning, named params |
| GORM | No | ORM completo | ORM más popular. Migraciones automáticas, callbacks, preloading |
| bun | Sí | ORM ligero | SQLite/PostgreSQL/MySQL. Query builder fluido |
| jet | No | Query builder | Genera 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ía | Para qué |
|---|---|
| gorilla/websocket | La más usada, madura y bien documentada |
| nhooyr.io/websocket | Moderna, más simple, mejor soporte para context |
| Melody | Framework 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.
| Escenario | Solución Go |
|---|---|
| Lanzar N tareas independientes | go func() + sync.WaitGroup |
| Productor-consumidor | Canal 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ón | context.WithTimeout |
| Rate limiting | Canal ticker o rate.Limiter |
| Pipeline de procesamiento | Canales encadenados |
| Broadcast a N listeners | Canal cerrado (todos reciben zero value) |
| Shared state protegido | sync.Mutex o sync.RWMutex |
| Una sola inicialización | sync.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
| Concepto | Concurrencia | Paralelismo |
|---|---|---|
| Definición | Múltiples tareas progresan al mismo tiempo | Múltiples tareas se ejecutan simultáneamente |
| En Go | Goroutines multiplexadas en threads del SO | Varias goroutines ejecutándose en distintos cores |
| Núcleos | No necesita múltiples cores | Necesita múltiples cores |
GOMAXPROCS | Controla cuántos threads del SO usa el runtime | runtime.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.
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 testcompila y ejecuta en el mismo proceso — rápido-benchejecuta benchmarks conb.Niteraciones automáticas- Subtests (
t.Run) se ejecutan en paralelo si usast.Parallel()
✅ Buenas prácticas
- ✅ Usa table-driven tests para múltiples casos
- ✅ Los nombres de archivos test deben terminar en
_test.go - ⚠️ No uses
ifpara errores de test — usat.Error/t.Fatal - ❌ No dependas del orden de ejecución — cada test es independiente
🏗️ Metodología
- Unit tests: un archivo
_test.gopor 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.
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
ifmanual requirellama at.FailNow()internamente — detiene el goroutine actual- Las suites (
testify/suite) añaden estructura pero no overhead significativo
✅ Buenas prácticas
- ✅ Usa
assertpara la mayoría de casos (no detiene el test) - ✅ Usa
requirecuando no tenga sentido continuar tras el fallo - ⚠️ No mezcles
t.Errorcon 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/suitepara 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
-coverinstrumenta el código añadiendo contadores — overhead ~5-10% en tiempo de test-benchmemmide alocaciones por operación (allocations/op)- Los resultados de bench son estables con
-count=5para 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
gopara dudas técnicas
Repositorios de código
- golang/go — Código fuente del lenguaje y la stdlib
- awesome-go — Lista curada de frameworks, librerías y recursos
- golang-standards/project-layout — Estructura estándar de proyectos Go
- dgraph-io/badger — Base de datos KV embebida en Go
- gin-gonic/gin — Framework web más popular
Blogs y newsletters
- Go dev blog — Blog oficial del equipo de Go
- Go by Example — Ejemplos interactivos de cada concepto
- Go Time podcast — Podcast semanal sobre Go (inglés)
- Just For Func — YouTube channel sobre Go avanzado (inglés)
- Dave Cheney blog — Artículos profundos sobre Go, testing, errores
Fuente: búsqueda en YouTube y Reddit, listas curadas de Awesome Go
Referencias y documentación oficial
- Docs oficiales: go.dev/doc — tutorial, effective go, language reference
- Go Specification: go.dev/ref/spec — especificación completa del lenguaje
- Go Modules: go.dev/doc/modules — gestión de dependencias
- Go Packages: pkg.go.dev — documentación de todos los paquetes
- Go Playground: go.dev/play — probar código Go en el navegador
- Go DL: go.dev/dl — descargas oficiales
- Go Forum: forum.golangbridge.org — foro de la comunidad
- Go GitHub: github.com/golang/go — issues, proposals, contribuciones
Fuentes de esta guía
Esta guía se ha elaborado a partir de las siguientes fuentes consultadas durante su desarrollo:
- Go dev — Documentación oficial y tutorial
- Go by Example — Ejemplos prácticos de cada concepto
- Effective Go — Guía de estilo y buenas prácticas
- Go Specification — Referencia completa del lenguaje
- Gin Web Framework — Documentación
- GORM — Documentación
- sqlx — Documentación
- Go Blog — Artículos oficiales del equipo Go
- Dave Cheney — Blog sobre Go
- Go Time Podcast — Discusiones semanales sobre Go
- r/golang — Comunidad, debates y proyectos
- Awesome Go — Lista curada de librerías
- Go Proverbs (Rob Pike) — Filosofía del lenguaje en aforismos
- Concurrency is not parallelism (Rob Pike) — Charla fundamental sobre concurrencia
- Go Standard Project Layout — Convenciones de estructura de proyectos
⚠️ 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.
| Error | Causa | Solución |
|---|---|---|
undefined: fmt / ./main.go:7:2: undefined: fmt | Usas fmt.Println pero no lo importaste | Añade import "fmt" entre los imports |
undefined: X | La función o variable X no existe en ese ámbito o paquete | Comprueba el nombre y que esté exportada (mayúscula) o disponible |
cannot use X (type string) as type int in argument | Pasa un string a una función que espera un int | Convierte el valor: int(variable) o strconv.Atoi(). Go no hace coerción implícita |
go: go.mod file not found | Ejecutas go get, go run o go build fuera de un módulo | Crea el módulo con go mod init <nombre> en la raíz del proyecto |
command-line-arguments con go run | Ejecutas go run sobre un paquete sin func main o con varias | Crea o localiza el fichero con package main y func main() |
dial tcp: lookup ...: no such host al descargar dependencias | Problema de red o DNS al go get / go mod download | Revisa 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 run | Variables de entorno de versiones antiguas | go env -w GO111MODULE=on (o auto) y reapunta GOPATH |
linker 'cc' not found en Windows/CGO | Falta un compilador C para enlazar | Instala un compilador C (MinGW/GCC) o desactiva CGO con CGO_ENABLED=0 |
🔗 Guías relacionadas
- Para empaquetar tu binario en contenedores y publicarlo: Guía de Docker: De 0 a 100.
- Primeros pasos con contenedores: Instalación de Docker y primeros pasos.
🏆 Retos Relacionados
Pon a prueba lo aprendido con estos desafíos:
