Configuración lista para producción (por entorno)
Un servicio escalable no “cablea” valores (puertos, credenciales, URLs) en el código: los recibe desde el entorno (variables, archivos montados, secretos del proveedor cloud). La meta es que el mismo binario se ejecute en dev/staging/prod cambiando solo configuración.
Principios prácticos
- Config por variables de entorno: 12-factor style. Fácil de inyectar en contenedores y plataformas cloud.
- Valores por defecto seguros: por ejemplo, bind a 0.0.0.0:8080, timeouts razonables.
- Validación al inicio: fallar rápido si falta algo crítico (p. ej., DSN).
- Separar config de lógica: un paquete
configque solo parsea/valida.
Ejemplo: struct de configuración + carga desde env
package config
import (
"errors"
"os"
"strconv"
"time"
)
type Config struct {
Env string // dev|staging|prod
HTTPAddr string // ":8080"
ShutdownTimeout time.Duration
LogLevel string // debug|info|warn|error
DBPath string // persistencia simple (archivo)
}
func Load() (Config, error) {
cfg := Config{
Env: getenv("APP_ENV", "dev"),
HTTPAddr: getenv("HTTP_ADDR", ":8080"),
LogLevel: getenv("LOG_LEVEL", "info"),
DBPath: getenv("DB_PATH", "./data.db"),
ShutdownTimeout: getenvDuration("SHUTDOWN_TIMEOUT", 10*time.Second),
}
if cfg.Env == "prod" && cfg.DBPath == "" {
return Config{}, errors.New("DB_PATH requerido en prod")
}
return cfg, nil
}
func getenv(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func getenvDuration(key string, def time.Duration) time.Duration {
v := os.Getenv(key)
if v == "" {
return def
}
// acepta segundos como entero para simplicidad
n, err := strconv.Atoi(v)
if err != nil {
return def
}
return time.Duration(n) * time.Second
}Esta carga es deliberadamente simple. En servicios reales puedes soportar formatos como 1m30s y validaciones más estrictas, pero la idea clave es: todo entra por configuración y se valida al inicio.
Manejo de señales y apagado elegante (graceful shutdown)
En producción, tu proceso recibirá señales del sistema (por despliegues, escalado, mantenimiento). Un servicio robusto debe: dejar de aceptar tráfico, terminar requests en curso dentro de un timeout y cerrar recursos (archivos, conexiones, goroutines) sin corrupción.
Patrón recomendado
- Crear un
context.Contextraíz cancelable. - Escuchar
SIGINT/SIGTERMconsignal.NotifyContext. - Arrancar el servidor HTTP en una goroutine.
- Al cancelar, llamar
Server.Shutdown(ctxTimeout). - Cerrar dependencias (storage, colas, etc.) después del shutdown.
Ejemplo mínimo
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
errCh := make(chan error, 1)
go func() {
errCh <- srv.ListenAndServe()
}()
select {
case <-ctx.Done():
// señal recibida
case err := <-errCh:
if err != nil && err != http.ErrServerClosed {
return err
}
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
// cerrar storage, flush de logs, etc.Importante: Shutdown espera a que terminen requests activos (hasta el timeout). Si tienes goroutines internas (workers), también deben escuchar el ctx para detenerse.
Salud del servicio: health checks (liveness y readiness)
Los orquestadores (Kubernetes, ECS, Nomad) y balanceadores necesitan endpoints para decidir si un contenedor está vivo y si está listo para recibir tráfico.
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
- Liveness: “¿el proceso está funcionando?” Si falla, se reinicia.
- Readiness: “¿puede atender tráfico?” Si falla, se saca del balanceo sin reiniciar necesariamente.
Implementación práctica
Define dos rutas: /healthz (liveness) y /readyz (readiness). Liveness suele responder 200 si el proceso está arriba. Readiness valida dependencias mínimas (por ejemplo, acceso a persistencia).
type Readiness interface {
Ready(ctx context.Context) error
}
func healthz(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}
func readyz(dep Readiness) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond)
defer cancel()
if err := dep.Ready(ctx); err != nil {
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = w.Write([]byte("not ready"))
return
}
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ready"))
}
}La clave es que readiness sea rápido y no bloquee. Si tu dependencia es lenta, usa timeouts cortos y caché del estado.
Observabilidad mínima: logging estructurado, trazas básicas y métricas
Escalar implica entender qué ocurre sin entrar al servidor. La observabilidad se apoya en tres pilares: logs, trazas y métricas. Aquí implementaremos una base ligera y compatible con herramientas comunes.
Logging estructurado (JSON) con slog
El logging estructurado facilita filtrar por campos (request_id, status, latency). En Go moderno puedes usar log/slog.
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
logger.Info("service starting",
"env", cfg.Env,
"addr", cfg.HTTPAddr,
)Middleware de request logging
func requestLogger(logger *slog.Logger, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rid := r.Header.Get("X-Request-Id")
if rid == "" {
rid = strconv.FormatInt(time.Now().UnixNano(), 36)
}
ww := &wrapWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
logger.Info("http_request",
"method", r.Method,
"path", r.URL.Path,
"status", ww.status,
"bytes", ww.bytes,
"duration_ms", time.Since(start).Milliseconds(),
"request_id", rid,
)
})
}
type wrapWriter struct {
http.ResponseWriter
status int
bytes int
}
func (w *wrapWriter) WriteHeader(code int) {
w.status = code
w.ResponseWriter.WriteHeader(code)
}
func (w *wrapWriter) Write(p []byte) (int, error) {
n, err := w.ResponseWriter.Write(p)
w.bytes += n
return n, err
}Si tu plataforma inyecta un request id (por ejemplo, un API gateway), lo reutilizas; si no, lo generas.
Trazas básicas con OpenTelemetry (concepto y esqueleto)
Las trazas conectan eventos de una request a través de componentes (handlers, storage, llamadas HTTP). En un microservicio, esto es esencial para entender latencias y cuellos de botella. La implementación completa depende del exportador (OTLP, Jaeger, etc.), pero el patrón es: inicializar un tracer provider, instrumentar handlers y crear spans en operaciones relevantes.
// Pseudocódigo de instrumentación (sin exportador específico)
tracer := otel.Tracer("svc")
func handler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "GET /items")
defer span.End()
items, err := store.List(ctx)
if err != nil {
span.RecordError(err)
w.WriteHeader(500)
return
}
_ = items
}Incluso con instrumentación mínima (un span por handler y spans por I/O), ya obtienes valor: latencias por endpoint y errores asociados.
Métricas: contadores y latencias
Las métricas responden “¿cuánto?” y “¿con qué frecuencia?”. Un mínimo útil: contador de requests, contador de errores y un histograma (o resumen) de latencias por endpoint.
Si usas Prometheus, lo típico es exponer /metrics. A nivel conceptual:
http_requests_total{path,method,status}http_request_duration_seconds_bucket{path,method}db_errors_total
En el proyecto integrador verás una versión simple con contadores en memoria (útil para aprender el flujo). En producción, reemplázala por un cliente real de métricas.
Empaquetado y ejecución en cloud
Binarios estáticos y portabilidad
Go facilita desplegar un único binario. Para contenedores minimalistas (distroless/scratch) conviene compilar estático (cuando sea posible) y desactivar CGO.
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" -o app ./cmd/appSi dependes de librerías que requieren CGO, el binario puede no ser completamente estático. Aun así, el enfoque de “un binario por servicio” sigue siendo válido.
Variables de entorno y secretos
- Config no sensible (puerto, nivel de log): variables de entorno.
- Secretos (tokens, claves): gestor de secretos del proveedor o archivos montados; evita incluirlos en la imagen.
- Rotación: el servicio debe tolerar reinicios; por eso el shutdown elegante es clave.
Principios de microservicios aplicados
- Responsabilidad única: el servicio hace una cosa bien (por ejemplo, gestionar “items”). Evita mezclar dominios.
- Comunicación explícita: HTTP/JSON o gRPC; contratos versionados; timeouts siempre.
- Resiliencia: timeouts, límites de concurrencia, reintentos con backoff (solo cuando sea seguro), circuit breakers si aplica.
- Observabilidad desde el día 1: logs estructurados, health checks, métricas y trazas mínimas.
Proyecto integrador: servicio HTTP concurrente con persistencia simple, pruebas y observabilidad mínima
Construirás un servicio “Items” con operaciones básicas y componentes listos para producción: configuración por entorno, shutdown elegante, health checks, logging estructurado y métricas simples. La persistencia será un archivo JSON (simple pero suficiente para practicar patrones de I/O y concurrencia segura).
Objetivo funcional
POST /items: crea un item.GET /items: lista items.GET /items/{id}: obtiene un item.GET /healthzyGET /readyz.GET /metrics: métricas simples en texto.
Estructura sugerida
| Ruta | Responsabilidad |
|---|---|
/cmd/app/main.go | Wiring: config, logger, server, señales |
/internal/config | Carga/validación de configuración |
/internal/httpapi | Handlers, middlewares, rutas |
/internal/store | Persistencia (archivo JSON) + concurrencia |
/internal/obs | Métricas simples (contadores/latencias) |
Paso 1: modelo y store con concurrencia
El store debe ser seguro ante concurrencia (múltiples requests). Usaremos un sync.RWMutex y persistencia a disco con escritura atómica (escribir a archivo temporal y renombrar).
package store
import (
"context"
"encoding/json"
"errors"
"os"
"path/filepath"
"sync"
"time"
)
type Item struct {
ID string `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
type Store struct {
mu sync.RWMutex
path string
items map[string]Item
}
func New(path string) (*Store, error) {
s := &Store{path: path, items: map[string]Item{}}
if err := s.load(); err != nil {
return nil, err
}
return s, nil
}
func (s *Store) Ready(ctx context.Context) error {
// readiness simple: poder crear/abrir el archivo
s.mu.RLock()
defer s.mu.RUnlock()
_, err := os.Stat(s.path)
if err == nil {
return nil
}
if errors.Is(err, os.ErrNotExist) {
// si no existe, intentamos crear directorio
dir := filepath.Dir(s.path)
if dir != "." {
return os.MkdirAll(dir, 0o755)
}
return nil
}
return err
}
func (s *Store) load() error {
b, err := os.ReadFile(s.path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
var list []Item
if err := json.Unmarshal(b, &list); err != nil {
return err
}
for _, it := range list {
s.items[it.ID] = it
}
return nil
}
func (s *Store) saveLocked() error {
list := make([]Item, 0, len(s.items))
for _, it := range s.items {
list = append(list, it)
}
b, err := json.MarshalIndent(list, "", " ")
if err != nil {
return err
}
tmp := s.path + ".tmp"
if err := os.WriteFile(tmp, b, 0o644); err != nil {
return err
}
return os.Rename(tmp, s.path)
}
func (s *Store) Create(ctx context.Context, it Item) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.items[it.ID]; ok {
return errors.New("item ya existe")
}
s.items[it.ID] = it
return s.saveLocked()
}
func (s *Store) Get(ctx context.Context, id string) (Item, error) {
s.mu.RLock()
defer s.mu.RUnlock()
it, ok := s.items[id]
if !ok {
return Item{}, errors.New("no encontrado")
}
return it, nil
}
func (s *Store) List(ctx context.Context) ([]Item, error) {
s.mu.RLock()
defer s.mu.RUnlock()
out := make([]Item, 0, len(s.items))
for _, it := range s.items {
out = append(out, it)
}
return out, nil
}Paso 2: métricas simples en memoria
Para entender el flujo sin dependencias externas, implementa contadores y latencias agregadas. Luego podrás reemplazarlo por Prometheus/OpenTelemetry Metrics.
package obs
import (
"fmt"
"sync/atomic"
"time"
)
type Metrics struct {
Requests uint64
Errors uint64
LatencyMsTotal uint64
}
func (m *Metrics) IncRequests() { atomic.AddUint64(&m.Requests, 1) }
func (m *Metrics) IncErrors() { atomic.AddUint64(&m.Errors, 1) }
func (m *Metrics) AddLatency(d time.Duration) {
atomic.AddUint64(&m.LatencyMsTotal, uint64(d.Milliseconds()))
}
func (m *Metrics) Render() string {
req := atomic.LoadUint64(&m.Requests)
err := atomic.LoadUint64(&m.Errors)
lat := atomic.LoadUint64(&m.LatencyMsTotal)
avg := uint64(0)
if req > 0 {
avg = lat / req
}
return fmt.Sprintf("requests_total %d\nerrors_total %d\navg_latency_ms %d\n", req, err, avg)
}Paso 3: API HTTP concurrente + middlewares
Usa un ServeMux y compón middlewares: request logging y métricas. Para rutas con parámetro (/items/{id}) puedes parsear el path de forma simple.
package httpapi
import (
"encoding/json"
"net/http"
"strings"
"time"
"log/slog"
"example/internal/obs"
"example/internal/store"
)
type API struct {
Log *slog.Logger
Store *store.Store
Metrics *obs.Metrics
}
func (a *API) Routes() http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", healthz)
mux.HandleFunc("/readyz", readyz(a.Store))
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte(a.Metrics.Render()))
})
mux.HandleFunc("/items", a.items)
mux.HandleFunc("/items/", a.itemByID)
h := http.Handler(mux)
h = a.metricsMiddleware(h)
h = requestLogger(a.Log, h)
return h
}
func (a *API) metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
a.Metrics.IncRequests()
ww := &wrapWriter{ResponseWriter: w, status: 200}
next.ServeHTTP(ww, r)
a.Metrics.AddLatency(time.Since(start))
if ww.status >= 500 {
a.Metrics.IncErrors()
}
})
}
func (a *API) items(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
list, err := a.Store.List(r.Context())
if err != nil {
w.WriteHeader(500)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(list)
case http.MethodPost:
var in struct{ ID, Name string }
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
w.WriteHeader(400)
return
}
it := store.Item{ID: in.ID, Name: in.Name, CreatedAt: time.Now().UTC()}
if err := a.Store.Create(r.Context(), it); err != nil {
w.WriteHeader(409)
return
}
w.WriteHeader(201)
default:
w.WriteHeader(405)
}
}
func (a *API) itemByID(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(405)
return
}
id := strings.TrimPrefix(r.URL.Path, "/items/")
if id == "" {
w.WriteHeader(400)
return
}
it, err := a.Store.Get(r.Context(), id)
if err != nil {
w.WriteHeader(404)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(it)
}Paso 4: main wiring (config + server + señales)
package main
import (
"context"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"example/internal/config"
"example/internal/httpapi"
"example/internal/obs"
"example/internal/store"
)
func main() {
cfg, err := config.Load()
if err != nil {
panic(err)
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
st, err := store.New(cfg.DBPath)
if err != nil {
logger.Error("store init failed", "error", err)
os.Exit(1)
}
m := &obs.Metrics{}
api := &httpapi.API{Log: logger, Store: st, Metrics: m}
srv := &http.Server{
Addr: cfg.HTTPAddr,
Handler: api.Routes(),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
errCh := make(chan error, 1)
go func() { errCh <- srv.ListenAndServe() }()
logger.Info("service started", "addr", cfg.HTTPAddr, "env", cfg.Env)
select {
case <-ctx.Done():
logger.Info("shutdown signal received")
case err := <-errCh:
if err != nil && err != http.ErrServerClosed {
logger.Error("server error", "error", err)
os.Exit(1)
}
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
logger.Info("server stopped")
}Paso 5: pruebas esenciales (store y handlers)
En un servicio escalable, las pruebas mínimas deben cubrir: (1) persistencia y concurrencia del store, (2) contratos HTTP básicos (status codes y payloads). Aquí tienes dos ejemplos.
Prueba del store: create/get
package store_test
import (
"context"
"os"
"testing"
"time"
"example/internal/store"
)
func TestStoreCreateGet(t *testing.T) {
f, err := os.CreateTemp("", "items-*.json")
if err != nil { t.Fatal(err) }
path := f.Name()
_ = f.Close()
defer os.Remove(path)
s, err := store.New(path)
if err != nil { t.Fatal(err) }
it := store.Item{ID: "1", Name: "a", CreatedAt: time.Now().UTC()}
if err := s.Create(context.Background(), it); err != nil { t.Fatal(err) }
got, err := s.Get(context.Background(), "1")
if err != nil { t.Fatal(err) }
if got.Name != "a" { t.Fatalf("expected name a, got %s", got.Name) }
}Prueba HTTP: POST y luego GET
package httpapi_test
import (
"bytes"
"net/http"
"net/http/httptest"
"os"
"testing"
"log/slog"
"example/internal/httpapi"
"example/internal/obs"
"example/internal/store"
)
func TestItemsFlow(t *testing.T) {
f, _ := os.CreateTemp("", "items-*.json")
path := f.Name()
_ = f.Close()
defer os.Remove(path)
st, err := store.New(path)
if err != nil { t.Fatal(err) }
api := &httpapi.API{
Log: slog.New(slog.NewTextHandler(os.Stdout, nil)),
Store: st,
Metrics: &obs.Metrics{},
}
h := api.Routes()
req := httptest.NewRequest(http.MethodPost, "/items", bytes.NewBufferString(`{"id":"1","name":"x"}`))
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != 201 { t.Fatalf("expected 201, got %d", w.Code) }
req2 := httptest.NewRequest(http.MethodGet, "/items/1", nil)
w2 := httptest.NewRecorder()
h.ServeHTTP(w2, req2)
if w2.Code != 200 { t.Fatalf("expected 200, got %d", w2.Code) }
}Paso 6: ejecución por entorno (ejemplos)
Ejecutar en local:
export APP_ENV=dev
export HTTP_ADDR=:8080
export DB_PATH=./data/items.json
export LOG_LEVEL=info
export SHUTDOWN_TIMEOUT=10
go run ./cmd/appProbar endpoints:
curl -i http://localhost:8080/healthz
curl -i http://localhost:8080/readyz
curl -i -X POST http://localhost:8080/items -d '{"id":"1","name":"demo"}'
curl -i http://localhost:8080/items
curl -i http://localhost:8080/metricsChecklist de “listo para producción” para este proyecto
- Config por entorno validada al inicio.
- Time-outs HTTP configurados.
- Shutdown elegante con señales y timeout.
- Health checks separados (liveness/readiness).
- Logging estructurado por request.
- Métricas mínimas expuestas.
- Persistencia segura ante concurrencia y escritura atómica.
- Pruebas de store y flujo HTTP básico.