Cómo escribo servicios HTTP en Go después de 13 años

Los desarrolladores de Go están debatiendo cómo estructurar servicios HTTP para que sean a la vez testeables y mantenibles, centrándose en patrones como minimizar la lógica en `main`, pasar dependencias explícitamente a los handlers y evitar structs de configuración o de servidor “god” demasiado grandes. Muchos prefieren funciones pequeñas y componibles y una inyección de dependencias explícita (a veces mediante librerías como `fx` de Uber), mientras que otros advierten contra la sobreingeniería y prefieren `main` más simples y de estilo script para servicios a medida. Un tema recurrente es usar tipos y parseo (por ejemplo, “parse, don’t validate”) para codificar invariantes y reducir bugs, en lugar de depender de validaciones ad hoc dispersas por todo el código.

Estructura de main y capacidad de prueba

  • A muchos les gusta un main() pequeño que delega en una función run() para que los errores puedan devolverse y la mayor parte de la lógica se vuelva testeable y reutilizable.
  • Otros prefieren un estilo de “script plano” en main.go para servicios internos, manteniendo visible el flujo de control de nivel superior en lugar de empujarlo a helpers.
  • Consenso: mantener pequeño el código de arranque no testeable, pero no a costa de la legibilidad; main() en sí no puede llamarse desde tests, lo que motiva la indirection.

Handlers, dependencias y DI

  • Fuerte apoyo a las dependencias explícitas: los handlers deberían recibir lo que necesitan mediante argumentos o structs por handler, no oculto en un gran struct de servidor.
  • Algunos quieren “claridad brutal” incluso si eso significa muchos parámetros; otros agrupan dependencias en structs de contexto/entorno o structs de handler para evitar firmas enormes.
  • Hay tensión entre simplicidad y acoplamiento: los grandes structs de servidor “god” y los constructores con muchos args huelen a sobreacoplamiento.

Configuración y patrones de opciones

  • Pasar un objeto global mutable de configuración por todas partes es ampliamente criticado; acopla subsistemas y puede causar errores sutiles de orden de ejecución.
  • Patrones preferidos: configuración inmutable o “congelada”; copiar la config en campos internos; structs de configuración por paquete; y “functional options” o structs de opciones para constructores.
  • Preocupación: los patrones de opciones demasiado ingeniosos añaden boilerplate y perjudican la descubribilidad; los structs de configuración simples suelen ganar a largo plazo.

Validación vs “parse, don’t validate” y tipos

  • Muchos abogan por envolver primitivas (por ejemplo, Username) en tipos dedicados construidos mediante parseo/validación para que los estados inválidos no puedan representarse.
  • Debate sobre los valores cero de Go: socavan las garantías cuando los structs pueden instanciarse sin pasar por constructores.
  • Algunos ven este patrón como sobreingeniería o engorroso en Go; otros lo ven como esencial para evitar bugs “stringly typed”.

Estrategias de testing

  • Varios prefieren tests al estilo end-to-end que levantan un servidor de pruebas con dependencias falsas, ejercitando HTTP y los caminos del middleware.
  • Otros se centran en testear la lógica de los handlers por unidad, inyectando dependencias y usando pequeñas funciones auxiliares.

Spec-first y generación de código

  • A algunos participantes les gustan los flujos OpenAPI-first con generadores (por ejemplo, ogen, oapi-codegen), para evitar código repetitivo de validación y routing.
  • Otros encuentran tediosa la autoría de OpenAPI, pero señalan que las herramientas y la autoría asistida por LLM ayudan, y que los IDL (incluido gRPC/gateway) pueden ser productivos.

Frameworks de inyección de dependencias

  • Los frameworks DI ligeros (por ejemplo, fx) son elogiados por manejar grafos complejos de inicialización/apagado.
  • Los escépticos argumentan que los frameworks DI añaden indirection; prefieren la construcción explícita en main, afirmando que los grafos acíclicos y el teardown manual siguen siendo manejables.