mauro.ec

Ideas, thoughts, and proofs of concept

Paquete Context en Golang

Índice

Introducción

El proceso de una aplicación requiere generalmente el envío de datos de request entre rutinas, por ejemplo: un token de autenticación o un parámetro recibido desde un formulario. También es muy común que al cancelar o producirse un timeout en una petición se requiera que todas las subrutinas relacionadas liberen los recursos usados, para esto el paquete Context provee métodos y estructuras que permiten definir tipos de contexto así como el envío de señales para cancelar, definir tiempos límite y enviar valores entre rutinas asociadas.

La interfaz Context se encuentra definida de la siguiente forma:

type Context interface {
   Done() <-chan struct{}     
   Err() error
   Deadline() (deadline time.Time, ok bool)
   Value(key interface{}) interface{} 
}

Donde:

  • Done().- retorna un canal que permite conocer si el Contexto ha sido cancelado o se ha producido un time out.
  • Err().- indica la razón por la que se canceló el Contexto.
  • Deadline().- puede ser usado para verificar si el Contexto será cancelado luego de cierto tiempo.
  • Value().- retorna el valor asociado con el key enviado.

Es importante notar que:

  • Se puede enviar un contexto simple a cualquier número de rutinas.
  • La invocación de sus métodos son seguros.
  • Al cancelar el contexto todas las rutinas que lo usan deben ser canceladas y finalizar su ejecución.
  • El Contexto es enviado como valor de esta forma cada subrutina puede modificar las propiedades requeridas sin afectar las de su padre.
  • El contexto es inmutable.

Las funciones provistas por el paquete Context son:

  • context.Background()

    • Retorna un contexto vacío siendo la raíz del aŕbol de contextos
    • No puede ser cancelado
    • No tiene valor ni fecha de caducidad (deadline)
    • Usado típicamente por la función main
    • Actúa como un contexto de nivel superior para request entrantes
  • context.TODO()

    • Retorna un contexto vacío
    • Es usado posteriormente para validar el contexto usado o como un espacio para colocar valores a usarse

Es posible especificar comportamientos relacionados con: cancelar, establecer un tiempo de caducidad (timeout), fechas de expiración (deadline) y envío de valores para esto disponemos de las siguientes funciones:

  • context.WithCancel
  • context.WithTimeout
  • context.WithDeadline
  • context.WithValue

context.WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

Esta función context.WithCancel retorna una copia del contexto padre con un nuevo canal Done y una referencia a la función cancel() la cuál puede ser usada para cerrar el canal Done del contexto generado. El cerrar el canal implica que las operaciones relacionadas finalizarán su operación liberarando los recursos asociados al contexto, se debe notar que cancel() no espera por la finalización de una operación y aún cuando esta función puede ser llamada múltiples veces por varias rutinas solo tendrá efecto la primera invocación.

// Crear un contexto cancelable
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Ejemplo:

En este caso se crea un contexto cancelable, el cuál es enviado a dos rutinas que detendrán su ejecución luego de 5 segundos:

func main() {
    // Creando un contexto cancelable
    ctx, cancel := context.WithCancel(context.Background())

    go function1(ctx)
    go function2(ctx)

    time.Sleep(time.Second * 5)

    // Cancela el contexto
    cancel()

    time.Sleep(time.Second * 3)
    fmt.Println("bye")
}

Cada rutina realiza un proceso definido cancelando su operación una vez que se ha cerrado el canal Done:

func function1(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("function1 cancelada")
            return
        default:
            fmt.Println("function1 operando")
            time.Sleep(time.Second * 1)
        }
    }
}
func function2(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("function2 cancelada")
            return
        default:
            fmt.Println("function2 operando")
            time.Sleep(time.Second * 3)
        }
    }
}

En el resultado obtenido se puede apreciar la ejecución de las rutinas así como su cancelación.

function1 operando
function2 operando
function1 operando
function1 operando
function2 operando
function1 operando
function1 operando
function1 cancelada
function2 cancelada
bye

context.WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

Mediante context.WithDeadline es posible determinar la fecha y hora de caducidad de un proceso una vez alcanzado este límite automáticamente el canal Done es cerrado.

Ejemplo:

