Comprender el paquete de contexto en Golang

imagen


El paquete de contexto en Go es útil para las interacciones con API y procesos lentos, especialmente en los sistemas de grado de producción que se ocupan de las solicitudes web. Con su ayuda, se puede notificar a los goroutines sobre la necesidad de completar su trabajo.


A continuación hay una pequeña guía para ayudarlo a usar este paquete en sus proyectos, así como algunas de las mejores prácticas y dificultades.


(Nota: El contexto se usa en muchos paquetes, por ejemplo, al trabajar con Docker ).


Antes de empezar


Para usar contextos, debes comprender qué son los goroutine y los canales. Trataré de considerarlos brevemente. Si ya está familiarizado con ellos, vaya directamente a la sección Contexto.


Gorutin


La documentación oficial dice que "Gorutin es un flujo ligero de ejecución". Las goroutinas son más livianas que las hebras, por lo que administrarlas requiere relativamente menos recursos.


Sandbox


package main import "fmt" // ,   Hello func printHello() { fmt.Println("Hello from printHello") } func main() { //   //       go func(){fmt.Println("Hello inline")}() //     go printHello() fmt.Println("Hello from main") } 

Si ejecuta este programa, verá que solo Hello from main imprime Hello from main . De hecho, ambas gorutinas comienzan, pero la main termina antes. Entonces, los Goroutines necesitan una forma de informar a main sobre el final de su ejecución, y para que ella espere esto. Aquí los canales vienen en nuestra ayuda.


Canales


Los canales son una forma de comunicación entre goroutines. Se utilizan cuando desea transferir resultados, errores u otra información de una rutina a otra. Los canales son de diferentes tipos, por ejemplo, un canal de tipo int recibe enteros y un canal de error de tipo recibe errores, etc.


Digamos que tenemos un canal ch de tipo int . Si desea enviar algo al canal, la sintaxis será ch <- 1 . Puede obtener algo del canal como este: var := <- ch , es decir tome el valor del canal y guárdelo en la variable var .


El siguiente código ilustra cómo usar los canales para confirmar que las gorutinas han completado su trabajo y han devuelto sus valores a main .


Nota: Los grupos de espera también se pueden usar para la sincronización, pero en este artículo seleccioné canales para ejemplos de código, ya que los usaremos más adelante en la sección de contexto.


Sandbox


 package main import "fmt" //       int   func printHello(ch chan int) { fmt.Println("Hello from printHello") //     ch <- 2 } func main() { //  .       make //       : // ch := make(chan int, 2),       . ch := make(chan int) //  .  ,    . //       go func(){ fmt.Println("Hello inline") //     ch <- 1 }() //     go printHello(ch) fmt.Println("Hello from main") //      //     ,    i := <- ch fmt.Println("Received ",i) //      //    ,      <- ch } 

Contexto


El paquete de contexto in go le permite pasar datos a su programa en algún tipo de "contexto". El contexto, como un tiempo de espera, una fecha límite o un canal, señala un cierre y el retorno de las llamadas.


Por ejemplo, si realiza una solicitud web o ejecuta un comando del sistema, sería una buena idea utilizar un tiempo de espera para los sistemas de grado de producción. Porque si la API a la que accede es lenta, es poco probable que desee acumular solicitudes en su sistema, ya que esto puede aumentar la carga y disminuir el rendimiento al procesar sus propias solicitudes. El resultado es un efecto en cascada.


Y aquí el contexto de tiempo de espera o fecha límite puede ser el correcto.


Creación de contexto


El paquete de contexto le permite crear y heredar contexto de las siguientes maneras:


context.Background () contexto de ctx


Esta función devuelve un contexto vacío. Debe usarse solo a un nivel alto (en el manejador de solicitudes de nivel principal o superior). Se puede usar para obtener otros contextos, que discutiremos más adelante.


 ctx, cancel := context.Background() 

Nota trans .: Hay una imprecisión en el artículo original, el ejemplo correcto de usar el context.Background será el siguiente:


 ctx := context.Background() 

context.TODO () Contexto ctx


Esta función también crea un contexto vacío. Y también debe usarse solo a un nivel alto, ya sea cuando no esté seguro de qué contexto usar, o si la función aún no recibe el contexto deseado. Esto significa que usted (o alguien que admite el código) planea agregar contexto a la función más adelante.


 ctx, cancel := context.TODO() 

Nota trans .: Hay una imprecisión en el artículo original, el ejemplo correcto de usar el context.TODO . context.TODO será el siguiente:


 ctx := context.TODO() 

Curiosamente, eche un vistazo al código , es absolutamente igual que el fondo. La única diferencia es que, en este caso, puede usar las herramientas de análisis estático para verificar la validez de la transferencia de contexto, que es un detalle importante, ya que estas herramientas ayudan a identificar posibles errores en una etapa temprana y pueden incluirse en la canalización de CI / CD.


Desde aquí :


 var ( background = new(emptyCtx) todo = new(emptyCtx) ) 

