- Los servicios HTTP en Go que se mantienen por mucho tiempo son más fáciles de mantener y verificar cuando se construyen con inyección explícita de dependencias, rutas reunidas en un solo lugar y una función
runque se puede probar - En lugar de métodos de una estructura de servidor, los handlers se crean como funciones que devuelven
http.Handlery reciben por cierre los valores que necesitan; el middleware compartido se compone en la creación del servidor y en el registro de rutas - Mantener
func main()liviana e inyectar enrun()uncontext.Context, argumentos, acceso al entorno y entrada/salida estándar simplifica el manejo del apagado y el control en pruebas - Helpers para codificar/decodificar solicitudes y respuestas, validación, adaptadores de middleware e inicialización diferida con
sync.Oncereducen código repetido sin salir del flujo estándar denet/httpen Go - Para pruebas, se prefiere un enfoque end-to-end más cercano a llamadas reales de API que probar handlers individuales; cada prueba levanta su propio servidor y verifica su disponibilidad con
/healthzo/readyz
Creación del servidor y punto de entrada del servicio
- El constructor
NewServerse define como la función que crea elhttp.Handlerprincipal del servicio- Normalmente hay uno por servicio, y las rutas internas desvían cada solicitud a su handler correspondiente
- Recibe como argumentos todas las dependencias, como logger, configuración, repositorios y clientes externos
- Siempre que sea posible, conviene devolver un
http.Handler; en casos más complejos puede usarse un tipo dedicado - Configura su propio muxer y luego lo pasa a una función de registro de rutas en
routes.go
- Todo el procesamiento HTTP común a todos los endpoints se agrupa en
NewServer- CORS
- middleware de autenticación
- logging
- middleware de ID de trazabilidad
- Aunque la lista de dependencias sea larga, se prefiere hacerla explícita como argumentos de función
- Si se omite un campo de una estructura, el compilador podría no impedirlo, pero una función no se puede llamar si no recibe los valores necesarios
- Una lista larga de argumentos sigue siendo legible si se formatea en vertical
- Las dependencias que no se usan en una prueba específica se pasan como
nilpara indicar que no deberían utilizarse
Reunir la superficie de la API en routes.go
routes.gose mantiene como el archivo donde se pueden ver todas las rutas del servicio en un solo lugar- Cada proyecto gana un punto único para recorrer la superficie de la API
- Debido a la gran lista de dependencias en
NewServer,addRoutestambién puede terminar con una lista similar - La verificación de tipos de Go detecta argumentos faltantes o en orden incorrecto
- Siempre que sea posible,
addRoutesdebe mantenerse simple y plana- Todo lo que pueda fallar se resuelve antes en la función
run - En la etapa de registro, el enfoque debe estar en el ruteo con
mux.Handle,mux.HandleFuncyhttp.NotFoundHandler - Si el diseño exige que los handlers puedan devolver errores,
addRoutestambién puede devolverlos
- Todo lo que pueda fallar se resuelve antes en la función
main solo llama a run
func main()se deja como una función ligera que llama arun(), escribe el error enstderrsi ocurre y termina de forma anormalrunrecibe como argumentos uncontext.Context, argumentos, entrada/salida y funciones de acceso al entorno, es decir, los elementos base que ofrece el sistema operativo- Como
rundevuelve un error, se puede manejar como cualquier otro error en Go
- Ejemplos de valores que pueden pasarse a
runos.Args: para argumentos del programa y parsing de flagsos.Stdin: para leer entradaos.Stdout: para escribir salidaos.Stderr: para escribir logs de erroros.Getenv: para leer variables de entornoos.Getwd: para obtener el directorio de trabajo actual
signal.NotifyContextse configura dentro derun- Si entra una señal de salida como
Ctrl+C, el contexto se cancela - Si
rundevuelvenil, el proceso termina normalmente - Si devuelve un error,
mainlo imprime y sale con un código distinto de 0
- Si entra una señal de salida como
- Evitar estado global permite usar
t.Parallel()en más pruebas- Incluso si
runse llama varias veces, cada ejecución no interfiere con las demás - Los flags se manejan con
flags.NewFlagSetdentro derunen lugar delflagglobal - Las variables de entorno se controlan inyectando
getenv func(string) stringen vez de modificar el entorno real - A diferencia de
t.SetEnv, esto permite seguir usando pruebas en paralelo
- Incluso si
Manejo del apagado y estado de disponibilidad
- El contexto debe propagarse a todas las capas del servicio
- Cuando llega una señal de salida, el contexto se cancela
- Los trabajos largos o repetitivos deben revisar
ctx.Err()octx.Done()y detenerse - Incluso si se lanzan otras goroutines, el contexto sirve para decidir cuándo deben parar
- Al cerrar el servidor HTTP, se llama a
Shutdownpara detenerlo de forma ordenada- En el ejemplo, una goroutine separada espera
ctx.Done() - El contexto de cierre usa un timeout de
10 * time.Second - Si ocurre un error durante el cierre, se registra en
stderr
- En el ejemplo, una goroutine separada espera
- Para confirmar en pruebas que el servidor realmente está listo, se exponen endpoints como
/healthzo/readyz- También podría usarse una señal de disponibilidad por otro canal, pero se prefiere comprobarlo con una solicitud HTTP real
- El bucle de verificación sigue haciendo solicitudes hasta recibir
200 OK - Si se cancela el contexto o se alcanza el timeout, devuelve un error
- En el ejemplo, el bucle espera
250msentre intentos
Forma de componer handlers
- En vez de implementar directamente
http.Handlerohttp.HandlerFunc, las funciones handler los devuelven- Ejemplo:
func handleSomething(logger *Logger) http.Handler - Esto permite construir un entorno de cierre específico para cada handler
- Los valores inicializados pueden reutilizarse al procesar solicitudes
- Ejemplo:
- Es más seguro usar los datos compartidos solo en modo lectura
- Si un handler modifica valores, se necesitan protecciones como un mutex
- En general no se recomienda guardar el estado del programa dentro del cierre
- En entornos cloud no conviene asumir que una instancia vivirá mucho tiempo
- El servidor puede apagarse para ahorrar recursos o caerse por otros motivos
- Puede haber varias instancias ejecutándose al mismo tiempo y distribuyendo solicitudes de forma impredecible
- El estado persistente real del proyecto conviene guardarlo en una base de datos o en una API de almacenamiento separada
Codificación/decodificación de solicitudes y respuestas, y validación
- Como todo servicio necesita decodificar el cuerpo de las solicitudes y codificar el de las respuestas, se definen helpers
encode/decode- El ejemplo establece el
Content-Typede JSON, escribe el código de estado y luego llama ajson.NewEncoder(w).Encode(v) - La decodificación envuelve
json.NewDecoder(r.Body).Decode(&v)y agrega contexto al error - Con genéricos, se puede inferir el tipo en llamadas como
encode(w, r, http.StatusOK, obj) - Como
decodedevuelve un tipo, hay que especificar el tipo esperado, por ejemplodecode[CreateSomethingRequest](https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/r)
- El ejemplo establece el
- Para validación se usa una interfaz de un solo método
- La interfaz
Validatortiene la formaValid(ctx context.Context) map[string]string - Si no hay problemas, se devuelve un map de longitud 0
- Si los hay, el nombre del campo va como key y una descripción legible por personas como value
- La interfaz
- Lo que se valida aquí son revisiones rápidas de campos
- que los campos requeridos no estén vacíos
- que una cadena específica, como un correo, tenga el formato correcto
- que un número esté dentro del rango permitido
- Las validaciones más complejas, como consultas a base de datos, se resuelven en otro lugar
- Ese tipo de validaciones son demasiado importantes para ocultarlas dentro de una función de validación rápida
- La versión genérica
decodeValid[T Validator]obliga a que el tipoTimplementeValidator - Llamar a
len(problems)sobre un mapnildevuelve 0, así que no produce panic
Patrón adaptador de middleware
- El middleware recibe un
http.Handlery devuelve un nuevohttp.Handler- Puede ejecutar código antes y después de llamar al handler original
- Según la condición, incluso puede no llamar al handler original
- En el ejemplo,
adminOnlydevuelveHTTP 404 Not Foundsi no es administrador y no llama al handler original
- El lugar habitual para aplicar middleware es
routes.go- Con solo ver la lista de endpoints ya se entiende qué middleware tiene cada ruta
- Si la lista de middleware crece, puede dividirse en varias líneas para mejorar la lectura
- Cuando un middleware tiene muchas dependencias, se envuelve en una función que devuelve middleware
newMiddleware(logger, db, slackClient, rroll)devuelvefunc(http.Handler) http.Handler- En el registro de rutas se usa de forma compacta, como
middleware(handleSomething(...)) - También podría definirse un
type middleware func(h http.Handler) http.Handler, pero usar el tipo de retorno explícito hace el código más claro de leer
Reducir el alcance de los tipos de solicitud/respuesta
- Los tipos de solicitud o respuesta que solo se usan en un endpoint específico pueden definirse dentro de la función handler
- Así se mantiene limpio el namespace global
- También evita que otros handlers dependan de tipos cuya estabilidad no está garantizada
- Esto puede generar fricción si el mismo tipo hace falta en las pruebas
- En ese caso, es válido mover el tipo hacia afuera
- Si el tipo queda dentro del handler, en las pruebas se puede declarar una nueva estructura anónima o un tipo local
- Los tipos locales en pruebas hacen más explícita la intención
- Por ejemplo, si el endpoint
/greetsolo necesita el campoNamey no unPersoncompleto, la estructura de entrada de la prueba puede tener soloName - Quien lea la prueba entiende de inmediato qué campos le importan a ese endpoint
- Por ejemplo, si el endpoint
Diferir la inicialización con sync.Once
- Si durante la preparación del handler hay trabajo costoso, puede aplazarse hasta la primera solicitud usando
sync.Once- Esto reduce el tiempo de arranque de la aplicación
- Si nunca se llama al handler, el trabajo costoso nunca se ejecuta
- En el ejemplo, el parseo del archivo de plantilla se hace una sola vez en la primera solicitud
sync.Oncegarantiza que el código se ejecute una sola vez- Otras solicitudes concurrentes esperan a que termine la inicialización
- La revisión del error se hace fuera de
init.Dopara que el error siga saliendo a la superficie
- Este enfoque mueve el tiempo de inicialización desde el arranque al primer acceso en runtime a ese endpoint
- En entornos donde se usa mucho Google App Engine, este enfoque puede encajar bien
- Según el entorno de despliegue, hay que decidir dónde y cuándo usar
sync.Once
Estrategia de pruebas
- Esta estructura pone la facilidad de prueba como un objetivo importante
- La función
runpermite ejecutar directamente el programa desde el código de prueba - Las pruebas se evalúan según si facilitan entender el comportamiento del programa, reducen el temor a romper algo al cambiarlo y dan confianza para desplegar a producción después de pasar
- La función
- También es posible probar handlers de forma aislada
- Se llama a la función que crea el handler y se le pasan las dependencias necesarias
- Se construyen solicitud y respuesta con
httptest.NewRecorderyhttp.NewRequest - Luego se validan el código de estado, el cuerpo de la respuesta y los headers
- Este enfoque entra directo al código del handler, saltándose middleware como autenticación
- Pero se prefiere más un enfoque cercano a pruebas end-to-end
- Se llama a
runpara levantar el programa de una forma cercana a su ejecución real - Incluye parsing de argumentos, conexión de dependencias, migraciones de base de datos e inicio del servidor
- Cuando la prueba llama a la API, se validan todas las capas junto con
routes.go - Incluso puede interactuar con una base de datos real
- Se llama a
- Este enfoque ayuda a reducir pruebas repetitivas
- Si cada capa se prueba por separado, el mismo contenido puede verificarse varias veces de formas ligeramente distintas
- Las pruebas end-to-end ofrecen un conjunto central de pruebas que describen la interacción entre el usuario y el sistema
- Las pruebas unitarias ya existentes, por ejemplo surgidas de TDD, pueden mantenerse si siguen siendo útiles, pero si repiten lo mismo que las end-to-end, pueden eliminarse
- Cada prueba puede ejecutar su propia instancia del programa
- En cada prueba se pasan distintos argumentos, flags, entrada/salida estándar y variables de entorno
- Se crea una función de cancelación con
context.WithCancely se registra ent.Cleanup(cancel) - Al terminar la prueba, el contexto se cancela y el programa se apaga de forma ordenada
t.Cleanupde Go 1.14 se usa como alternativa a escribirdefermanualmente
Alcance real de aplicación y contexto organizacional
- Al crear una API simple, este patrón apunta a un código fácil de leer y de ampliar
- Es fácil copiar el patrón y extenderlo
- Facilita que gente nueva trabaje sobre él
- Reduce la ansiedad al hacer cambios
- Todo se arma de forma explícita, sin comportamiento mágico
- Incluso usando herramientas de generación de código, este enfoque puede mantenerse
- Como ejemplo, para generar boilerplate basado en plantillas puede usarse Oto package
- En proyectos grandes o en organizaciones grandes, las decisiones pueden cambiar según la tecnología ya adoptada
- En organizaciones como Grafana Labs, ciertas herramientas y abstracciones pueden estar ampliamente establecidas
- gRPC es un ejemplo de eso
- Cuando ya existen patrones consolidados y experiencia acumulada, seguir ese flujo puede ser la elección más práctica
- También se incluye el contexto de la línea de productos Grafana IRM
- Grafana IRM es una línea de productos que Grafana Labs está construyendo
- Grafana Alerting envía alertas cuando una métrica sale del rango permitido
- Grafana OnCall automatiza el proceso de contactar a la persona adecuada mediante horarios y reglas de escalamiento
- Grafana Incident crea una sala de Zoom, un canal dedicado de Slack y una línea de tiempo de eventos para ayudar en la respuesta a incidentes
- Los elementos marcados con una reacción de emoji de cara de robot en un canal de Slack se agregan a la línea de tiempo
1 comentarios
Opiniones de Hacker News
También probé el enfoque de tener un validador aparte, como un método
Valid, pero desde que leí “Parse, Don’t Validate” [0] de Lexi Lambda, siento que aprovechar el verificador de tipos de Go produce muchos menos erroresPor ejemplo, si quieres asegurarte de que un usuario nunca pueda especificar un nombre de usuario inválido que contenga corchetes angulares, con el enfoque del validador tienes que llamar al validador en todas las rutas de código donde el nombre de usuario provenga de una entrada no confiable
En cambio, si tienes un tipo
Usernamey un constructorNewUsername(username string) (Username, error), el simple hecho de que exista un objetoUsernameya te garantiza que pasó la validación[0] https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
string; hay que parsearlo y asignarle un tipoEste patrón permite hacer la validación tan temprano o tan tarde como se quiera, pero no indica cuándo conviene hacerla. Por lo general, muchas veces lo mejor es hacerla como parte del proceso de parsear/validar un objeto más grande
Para la idea de manejar datos no validados en el contexto de una UI, vale la pena consultar “I is for Intent” [1] de Steven Witten
[1] https://acko.net/blog/i-is-for-intent/
Usernameestá incluido dentro de una estructura y olvidas establecer el valor, termina entrando un zero value que puede violar las restriccionesUno de los patrones que más detesto es recibir un objeto Config que representa la configuración de todo el sistema y pasarlo de un lado a otro de forma mutable
Al hacer eso, todo queda acoplado a través del objeto de configuración. En un sistema, alguien volvía a escribir valores en el objeto de configuración que recibía, de modo que, para que todo funcionara correctamente, cada parte tenía que configurarse en un orden específico
En otro caso, un subsistema escribía en el objeto de configuración datos que otro leería después, lo que hacía imposible desactivar una parte del sistema
El patrón de “la configuración es un único gran valor mutable” es bastante molesto no solo en Go, sino también en otros lenguajes
En proyectos de Python se usan mucho
dataclassde configuración inmutables y se pasan a varios módulos; cuando varias funciones dependen de varios valores, en vez de pasar cada uno como argumento de función y definir tipos por separado, tener todas las variables y definiciones de tipos en un solodataclasstermina siendo un patrón de diseño bastante prácticoOptionLas
optionsinternas solo se modifican durante la creación, y hacia afuera solo se exponenConfigy sus accesores. Por ejemplo, se podría crear conconfig.New(config.Name("Emanon"))y leer concfg.Name()Configpor cada paquete, y hacer queconfigs.Configsea una recopilación de lasConfigde cada paqueteQuizá no sea una buena práctica en Go, pero al inicio permite construir la configuración completa del sistema como una sola entidad, y pasar a cada paquete solo las dependencias mínimas que necesita
También facilita un poco las pruebas, porque ya no hace falta simular toda la configuración solo para probar un paquete
Nunca debe modificarse. No se sabe dónde ni cómo se usa, así que es una lástima todo el costo humano que se desperdicia; si hace falta un cambio, es mejor crear un valor derivado del original
Lo gracioso es que, por diseño, el objeto de configuración era en cierto modo inmutable, y para modificarlo había que usar una API
WARNING_DO_NOT_USE, pero aun así la usé para cambiar el objeto y terminé causando el incidenteMe gusta mucho el trabajo de Mat Ryer, y desde entonces he aplicado la mayoría de las ideas de la versión de 2018 de este artículo a todos mis proyectos en Go.
Pero siempre me incomodó que
NewServerfuera un constructor grande que recibe todas las dependencias como argumentos, y que en las pruebas se pasenilpara señalar que una dependencia no se necesita.Como resultado, una gran parte del código termina con mucho estado compartido innecesario. En la práctica, hay muchos handlers HTTP que solo necesitan verificar si el usuario de la solicitud puede acceder a un recurso y llamar a una única función del almacén de datos, pero terminan siendo parte de un bloque enorme con acceso a todos los objetos del servidor padre y a todo el almacén de datos.
Incluso si solo quieres mockear dos métodos para probar, es difícil escribir una prueba simple; el patrón de Mat Ryer es el mejor que he visto hasta ahora, pero queda la sensación de que debe haber una solución mejor.
Si es posible, los plugins son una buena estrategia de límites de código. Una arquitectura de plugins está, por defecto, desactivada, y solo se hace visible cuando se opta explícitamente por usarla, así que no obliga a ningún bloque de código a cargar con todas las posibilidades.
A este tipo de software lo estoy llamando “a la carta”. En general, hay que evitar la situación de “hacer todo para poder hacer cualquier cosa”.
userso un paquetecomments.Estos paquetes no tienen ninguna interfaz HTTP, pero cada uno tiene su propio
mainy una especie de interfaz CLI. El comentario de archivo//go:build ignoreresulta útil para eso.En vez de definir un handler como
func HandleX(w http.ResponseWriter, req *http.Request), hago que reciba las dependencias necesarias con una forma comofunc HandleX(store *DataStore, dep1 Foo, dep2 Bar, commonDep Common) http.HandlerFuncy que devuelva internamente elhttp.HandlerFuncreal.Luego lo inicializo una sola vez en el punto de entrada.
NewServerestá haciendo demasiadas cosas. Es muy probable que se estén acoplando demasiados tipos de datos y comportamientos.Como ejemplo simple, si agregas un logger como dependencia del constructor, el objeto pasa a hacer un poco más que la implementación simple original. Eso en sí está bien, pero es una lástima si no puedes encontrar una forma de registrar logs sin modificar la implementación de lo simple.
Las funciones de orden superior, por ejemplo un decorador de logger, permiten la composición, aunque también tienen desventajas. Aun así, son una forma de estructura manejable, no un error.
La idea central es validar los valores de la estructura de configuración opcional dentro de
NewServery luego copiarlos a la estructura del servidor. Así se pueden mockear menos dependencias y las pruebas se vuelven mucho más fáciles.También probé mucho el patrón de opciones funcionales, como recomiendan varias personas, pero terminé abandonándolo. Me pareció un poco demasiado ingenioso y difícil de leer, y con más boilerplate que el patrón de estructura de configuración + validación y copia.
[0] https://news.ycombinator.com/item?id=39320170
Ojalá esta idea se aceptara más ampliamente para servicios HTTP en cualquier lenguaje: si un handler necesita dependencias, debería pedirlas directamente como argumentos, no quedar como un método colgado de una estructura de servidor que introduce dependencias sorpresa al probar.
Los handlers de un servicio HTTP suelen contener bastante lógica de negocio, y es probable que esa lógica tenga muchas dependencias. En la práctica, veo con frecuencia un solo handler que usa una DB, caché, almacenamiento de blobs, verificación de permisos específica del endpoint, verificador de licencias, cola, logger especial, cliente de métricas, etc.
Puede haber 9 o más parámetros, y los linters o las reglas empíricas suelen intentar impedirlo, pero las dependencias no desaparecen: solo se esconden en la clase/estructura
servery se finge que hay pocas dependencias porque la firma del método es corta.Con el tiempo, aunque lleguen a ser 20, me parece mejor el código donde todas las dependencias aparecen en la firma de la función/método. Así no se disimula el crecimiento de la complejidad del código.
Por ejemplo, dentro de una estructura
CreateUserpongo solo las dependencias necesarias para esa operación, comostore,cache,logger,pub, e implementoServeHTTP.En
main.goo donde se configuran las dependencias, creo cada operación y le paso solo las dependencias que necesita. Esto es bueno porque permite mantener los métodos helper de una operación/handler específico como métodos privados de esa estructura.Pero si una operación necesita otra, puede volverse engorroso porque empiezas a pasarlas entre sí o tienes que extraerlas a un paquete/servicio separado.
Por ejemplo, si escribes algo como
handleHello({ db, cache, blobStore, authz }, req, res), puedes reutilizarlo cuando dos handlers usan exactamente el mismo contexto, y también es fácil declarar el contexto de cada handler en el punto de llamada.Estoy de acuerdo con gran parte de este artículo y quisiera agregar algunas cosas más.
Si pasas un WaitGroup junto con el contexto de la app a la estructura del servicio, una interrupción puede activar el cierre de la app a través del contexto y la goroutine principal puede esperar el WaitGroup antes de salir realmente.
En un programa CLI es útil probar stdout, stdin, stderr, args, env, etc., pero en un servidor HTTP me parece menos relevante. A la función
runle pasaría una configuración estructurada para que las pruebas queden más enfocadas.Me opongo a parsear plantillas en el handler con
sync.Once. Creo que el handler no debería parsear plantillas; eso debería hacerse al iniciar la app. Si las plantillas no se pueden parsear, la app no debería quedar lista para recibir solicitudes y debería terminar con un código de salida distinto de 0.Últimamente estuve jugando con ogen: https://github.com/ogen-go/ogen
Si escribes una definición OpenAPI, se encarga del routing, las definiciones de structs, la validación de esquemas JSON, etc. Lo único que me toca hacer es implementar el servicio.
Cosas como validar rangos de enteros en query strings son demasiado tediosas y, si las escribes a mano, es facilísimo cometer errores de tipeo.
Todavía estoy en etapa de probarlo, así que no he encontrado partes malas.
Escribir IDL similares, como Protobuf o capnproto, se siente mucho más productivo.
[1] https://github.com/danielgtaylor/huma
[2] https://github.com/swaggest/rest
Pensé que escribir la especificación sería tedioso, pero resultó bastante mejor de lo esperado y, como de todos modos necesitas la especificación, creo que es mejor escribirla de antemano.
Siento que fx (https://github.com/uber-go/fx) es una herramienta muy simple y versátil para diseñar aplicaciones.
Los consejos del artículo siguen siendo útiles, pero elimina por completo la parte de “¿cómo garantizo que X esté inicializado cuando Y lo necesita?”. El problema N*M se reduce a un problema N: solo tienes que preocuparte por cómo inicializar cada pieza, sin tener que preocuparte por sincronizar la inicialización entre las piezas.
He usado bastantes bibliotecas de inyección de dependencias en varios lenguajes e incluso he implementado algunas, pero la simplicidad y generalidad de fx es lo que más me ha gustado hasta ahora.
En un sistema bien diseñado, este problema debería ser trivial. Garantizar que algo esté inicializado cuando quieres usarlo es una cuestión de que esté listo para pasarlo como argumento al constructor.
Puedes hacerlo así:
stockService := NewStockService(),orderService := NewOrderService(),orderProcessor := NewOrderProcessor(stockService, orderService).No debería hacer falta ninguna “sincronización” de inicialización y, si te equivocas, no compila. Si agregas una dependencia circular, también queda claro porque no puedes construirlo en el orden correcto.
Es un gran artículo con muchas ideas interesantes. No puedo creer que no conociera
signal.NotifyContext.Ahora podré recordar cómo hacer manejo de señales sin copiar y pegarlo en cada proyecto.
Me gusta mucho el enfoque que se usa aquí, pero mis pruebas son un poco distintas.
En
newTestServer()levanto un servidor con dependencias falsas y, si quiero probar un error de dependencia, cambio esa propiedad por un fake que devuelve un error.Así puedo verificar rutas de error, entradas de log, emisión de métricas, timeouts e incluso graceful shutdown.
Después de que el servidor arranca, reviso a qué puerto quedó enlazado, porque el valor predeterminado es
:0y hay que esperar al puerto realmente asignado.Las pruebas “unitarias” pueden hacerse a nivel de handler o a nivel HTTP, pasando por todo el middleware o por ninguno, y aun así probar el código suficientemente de la forma en que lo verá el usuario. También es posible levantar N instancias y probarlas en paralelo.
No uso Go, pero me gustan estos patrones. Siento que se aplican de forma bastante universal al código testeable.
No quiero volver a ver guías de inicio rápido, especialmente de Python, que tratan las dependencias de forma implícita/estática/imposible de probar.