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ónrun()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.gopara 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.