context.WithValue (contexto principal, clave, interfaz val {}) (contexto ctx, cancelar CancelFunc)


Nota Carril: hay una imprecisión en el artículo original, la firma correcta para el context.WithValue será el siguiente:


 context.WithValue(parent Context, key, val interface{}) Context 

Esta función toma un contexto y devuelve un contexto derivado de él en el que el valor val está asociado con la key y pasa por todo el árbol de contexto. Es decir, tan pronto como cree un contexto WithValue , cualquier contexto derivado recibirá este valor.


No se recomienda pasar parámetros críticos utilizando valores de contexto; en cambio, las funciones deben tomarlos explícitamente en la firma.


 ctx := context.WithValue(context.Background(), key, "test") 

context.WithCancel (contexto principal) (contexto ctx, cancelar CancelFunc)


Se pone un poco más interesante aquí. Esta función crea un nuevo contexto a partir del padre que se le pasó. El padre puede ser el contexto de fondo o el contexto pasado como argumento a la función.


Se devuelven el contexto derivado y la función deshacer. Solo la función que lo crea debe llamar a la función para cancelar el contexto. Puede pasar la función de deshacer a otras funciones si lo desea, pero esto no se recomienda. Por lo general, esta decisión se toma de un malentendido de la cancelación del contexto. Debido a esto, los contextos generados por este padre pueden afectar el programa, lo que conducirá a un resultado inesperado. En resumen, es mejor NUNCA pasar una función de cancelación.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

Nota Carril: En el artículo original, el autor, aparentemente, erróneamente para el context.WithCancel dio un ejemplo con el context.WithDeadline . Con el context.WithDeadline . El ejemplo correcto para el context.WithCancel sería:


 ctx, cancel := context.WithCancel(context.Background()) 

context.WithDeadline (contexto principal, d time.Time) (contexto ctx, cancelar CancelFunc)


Esta función devuelve un contexto derivado de su padre, que se cancela después de una fecha límite o llamada a la función cancelar. Por ejemplo, puede crear un contexto que se cancela automáticamente en un momento específico y pasa esto a las funciones secundarias. Cuando este contexto se cancela después de la fecha límite, todas las funciones que tienen este contexto deben ser notificadas por notificación.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

context.WithTimeout (Contexto principal, tiempo de espera. Duración) (Contexto ctx, cancelar CancelFunc)


Esta función es similar a context.WithDeadline. La diferencia es que el período de tiempo se usa como entrada. Esta función devuelve un contexto derivado que se cancela cuando se llama a la función cancelar o después de un tiempo.


 ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second)) 

Nota Carril: En el artículo original, el autor, aparentemente, erróneamente para el context.WithTimeout dio un ejemplo con el context.WithDeadline . Con la línea de tiempo. El ejemplo correcto para el context.WithTimeout sería este:


 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) 

Recepción y uso de contextos en sus funciones.


Ahora que sabemos cómo crear contextos (Background y TODO) y cómo generar contextos (WithValue, WithCancel, Deadline y Timeout), analicemos cómo usarlos.


En el siguiente ejemplo, puede ver que la función que toma el contexto inicia la rutina y espera que regrese o cancele el contexto. La instrucción select nos ayuda a determinar qué sucede primero y terminar la función.


Después de cerrar el canal Listo <-ctx.Done() , se selecciona el caso de case <-ctx.Done(): Tan pronto como esto suceda, la función debería interrumpir el trabajo y prepararse para un retorno. Esto significa que debe cerrar cualquier conexión abierta, liberar recursos y volver de la función. Hay momentos en que la liberación de recursos puede retrasar el retorno, por ejemplo, la limpieza se bloquea. Debes tener esto en cuenta.


El ejemplo que sigue a esta sección es un programa go completamente terminado que ilustra los tiempos de espera y las funciones de deshacer.


 // ,  -      // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } 

Ejemplo


Como vimos, al usar contextos, puede trabajar con plazos, tiempos de espera y también llamar a la función de cancelación, lo que deja en claro a todas las funciones que usan un contexto derivado que necesita completar su trabajo y ejecutar la devolución. Considere un ejemplo:


función main :


  • Crea un contexto de función de cancelación
  • Llama a la función cancelar después de un tiempo de espera arbitrario

Función doWorkContext :


  • Crea un contexto derivado con un tiempo de espera
  • Este contexto se cancela cuando la función principal llama a cancelFunction, el tiempo de espera caduca o doWorkContext llama a cancelFunction.
  • Ejecuta goroutine para realizar una tarea lenta, pasando el contexto resultante
  • Espera a que se completen las goroutinas o se cancele el contexto de main, lo que ocurra primero

Función sleepRandomContext :


  • Lanza goroutine para realizar una tarea lenta
  • Espera a que termine la gorutina, o
  • Espera a que el contexto sea cancelado por la función principal, el tiempo de espera o llame a su propia función cancelar

función sleepRandom :


  • Se duerme al azar

