Skip to content

🎯 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:

bash
go version        # go version go1.26.5
go
// main.go
package main

import "fmt"

func main() {
    fmt.Println("¡Hola, Go!")
}
bash
go run main.go        # compila y ejecuta
go build              # genera un binario ejecutable

Inicia un módulo (gestión de dependencias):

bash
mkdir tienda-go && cd tienda-go
go mod init tienda           # crea go.mod

8.3 · Fundamentos del lenguaje en 5 minutos

go
// 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:

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 != nil por 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:

go
// 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)
}
bash
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.sum

El modelo:

go
// 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:

go
// 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):

go
// 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):

go
// 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):

go
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:

go
// 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 todas

Los channels comunican goroutines de forma segura:

go
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:

go
// 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)
            }
        })
    }
}
bash
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 repoFake en 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. Y go test -race es 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:

go
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
}
go
// 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é %w y no %v: con %v el error se convierte en texto plano y pierdes la posibilidad de inspeccionarlo programáticamente más arriba. Con %w, errors.Is/errors.As pueden "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:

go
// 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 })
go
// 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 tipo Set[T]) y para reducir duplicación real entre repositorios similares. Para lógica de negocio específica, un tipo concreto (Producto, no T) 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)

bash
# 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:
./servidor

En 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

  1. gofmt / go fmt: el formato es automático y no se discute. Todo Go se ve igual.
  2. Maneja cada error (if err != nil); no uses _ para tragarte errores importantes.
  3. Interfaces pequeñas definidas donde se usan, no donde se implementan.
  4. context.Context como primer parámetro en funciones que hacen I/O (cancelación/timeouts).
  5. Paquetes por dominio (producto, pedido), no por capa técnica.
  6. go test -race antes de confiar en código concurrente.
  7. go vet y staticcheck para detectar problemas.
  8. 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 con pgx vive 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) con g.Go(func() error {...}) y g.Wait() espera a ambas, propagando el primer error que ocurra.
  • El fallo típico del punto 6: un repoFake que no protege su mapa interno con un mutex falla con -race en 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.