En una aplicación Go, hay un contexto que hace las cosas más interesantes. Un contexto transporta plazos, señales de cancelación y otros valores relacionados con las peticiones a través de los límites de la API y entre procesos. Cuando se trabaja con una petición se puede utilizar para cancelar o poner en timeout la petición. Una cancelación puede ocurrir por muchas razones diferentes. El usuario puede cerrar su navegador o hacer clic en cancelar mientras se procesa la solicitud. Podría haber una mala conexión a Internet o algún otro problema de red. Un cliente podría decidir que la solicitud está tardando demasiado y cerrarla. Independientemente de la razón, el contexto indicará que ha terminado. Esto significa que el trabajo que aún no se ha completado puede ser omitido.
Algunos paquetes para bases de datos, clientes http y otros requerirán que se les pase un contexto. Los otros probablemente tendrán una forma de establecerlo. Al principio no entendía por qué. Ya había un contexto establecido en la solicitud, así que simplemente lo pasé. Cuando un contexto se cancelaba empezábamos a ver errores de contexto cancelado. Esto se debe a que estos paquetes actuaban sobre la señal de contexto cancelado deteniendo todo su procesamiento y devolviendo un error.
ctx := request.Context()
// buscar una persona en la base de datos
persona, err := databaseCall(ctx)
...
// databaseCall Busca una persona en la base de datos.
funcdatabaseCall(ctxcontext.Context) (Persona, error) {
logInfo(ctx, "Realizando la llamada a la base de datos")
var persona Persona
// pausa un poco para permitir la cancelación del contexto
err := pause(ctx)
if err != nil {
return persona, err
}
// el popular paquete de base de datos pgxpostgres requiere que se establezca un contexto en la mayoría de las operaciones
connection, err := pgx.Connect(ctx,
"postgres://postgres:postgres@localhost:5432/postgres")
if err != nil {
// además de los errores habituales si el paquete pgx se da cuenta de que el contexto está hecho devolverá un error
return persona, err
}
defer connection.Close(ctx)
// consulta la base de datos en busca de una persona y rellena sus valores struct
err = connection.QueryRow(ctx, "select name from
people").Scan(&person.Name)
return persona, err
}
Para ver el código completo echa un vistazo a mi aplicación de ejemplo en GitHub en https://github.com/paul-ferguson/the-go-context
Observa cómo esta aplicación comprueba si la llamada a la base de datos devuelve un error. Si hay un error comprobamos si el contexto está hecho. Si lo está, la aplicación no realiza ningún otro proceso y simplemente devuelve. Ni siquiera necesita devolver un código de error http ni ninguna respuesta JSON.
Si quieres que una aplicación Go funcione como una aplicación Java necesitas usar el contexto apropiado. Digamos que tenemos un endpoint REST que está procesando una post llamada para guardar algunos datos. Querremos que se procese completamente y se persista en la base de datos independientemente de si se cancela el contexto de la petición. En este caso, podemos utilizar context.Background() para crear un nuevo contexto que nunca pueda ser cancelado. He mostrado un ejemplo de esto a continuación. También he incluido en los comentarios del código el método context.WithCancel()
,
context.WithDeadline()
o context.WithTimeout()
. Definen respectivamente un método de cancelación y una fecha límite o tiempo de espera/duración que activará la señal de contexto realizado. A context.Background()
sólo debería usarse en ciertos escenarios: funciones principales, inicialización y pruebas, y como Contexto de nivel superior para peticiones entrantes. Nuestro caso de uso es este último. En la mayoría de las situaciones, la mejor práctica es utilizar una de las otras opciones.
ctx:= context.Fondo()
/* esto: Pruebe estas otras opciones en su lugar:
// esto devuelve un contexto y una función que llamamos cancelar
ctx, cancel := context.WithCancel(request.Context())
// llamar a cancel activará la señal done
cancelar()
// esto activará un tiempo de espera después de 2 segundos
ctx, cancel := context.WithTimeout(request.Context(), time.Second * 2)
// del documento Go: Aunque ctx expire, es una buena práctica llamar a su función de cancelación en cualquier caso. No hacerlo puede mantener el contexto y su padre vivos más tiempo del necesario. defer cancel() // esto disparará un timeout después de un tiempo que es 2 segundos en el futuro ctx, cancel := context.WithDeadline(request.Context(), time.Now().Add(time.Second * 2)) aplazar cancel()
*/
...
// restCall Busca a una persona haciendo una llamada rest.
funcrestCall(ctxcontext.Context) (Persona, error) {
logInfo(ctx, "Realizando la llamada rest")
var persona Persona
// crear la petición get al endpoint del lado del servidor
request, err := http.NewRequestWithContext(ctx, "GET",
"http://localhost:8080/server-side-get", nil)
/*
prueba esto: Si no pasamos el contexto, la petición no se cancelará cuando se produzca una señal de "done". La petición
solicitud se procesará completamente desperdiciando recursos.
request, err := http.NewRequest("GET", "http://localhost:8080/server-side-get", nil)
*/
// pasamos el identificador de la petición en la cabecera, lo que nos permite rastrear esta petición
request.Header.Add(requestIDHeaderKey, ctx.Value(requestIDContextKey).(string))
// realiza la petición
response, err := http.DefaultClient.Do(request)
if err != nil {
//todo
return persona, err
}
//leer el cuerpo completo de la respuesta
body, err := io.ReadAll(response.Body)
if err != nil {
return persona, err
}
// cerrar el cuerpo de la respuesta
err = response.Body.Close()
if err != nil {
return persona, err
}
// unmarshal el contenido del cuerpo de la respuesta a una estructura de persona
err = json.Unmarshal(cuerpo, &persona)
return persona, err
}
Para ver el código completo echa un vistazo a mi aplicación de ejemplo en GitHub en https://github.com/paul-ferguson/the-go-context
Finalmente, para completar, debería mostrar cómo el contexto puede ser usado para pasar valores de solicitud a través de los límites de la API y entre procesos.Anteriormente he utilizado esto para registrar valores comunes, como un id de solicitud.He aquí un ejemplo.
const requestIDHeaderKey= "request-id"
const requestIDContextKey= contextKey(requestIDHeaderKey)
...
// establecer el id de la petición como valor en el contexto
requestId:= request.Header.Get("request-id")
if requestId== "" {
// no se ha establecido ningún id de petición, así que crea uno único
requestId= uuid.New().String()
}
ctx= context.WithValue(ctx, requestIDContextKey, requestId)
logInfo(ctx, "Se ha llamado a Get")
...
// aquí hay muchos paquetes de logging que podríamos haber usado, pero enrollando el nuestro para mayor claridad en este ejemplo
funclogInfo(ctxcontexto.Contexto, mensaje cadena) {
fmt.Println("info", mensaje, ctx.Value(requestIDContextKey))
}
Para ver el código completo echa un vistazo a mi aplicación de ejemplo en GitHub en https://github.com/paul-ferguson/the-go-context