🎯 Meta: aprender un lenguaje compilado de bajo nivel pero simple, pensado para servicios rápidos, concurrentes y fáciles de desplegar. Go es el lenguaje de Docker, Kubernetes, Terraform y gran parte de la infraestructura de la nube. Es el más "distinto" del libro y el que más te hace crecer.
Versiones: Go 1.26.5 · librería estándar (
net/http) · sqlc / pgx.
8.1 · ¿Por qué Go es diferente (y por qué lo dejamos para el final)?
- Compilado a un binario único: no necesitas runtime ni dependencias en el servidor. Copias un archivo y funciona. El sueño del despliegue.
- Rapidísimo y de bajo consumo de memoria.
- Concurrencia sencilla con goroutines (miles de tareas paralelas con una palabra:
go). - Tipado estático y explícito, sintaxis minimalista (pocas palabras clave).
- Manejo de errores explícito: no hay excepciones; los errores se devuelven y se comprueban.
🧠 El "shock cultural" de Go: vienes de frameworks que te dan todo; Go te da la librería estándar y espera que construyas. No hay "magia" (ni decoradores, ni DI oculta). El código es explícito hasta el detalle. Al principio parece verboso; luego amas su claridad: lees lo que hace, sin adivinar.
8.2 · Instalación y primer programa
Descarga Go 1.26 de go.dev/dl. Comprueba:
go version # go version go1.26.5// main.go
package main
import "fmt"
func main() {
fmt.Println("¡Hola, Go!")
}go run main.go # compila y ejecuta
go build # genera un binario ejecutableInicia un módulo (gestión de dependencias):
mkdir tienda-go && cd tienda-go
go mod init tienda # crea go.mod8.3 · Fundamentos del lenguaje en 5 minutos
// Variables (tipado inferido con :=)
nombre := "Ana" // string
edad := 29 // int
precio := 99.90 // float64
activo := true // bool
// Función (tipos DESPUÉS del nombre; puede devolver varios valores)
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("división por cero")
}
return a / b, nil // nil = "sin error"
}
// Structs (como una clase sin métodos, o un registro)
type Producto struct {
ID int `json:"id"`
Nombre string `json:"nombre"`
Precio float64 `json:"precio"`
}
// Slices (arrays dinámicos) y maps (diccionarios)
productos := []Producto{} // slice vacío
precios := map[string]float64{"mouse": 49.9}
// Bucle (solo existe "for", hace de todo)
for i, p := range productos {
fmt.Println(i, p.Nombre)
}El manejo de errores es LA característica cultural de Go:
resultado, err := dividir(10, 0)
if err != nil { // se comprueba SIEMPRE, explícitamente
log.Printf("error: %v", err)
return
}
fmt.Println(resultado)🧠
if err != nilpor todos lados: al principio molesta. Pero hace imposible ignorar un error por accidente (a diferencia de las excepciones que se propagan silenciosas). Go te obliga a decidir qué haces con cada fallo. Es tedioso y a la vez tremendamente robusto.
Las etiquetas json:"nombre" en el struct dicen cómo se serializa a JSON. Fíjate que solo los campos que empiezan por Mayúscula son públicos (exportados) — así funciona la visibilidad en Go.
8.4 · Servidor HTTP con la librería estándar
Go no necesita framework: net/http es potente. Desde Go 1.22 el router estándar entiende métodos y parámetros de ruta:
// main.go
package main
import (
"encoding/json"
"log"
"net/http"
)
type Producto struct {
ID int `json:"id"`
Nombre string `json:"nombre"`
Precio float64 `json:"precio"`
}
func main() {
mux := http.NewServeMux()
// GET /productos
mux.HandleFunc("GET /productos", func(w http.ResponseWriter, r *http.Request) {
productos := []Producto{{ID: 1, Nombre: "Mouse", Precio: 49.9}}
escribirJSON(w, http.StatusOK, productos)
})
// GET /productos/{id} (parámetro de ruta nativo)
mux.HandleFunc("GET /productos/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
escribirJSON(w, http.StatusOK, map[string]string{"id": id})
})
log.Println("Escuchando en :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
// Helper para responder JSON (reutilizable)
func escribirJSON(w http.ResponseWriter, code int, datos any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(datos)
}go run main.go💡 Tip: para muchos proyectos la librería estándar basta. Si quieres más comodidad, los frameworks populares en 2026 son Gin, Echo o Chi. Pero aprende primero
net/http: entenderás qué hacen los frameworks por debajo (a diferencia de quien empieza directo con Gin).
8.5 · CRUD completo con capas (arquitectura limpia desde ya)
Go invita a separar responsabilidades. Estructura típica:
tienda-go/
├── main.go ← arranque y rutas
├── internal/
│ ├── producto/
│ │ ├── modelo.go ← el struct Producto
│ │ ├── repositorio.go ← acceso a datos (BD)
│ │ ├── servicio.go ← lógica de negocio
│ │ └── handler.go ← HTTP (controlador)
│ └── db/
│ └── db.go ← conexión a PostgreSQL
├── go.mod
└── go.sumEl modelo:
// internal/producto/modelo.go
package producto
type Producto struct {
ID int64 `json:"id"`
Nombre string `json:"nombre"`
Precio float64 `json:"precio"`
Stock int `json:"stock"`
}
// DTO de entrada, con validación
type CrearInput struct {
Nombre string `json:"nombre"`
Precio float64 `json:"precio"`
Stock int `json:"stock"`
}
func (in CrearInput) Validar() error {
if in.Nombre == "" {
return fmt.Errorf("el nombre es obligatorio")
}
if in.Precio < 0 {
return fmt.Errorf("el precio no puede ser negativo")
}
return nil
}El repositorio (acceso a BD) — una interfaz, para poder cambiar la implementación o simularla en tests:
// internal/producto/repositorio.go
package producto
import (
"context"
"github.com/jackc/pgx/v5/pgxpool"
)
// La INTERFAZ define QUÉ se puede hacer, no CÓMO:
type Repositorio interface {
Listar(ctx context.Context) ([]Producto, error)
Crear(ctx context.Context, in CrearInput) (Producto, error)
Buscar(ctx context.Context, id int64) (Producto, error)
}
// Implementación con PostgreSQL (pgx):
type repoPG struct {
db *pgxpool.Pool
}
func NuevoRepositorio(db *pgxpool.Pool) Repositorio {
return &repoPG{db: db}
}
func (r *repoPG) Crear(ctx context.Context, in CrearInput) (Producto, error) {
var p Producto
err := r.db.QueryRow(ctx,
`INSERT INTO productos (nombre, precio, stock)
VALUES ($1, $2, $3) RETURNING id, nombre, precio, stock`,
in.Nombre, in.Precio, in.Stock,
).Scan(&p.ID, &p.Nombre, &p.Precio, &p.Stock)
return p, err
}
func (r *repoPG) Listar(ctx context.Context) ([]Producto, error) {
rows, err := r.db.Query(ctx, `SELECT id, nombre, precio, stock FROM productos`)
if err != nil {
return nil, err
}
defer rows.Close()
var productos []Producto
for rows.Next() {
var p Producto
if err := rows.Scan(&p.ID, &p.Nombre, &p.Precio, &p.Stock); err != nil {
return nil, err
}
productos = append(productos, p)
}
return productos, rows.Err()
}
func (r *repoPG) Buscar(ctx context.Context, id int64) (Producto, error) {
var p Producto
err := r.db.QueryRow(ctx,
`SELECT id, nombre, precio, stock FROM productos WHERE id = $1`, id,
).Scan(&p.ID, &p.Nombre, &p.Precio, &p.Stock)
return p, err
}El servicio (lógica):
// internal/producto/servicio.go
package producto
import "context"
type Servicio struct {
repo Repositorio
}
func NuevoServicio(repo Repositorio) *Servicio {
return &Servicio{repo: repo}
}
func (s *Servicio) Crear(ctx context.Context, in CrearInput) (Producto, error) {
if err := in.Validar(); err != nil {
return Producto{}, err
}
return s.repo.Crear(ctx, in)
}
func (s *Servicio) Listar(ctx context.Context) ([]Producto, error) {
return s.repo.Listar(ctx)
}El handler (HTTP):
// internal/producto/handler.go
package producto
import (
"encoding/json"
"net/http"
)
type Handler struct {
svc *Servicio
}
func NuevoHandler(svc *Servicio) *Handler {
return &Handler{svc: svc}
}
func (h *Handler) Crear(w http.ResponseWriter, r *http.Request) {
var in CrearInput
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, `{"error":"JSON inválido"}`, http.StatusBadRequest)
return
}
p, err := h.svc.Crear(r.Context(), in)
if err != nil {
http.Error(w, `{"error":"`+err.Error()+`"}`, http.StatusUnprocessableEntity)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(p)
}
func (h *Handler) Listar(w http.ResponseWriter, r *http.Request) {
productos, err := h.svc.Listar(r.Context())
if err != nil {
http.Error(w, `{"error":"error interno"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(productos)
}Ensamblado en main.go (esto es inyección de dependencias a mano — sin magia):
func main() {
pool, _ := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
defer pool.Close()
repo := producto.NuevoRepositorio(pool) // repo depende de la BD
svc := producto.NuevoServicio(repo) // servicio depende del repo
h := producto.NuevoHandler(svc) // handler depende del servicio
mux := http.NewServeMux()
mux.HandleFunc("GET /productos", h.Listar)
mux.HandleFunc("POST /productos", h.Crear)
log.Fatal(http.ListenAndServe(":8080", mux))
}🧠 Aquí lo ves TODO explícito: el handler recibe el servicio, el servicio el repositorio, el repositorio la BD. En NestJS o FastAPI esto lo hacía el framework por ti (DI automática). En Go lo escribes tú. Ventaja: entiendes exactamente cómo encaja la aplicación. Este patrón (repo → service → handler con interfaces) es Clean Architecture en la práctica (cap. 11).
8.6 · Concurrencia: goroutines y channels
El superpoder de Go. Lanzar una tarea concurrente es literalmente añadir go delante:
// Lanzar 3 tareas en paralelo y esperar a todas
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) { // "go" lanza una goroutine
defer wg.Done()
resp, _ := http.Get(u) // se ejecuta en paralelo
// ...procesar...
resp.Body.Close()
}(url)
}
wg.Wait() // espera a que terminen todasLos channels comunican goroutines de forma segura:
resultados := make(chan int)
go func() { resultados <- calcularAlgo() }() // envía al channel
valor := <-resultados // recibe (espera si hace falta)🧠 Goroutines vs threads: una goroutine pesa ~2KB (un hilo del SO, megabytes). Puedes lanzar millones. El runtime de Go las multiplexa sobre pocos hilos reales. Por eso Go maneja concurrencia masiva con facilidad. En Go 1.26 el recolector de basura "Green Tea" viene activado por defecto, mejorando aún más el rendimiento.
8.7 · Tests (integrados en el lenguaje)
Go trae testing en la librería estándar. Un archivo _test.go junto al código:
// internal/producto/servicio_test.go
package producto
import (
"context"
"testing"
)
// Un repositorio FALSO para testear el servicio sin BD real:
type repoFake struct {
creados []Producto
}
func (r *repoFake) Crear(ctx context.Context, in CrearInput) (Producto, error) {
p := Producto{ID: 1, Nombre: in.Nombre, Precio: in.Precio}
r.creados = append(r.creados, p)
return p, nil
}
func (r *repoFake) Listar(ctx context.Context) ([]Producto, error) { return r.creados, nil }
func (r *repoFake) Buscar(ctx context.Context, id int64) (Producto, error) { return Producto{}, nil }
func TestCrearProducto(t *testing.T) {
svc := NuevoServicio(&repoFake{})
p, err := svc.Crear(context.Background(), CrearInput{Nombre: "Mouse", Precio: 49.9})
if err != nil {
t.Fatalf("no esperaba error: %v", err)
}
if p.Nombre != "Mouse" {
t.Errorf("nombre = %q; quería %q", p.Nombre, "Mouse")
}
}
func TestCrearSinNombreFalla(t *testing.T) {
svc := NuevoServicio(&repoFake{})
_, err := svc.Crear(context.Background(), CrearInput{Precio: 10})
if err == nil {
t.Fatal("esperaba error por nombre vacío, no hubo")
}
}
// Tabla de casos (patrón idiomático en Go):
func TestValidar(t *testing.T) {
casos := []struct {
nombre string
input CrearInput
wantErr bool
}{
{"válido", CrearInput{Nombre: "X", Precio: 10}, false},
{"sin nombre", CrearInput{Precio: 10}, true},
{"precio negativo", CrearInput{Nombre: "X", Precio: -1}, true},
}
for _, c := range casos {
t.Run(c.nombre, func(t *testing.T) {
err := c.input.Validar()
if (err != nil) != c.wantErr {
t.Errorf("Validar() error = %v, wantErr = %v", err, c.wantErr)
}
})
}
}go test ./... # todos los tests del proyecto
go test -v ./... # detallado
go test -cover ./... # con cobertura
go test -race ./... # detecta condiciones de carrera (¡clave con goroutines!)💡 Tip: fíjate en el patrón repositorio como interfaz → en el test inyectas un
repoFakeen vez de la BD real. Poder simular dependencias es exactamente por qué se usan interfaces. Es el mismo principio de DI de FastAPI/Nest, pero explícito. Ygo test -racees una joya: detecta bugs de concurrencia que en otros lenguajes son pesadilla.
8.7.1 · Error wrapping — no pierdas el contexto del fallo
Devolver err tal cual, subiendo capa por capa, pierde de dónde vino. fmt.Errorf con %wenvuelve el error original sin perderlo:
func (s *Servicio) Crear(ctx context.Context, in CrearInput) (Producto, error) {
if err := in.Validar(); err != nil {
return Producto{}, err
}
p, err := s.repo.Crear(ctx, in)
if err != nil {
return Producto{}, fmt.Errorf("servicio: crear producto %q: %w", in.Nombre, err)
}
return p, nil
}// errors.Is comprueba si un error "es" (o envuelve) un error centinela específico:
var ErrDuplicado = errors.New("ya existe un producto con ese nombre")
if errors.Is(err, ErrDuplicado) {
http.Error(w, `{"error":"ya existe"}`, http.StatusConflict) // 409, no 500
return
}
// errors.As extrae un tipo de error concreto de la cadena envuelta:
var pgErr *pgconn.PgError
if errors.As(err, &pgErr) && pgErr.Code == "23505" { // violación de unique constraint
// ...
}🧠 Por qué
%wy no%v: con%vel error se convierte en texto plano y pierdes la posibilidad de inspeccionarlo programáticamente más arriba. Con%w,errors.Is/errors.Aspueden "ver a través" de todas las capas envueltas hasta encontrar el error original — puedes tener un mensaje descriptivo en cada capa ("servicio: crear producto: repo: %w") y aun así decidir el código HTTP correcto en el handler según el error de fondo real.
8.7.2 · Generics — una función, muchos tipos
Desde Go 1.18, una función puede ser genérica sobre un tipo, evitando duplicar código o recurrir a any (perdiendo el tipado). Útil para utilidades reutilizables entre paquetes:
// Funciona con []Producto, []Categoria, o cualquier slice — sin duplicar la función:
func Filtrar[T any](items []T, cumple func(T) bool) []T {
var resultado []T
for _, item := range items {
if cumple(item) {
resultado = append(resultado, item)
}
}
return resultado
}
disponibles := Filtrar(productos, func(p Producto) bool { return p.Stock > 0 })// Un repositorio genérico reduce boilerplate cuando varias entidades comparten forma:
type RepositorioCRUD[T any] interface {
Buscar(ctx context.Context, id int64) (T, error)
Listar(ctx context.Context) ([]T, error)
}⚠️ No abuses de los generics. Son geniales para utilidades genéricas de verdad (
Filtrar,Map, contenedores tipoSet[T]) y para reducir duplicación real entre repositorios similares. Para lógica de negocio específica, un tipo concreto (Producto, noT) suele ser más legible — Go prioriza claridad sobre abstracción clever. Si dudas, empieza sin generics.
8.8 · Compilar y desplegar (lo más fácil del libro)
# Compilar un binario para Linux desde cualquier SO (cross-compilation):
GOOS=linux GOARCH=amd64 go build -o servidor .
# El resultado es UN archivo. Lo copias al servidor y lo ejecutas:
./servidorEn Docker, la imagen final puede ser diminuta (multi-stage build, cap. 12): compilas en una imagen con Go y copias solo el binario a una imagen scratch (vacía) de pocos MB. Ningún otro stack del libro logra imágenes tan pequeñas.
8.9 · Buenas prácticas Go
gofmt/go fmt: el formato es automático y no se discute. Todo Go se ve igual.- Maneja cada error (
if err != nil); no uses_para tragarte errores importantes. - Interfaces pequeñas definidas donde se usan, no donde se implementan.
context.Contextcomo primer parámetro en funciones que hacen I/O (cancelación/timeouts).- Paquetes por dominio (
producto,pedido), no por capa técnica. go test -raceantes de confiar en código concurrente.go vetystaticcheckpara detectar problemas.- No sobre-uses frameworks: la librería estándar suele bastar.
✅ Ejercicio del capítulo
La API del blog en Go, con arquitectura por capas:
1. Paquetes: articulo, comentario, con modelo/repo/servicio/handler.
2. Repositorio como interfaz + implementación con pgx (PostgreSQL).
3. Validación en el input (método Validar()).
4. Rutas con net/http (GET/POST/PUT/DELETE, method routing nativo).
5. Un endpoint que use goroutines (ej. traer artículo + sus comentarios en
paralelo con un errgroup).
6. Tests: repoFake + tabla de casos para la validación. Corre con -race.
7. Compila un binario y ejecútalo.
8. Envuelve los errores del repositorio con `%w` y usa `errors.Is` en el handler
para devolver 409 en vez de 500 cuando el título del artículo ya existe.¡Felicidades! Has recorrido 6 lenguajes/frameworks de fácil a complejo. Ya no aprendes sintaxis: reconoces patrones (rutas, validación, servicios, repositorios, DI, tests) en cualquier stack. Ahora toca aprender a hacerlo bien.
💡 Pistas de la solución
- El repositorio como interfaz vive en el paquete
articulo(donde se USA, regla 3 de las buenas prácticas); la implementación conpgxvive aparte e implementa esa interfaz — así el servicio depende de la interfaz, no de PostgreSQL directamente. - Para el endpoint con goroutines: usa
golang.org/x/sync/errgroup— lanza dos goroutines (artículo y comentarios) cong.Go(func() error {...})yg.Wait()espera a ambas, propagando el primer error que ocurra. - El fallo típico del punto 6: un
repoFakeque no protege su mapa interno con un mutex falla con-raceen cuanto los tests corren en paralelo — o bien usa un mutex, o no compartas estado mutable entre tests.
🧠 Autoevaluación
Porque hace visible en la firma de cada función qué puede fallar, y obliga a decidir en el punto exacto dónde ocurre — no puedes "olvidarte" de un error como sí puedes olvidar un try/catch en otros lenguajes. El coste es más código; el beneficio es que los errores no se propagan en silencio hasta un catch genérico lejano.
Porque así el paquete que consume el repositorio (por ejemplo, el servicio de artículos) solo depende de "lo que necesita" (una interfaz mínima), no de la implementación concreta con PostgreSQL. Esto permite sustituir la implementación real por un fake en tests sin tocar el código que la usa — inversión de dependencias otra vez, como en FastAPI y NestJS.
Condiciones de carrera: dos goroutines accediendo a la misma variable al mismo tiempo, al menos una escribiendo, sin sincronización. El test puede pasar en apariencia (el resultado "parece" correcto) mientras el comportamiento es indefinido — el flag -race instrumenta el binario para detectarlo en tiempo de ejecución.
Go compila a un binario nativo estático sin dependencias externas (ni intérprete, ni runtime). Un multi-stage build (cap. 12) copia solo ese binario a una imagen scratch vacía. Node y Python necesitan su runtime completo dentro de la imagen final para ejecutar el código.
Pierdes la capacidad de inspeccionar el error original más arriba en la pila: %v lo convierte en texto plano, mientras que %w lo envuelve preservando la cadena completa, de forma que errors.Is/errors.As puedan seguir identificando el error de fondo (por ejemplo, distinguir una violación de unique constraint de un fallo de conexión) para decidir el código HTTP correcto.
Siguiente: 09-testing.md — la disciplina que separa aficionados de profesionales.