func main() {
    // Creando un contexto cancelable con fecha límite
    deadlineTime := time.Date(2021, time.February, 14, 0, 0, 0, 0, time.Now().UTC().Location())
    fmt.Println("Deadline: ", deadlineTime)
    ctx, _ := context.WithDeadline(context.Background(), deadlineTime)

    go function1(ctx)
    go function2(ctx)

    time.Sleep(time.Hour * 24)
}

Las funciones usadas por su parte no tienen mayor variación y se comportan al igual que en el caso anterior cancelando su operación una vez recibido el canal Done.

context.WithTimeout

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

context.WithTimeout es una forma adicional de invocar context.WithDeadline siendo un wrapper para esta función definida de la siguiente forma:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
     return WithDeadline(parent, time.Now().Add(timeout))
}

La diferencia entre WithTimeout y WithDeadline radica en el tiempo en el que finalizará la función, en el caso de WithTimeout el contador iniciará desde el momento en el que el contexto es creado mientras que WithDeadline se establece explícitamente el tiempo de expiración.

duration := 5 * time.Millisecond
ctx, cancel := context.WithTimeout(context.Background(), duration)
defer cancel()

En relación al ejemplo anterior se ha definido un tiempo de expiración de 3 segundos luego de los cuales se cerrará el canal Done.

func main() {
    fmt.Println("Tiempo de inicio: ", time.Now())
    // Creando un contexto cancelable con timeout
    timeout := time.Second * 3
    fmt.Println("timeout: ", timeout)
    ctx, _ := context.WithTimeout(context.Background(), timeout)

    go function1(ctx)
    go function2(ctx)

    time.Sleep(time.Minute * 1)
}

Las funciones definidas (function1 y function2) continuan su ejecución cada segundo y tres segundos respectivamente.

func function1(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("function1 cancelada: ", time.Now())
            return
        default:
            fmt.Println("function1 operando")
            time.Sleep(time.Second * 1)
        }
    }
}
func function2(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("function2 cancelada: ", time.Now())
            return
        default:
            fmt.Println("function2 operando")
            time.Sleep(time.Second * 3)
        }
    }
}

El resultado de este proceso se puede visualizar de la siguiente forma:

Tiempo de inicio:  2021-02-15 09:55:11.604323841 -0500 -05 m=+0.000118346
timeout:  3s
function2 operando
function1 operando
function1 operando
function1 operando
function2 operando
function1 cancelada:  2021-02-15 09:55:14.605914835 -0500 -05 m=+3.001709546
function2 cancelada:  2021-02-15 09:55:17.605139159 -0500 -05 m=+6.000933774

context.WithValue

func WithValue(parent Context, key, val interface{}) Context

Es posible enviar datos de request a través de las distintas rutinas asociadas a un contexto mediante context.WithValue.

type tokenKey string
ctx := context.WithValue(context.Background(), tokenKey("userTokenID"), "xyzABC123")

Para esto se debe notar que se ha definido un tipo de dato (tokenKey en este ejemplo). Para obtener el valor enviado se debe usar:

tokenID := ctx.Value(tokenKey("userTokenID")).(tokenKey)

Es posible agregar más de un key en un contexto, por ejemplo:

type tokenKey string
type name string

func main() {
    // Creando un contexto con un valor
    ctx := context.WithValue(context.Background(), tokenKey("userTokenID"), "xyzABC123")
    ctx = context.WithValue(ctx, name("userName"), "Mauro")

    go function1(ctx)

    time.Sleep(time.Second * 1)
}

La rutina por su parte puede tomar los valores enviados de la siguiente manera:

func function1(ctx context.Context) {
    tokenID := ctx.Value(tokenKey("userTokenID"))
    name := ctx.Value(name("userName"))

    fmt.Printf("Token: %s \n", tokenID)
    fmt.Printf("UserName: %s \n", name)
}

Mostrándo los los valores asociados a los key:

Token: xyzABC123 
UserName: Mauro 

Conclusiones

Las funciones y estructuras del paquete Context permiten enviar datos y señales entre rutinas de una forma sencilla, especialmente el uso de context.WithCancel y context.WithValue son de gran utilidad al permitir cancelar las rutinas asociadas a un contexto así como enviar valores entre rutinas.

Referencias

Context

Concurrency in Golang