Este ejemplo usa el modo de suspensión para simular un tiempo de procesamiento aleatorio, pero en realidad, puede usar canales para señalar esta función sobre el inicio de la limpieza y esperar la confirmación del canal de que la limpieza se ha completado.


Sandbox (Parece que el tiempo aleatorio que uso en el sandbox prácticamente no ha cambiado. Intente esto en su computadora local para ver la aleatoriedad)


Github


 package main import ( "context" "fmt" "math/rand" "Time" ) //   func sleepRandom(fromFunction string, ch chan int) { //    defer func() { fmt.Println(fromFunction, "sleepRandom complete") }() //    //   , // «»      seed := time.Now().UnixNano() r := rand.New(rand.NewSource(seed)) randomNumber := r.Intn(100) sleeptime := randomNumber + 100 fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms") time.Sleep(time.Duration(sleeptime) * time.Millisecond) fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms") //   ,     if ch != nil { ch <- sleeptime } } // ,       // ,   -    func sleepRandomContext(ctx context.Context, ch chan bool) { //  (. .:  )    //     // ,    defer func() { fmt.Println("sleepRandomContext complete") ch <- true }() //   sleeptimeChan := make(chan int) //       //     go sleepRandom("sleepRandomContext", sleeptimeChan) //  select        select { case <-ctx.Done(): //   ,    //  ,    doWorkContext  // doWorkContext  main  cancelFunction //  ,     -   //    ,    ( ) //    -  , //    ,   //         fmt.Println("sleepRandomContext: Time to return") case sleeptime := <-sleeptimeChan: //   ,       fmt.Println("Slept for ", sleeptime, "ms") } } //  ,         //       //   ,      main func doWorkContext(ctx context.Context) { //          - //  150  //  ,   ,   150  ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond) //         defer func() { fmt.Println("doWorkContext complete") cancelFunction() }() //       //         , //      ,    ch := make(chan bool) go sleepRandomContext(ctxWithTimeout, ch) //  select      select { case <-ctx.Done(): //   ,           //     ,   main   cancelFunction fmt.Println("doWorkContext: Time to return") case <-ch: //   ,       fmt.Println("sleepRandomContext returned") } } func main() { //   background ctx := context.Background() //     ctxWithCancel, cancelFunction := context.WithCancel(ctx) //      //        defer func() { fmt.Println("Main Defer: canceling context") cancelFunction() }() //     - //   ,        go func() { sleepRandom("Main", nil) cancelFunction() fmt.Println("Main Sleep complete. canceling context") }() //   doWorkContext(ctxWithCancel) } 

Trampas


Si la función usa contexto, asegúrese de que las notificaciones de cancelación se manejen correctamente. Por ejemplo, ese exec.CommandContext no cierra el canal de lectura hasta que el comando completa todos los tenedores creados por el proceso ( Github ), es decir, que cancelar el contexto no regresa inmediatamente de la función si espera con cmd.Wait (), hasta que todos los tenedores del comando externo completen el procesamiento.


Si usa un tiempo de espera o una fecha límite con un tiempo de ejecución máximo, es posible que no funcione como se esperaba. En tales casos, es mejor implementar tiempos de espera usando el time.After .


Mejores prácticas


  1. contexto. El fondo solo debe usarse en el nivel más alto, como la raíz de todos los contextos derivados.
  2. context.TODO debe usarse cuando no está seguro de qué usar, o si la función actual usará contexto en el futuro.
  3. Se recomiendan cancelaciones de contexto, pero estas funciones pueden tardar un tiempo en borrarse y salir.
  4. context.Value debe usarse con la mayor moderación posible y no debe usarse para pasar parámetros opcionales. Esto hace que la API sea incomprensible y puede provocar errores. Dichos valores deben pasarse como argumentos.
  5. No almacene contextos en una estructura; páselos explícitamente en funciones, preferiblemente como primer argumento.
  6. Nunca pase un contexto nulo como argumento. En caso de duda, use TODO.
  7. La estructura de Context no tiene un método de cancelación, porque solo la función que genera el contexto debería cancelarlo.

Del traductor


En nuestra empresa, utilizamos activamente el paquete Context cuando desarrollamos aplicaciones de servidor para uso interno. Pero tales aplicaciones para el funcionamiento normal, además de Context, requieren elementos adicionales, como:


  • Registro
  • Procesamiento de señal para la finalización de la aplicación, recarga y logrotate
  • Trabaja con archivos pid
  • Trabaja con archivos de configuración
  • Y otros

Por lo tanto, en algún momento, decidimos resumir toda la experiencia que habíamos acumulado y creamos paquetes auxiliares que simplificaron enormemente la escritura de aplicaciones (especialmente las aplicaciones que tienen API). Hemos publicado nuestros desarrollos en el dominio público y cualquiera puede usarlos. Los siguientes son algunos enlaces a paquetes útiles para resolver tales problemas:



Lea también otros artículos en nuestro blog:


Source: https://habr.com/ru/post/461723/


All Articles