1. Code
  2. Coding Fundamentals

Programación basada en context en Go

Scroll to top

Spanish (Español) translation by Carlos (you can also view the original English article)

Los programas de Go que ejecutan múltiples cálculos simultáneos en goroutines necesitan administrar su durabilidad. Las goroutines fuera de control pueden entrar en bucles infinitos, bloquear a otros goroutines en espera o simplemente tardar demasiado tiempo. Lo ideal sería que pudieras cancelar goroutines o hacer que se pausen de algún modo.

Ingresa a la programación basada en contenido. Go 1.7 introdujo el paquete context, que ofrece precisamente esas capacidades, así como la posibilidad de asociar valores arbitrarios con un contexto que viaja con la ejecución de las solicitudes y permite la comunicación y el paso de información fuera de banda.

En este tutorial, aprenderás los pormenores de los contextos en Go, cuándo y cómo usarlos, y cómo evitar abusar de ellos.

¿Quién necesita un contexto?

El contexto es una abstracción muy útil. Te permite encapsular información que no es relevante para el cálculo principal, como la identificación de la solicitud, el token de autorización y el tiempo de espera. Existen varios beneficios de esta encapsulación:

  • Separa los parámetros de cálculo centrales de los parámetros operativos.
  • Codifica aspectos operativos comunes y cómo comunicarlos a través de los límites.
  • Ofrece un mecanismo estándar para añadir información fuera de banda sin cambiar las firmas.

La interfaz de context

Aquí está toda la interfaz de context:

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

Las siguientes secciones explican el propósito de cada método.

El método Deadline()

Deadline devuelve el momento en que el trabajo hecho en nombre de este contexto debe ser cancelado. Deadline devuelve ok==false cuando no se establece un plazo. Las sucesivas llamadas a Deadline devuelven los mismos resultados.

El método Done()

Done() devuelve un canal que está cerrado cuando el trabajo efectuado en nombre de este contexto se debe cancelar. Done puede devolver nil si este contexto nunca puede ser cancelado. Llamadas sucesivas a Done() devuelven el mismo valor.

  • La función context.WithCancel() dispone para que el canal Done se cierre cuando se llame a cancel (cancelar).
  • La función context.WithDeadline() dispone que el canal Done se cierre cuando expire el plazo.
  • La función context.WithTimeout() hace que el canal Done se cierre cuando pase el tiempo de espera.

Done se puede utilizar en declaraciones seleccionadas:

1
 // Stream generates values with DoSomething and sends them 
2
 // to out until DoSomething returns an error or ctx.Done is
3
 // closed.
4
 func Stream(ctx context.Context, out chan<- Value) error {
5
     for {
6
 		v, err := DoSomething(ctx)
7
 		if err != nil {
8
 			return err
9
 		}
10
 		select {
11
 		case <-ctx.Done():
12
 			return ctx.Err()
13
 		case out <- v:
14
 		}
15
 	}
16
 }

Revisa este artículo del blog de Go para más ejemplos sobre cómo utilizar un canal Done para la cancelación.

El método Err()

Err() devuelve nil mientras el canal Done esté abierto. Devuelve Canceled si el contexto fue cancelado o DeadlineExceeded si la fecha límite del contexto pasó o el tiempo límite expiró. Después de que Done se cierra, las llamadas sucesivas a Err() devuelven el mismo valor. Aquí están las definiciones:

1
// Canceled is the error returned by Context.Err when the 
2
// context is canceled.
3
var Canceled = errors.New("context canceled")
4
5
// DeadlineExceeded is the error returned by Context.Err 
6
// when the context's deadline passes.
7
var DeadlineExceeded error = deadlineExceededError{}

El método Value()

Value devuelve el valor asociado con este contexto para una clave, o nil si no hay ningún valor asociado a la clave. Las llamadas sucesivas a Value con la misma clave devuelven el mismo resultado.

Utiliza los valores de contexto únicamente para los datos de solicitud que realizan la transición de los procesos y los límites de la API, no para pasar parámetros opcionales a las funciones.

Una clave identifica un valor específico en un contexto. Las funciones que desean almacenar valores en Context usualmente asignan una clave en una variable global y utilizan esa clave como argumento para context.WithValue() y Context.Value(). Una clave puede ser de cualquier tipo que admita igualdad.

Ámbito (scope) de context

Los contextos tienen ámbitos (scopes). Puedes derivar ámbitos de otros ámbitos, y el ámbito principal no tiene acceso a los valores de los ámbitos derivados, pero los ámbitos derivados tienen acceso a los valores de los ámbitos principales.

Los contextos forman una jerarquía. Comienzas con context.Background() o context.TODO(). Cada vez que llamas a WithCancel(), WithDeadline() o WithTimeout(), creas un contexto derivado y recibes una función de cancelación. Lo importante es que cuando un contexto principal se cancela o caduca, todos sus contextos derivados.

Debes utilizar context.Background() en la función main(), en las funciones init() y en las pruebas. Debes usar context.TODO() si no está seguro de qué contexto utilizar.

Ten en cuenta que Background y TODO no se pueden cancelar.

Deadlines, timeouts y cancelaciones

Como recordarás, WithDeadline() y WithTimeout() devuelven contextos que se cancelan automáticamente, mientras que WithCancel() devuelve un contexto y debe ser cancelado explícitamente. Todos ellos devuelven una función de cancelación, así que aunque timeout/deadline no haya expirado todavía, aún puedes cancelar cualquier contexto derivado.

Examinemos un ejemplo. Primero, aquí está la función contextDemo() con un nombre y un contexto. Se ejecuta en un bucle infinito, imprimiendo a la consola su nombre y el plazo de su contexto si lo hay. Luego, solo se suspende por un segundo.

1
package main 
2
3
import (
4
    "fmt"
5
    "context"
6
    "time"
7
)
8
9
func contextDemo(name string, ctx context.Context) {    
10
    for {
11
        if ok {
12
            fmt.Println(name, "will expire at:", deadline)
13
        } else {
14
            fmt.Println(name, "has no deadline")
15
        }
16
        time.Sleep(time.Second)
17
    }
18
}

La función principal crea tres contextos:

  • timeoutContext con un tiempo de espera de tres segundos
  • un cancelContext que no caduca
  • deadlineContext, que se deriva de cancelContext, con un plazo límite de cuatro horas a partir de este momento

Luego, inicia la función contextDemo como tres goroutines. Todos se ejecutan simultáneamente e imprimen su mensaje cada segundo.

Entonces la función principal espera a que el goroutine con timeoutCancel se cancele leyendo de su canal Done() (se bloqueará hasta que se cierre). Una vez que el tiempo de espera expira después de tres segundos, main() llama a cancelFunc() que cancela el gorutine con cancelContext, así como el último gorutine con el contexto derivado de las cuatro horas del plazo.

1
func main() {
2
    timeout := 3 * time.Second
3
    deadline := time.Now().Add(4 * time.Hour)
4
    timeOutContext, _ := context.WithTimeout(
5
        context.Background(), timeout)
6
    cancelContext, cancelFunc := context.WithCancel(
7
        context.Background())
8
    deadlineContext, _    := context.WithDeadline(
9
        cancelContext, deadline)
10
11
12
    go contextDemo("[timeoutContext]", timeOutContext)
13
    go contextDemo("[cancelContext]", cancelContext)
14
    go contextDemo("[deadlineContext]", deadlineContext)
15
16
    // Wait for the timeout to expire
17
    <- timeOutContext.Done()
18
19
    // This will cancel the deadline context as well as its
20
    // child - the cancelContext
21
    fmt.Println("Cancelling the cancel context...")
22
    cancelFunc()
23
24
    <- cancelContext.Done()
25
    fmt.Println("The cancel context has been cancelled...")
26
27
    // Wait for both contexts to be cancelled
28
    <- deadlineContext.Done()
29
    fmt.Println("The deadline context has been cancelled...")
30
}
31

Aquí está el resultado:

1
[cancelContext] has no deadline
2
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
3
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
4
[cancelContext] has no deadline
5
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
6
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
7
[cancelContext] has no deadline
8
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
9
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
10
Cancelling the cancel context...
11
The cancel context has been cancelled...
12
The deadline context has been cancelled...

Pasando valores en el contexto

Puedes adjuntar valores a un contexto utilizando la función WithValue(). Observa que se devuelve el contexto original, no un contexto derivado. Puedes leer los valores del contexto utilizando el método Value(). Vamos a modificar nuestra función de demostración para obtener su nombre del contexto en lugar de pasarlo como un parámetro:

1
func contextDemo(ctx context.Context) {
2
    deadline, ok := ctx.Deadline()
3
    name := ctx.Value("name")
4
    for {
5
        if ok {
6
            fmt.Println(name, "will expire at:", deadline)
7
        } else {
8
            fmt.Println(name, "has no deadline")
9
        }
10
        time.Sleep(time.Second)
11
    }
12
}

Y modifiquemos la función principal para adjuntar el nombre mediante WithValue():

1
go contextDemo(context.WithValue(
2
    timeOutContext, "name", "[timeoutContext]"))
3
go contextDemo(context.WithValue(
4
    cancelContext, "name", "[cancelContext]"))
5
go contextDemo(context.WithValue(
6
    deadlineContext, "name", "[deadlineContext]"))

El resultado sigue siendo el mismo. Revisa la sección de las mejores prácticas para obtener algunas pautas sobre el uso adecuado de los valores de contexto.

Las mejores prácticas

Han surgido varias prácticas deseables en torno a los valores de contexto:

  • Evitar pasar argumentos de función en valores de contexto.
  • Las funciones que desean almacenar valores en Context normalmente asignan una clave en una variable global.
  • Los paquetes deben definir las claves como un tipo no exportado para evitar colisiones.
  • Los paquetes que definen una clave de Context deben proveer accessors seguros para los valores almacenados con esa clave.

El contexto de la solicitud HTTP

Uno de los casos de uso más útiles para los contextos es pasar información junto con una solicitud HTTP. Esa información puede incluir un identificador de solicitud, credenciales de autenticación y más. En Go 1.7, el paquete net/http estándar aprovechó que el paquete context se «estandarizó» y añadió soporte de contexto directamente al objeto de la solicitud:

1
func (r *Request) Context() context.Context
2
func (r *Request) WithContext(ctx context.Context) *Request

Ahora es posible adjuntar una id de solicitud desde los encabezados hasta el handler final de una manera estándar. La función del controlador WithRequestID() extrae una ID de solicitud del encabezado «X-Request-ID» y genera un nuevo contexto con la ID de solicitud a partir de un contexto existente que utiliza. Luego lo pasa al siguiente controlador de la cadena. La función pública GetRequestID() proporciona acceso a los handlers que pueden ser definidos en otros paquetes.

1
const requestIDKey int = 0
2
3
4
func WithRequestID(next http.Handler) http.Handler {
5
    return http.HandlerFunc(
6
        func(rw http.ResponseWriter, req *http.Request) {
7
            // Extract request ID from request header
8
            reqID := req.Header.Get("X-Request-ID")
9
            // Create new context from request context with 
10
            // the request ID
11
            ctx := context.WithValue(
12
                req.Context(), requestIDKey, reqID)
13
            // Create new request with the new context
14
            req = req.WithContext(ctx)
15
            // Let the next handler in the chain take over.
16
            next.ServeHTTP(rw, req)
17
        }
18
    )
19
}
20
21
func GetRequestID(ctx context.Context) string {
22
    ctx.Value(requestIDKey).(string)
23
}
24
25
26
func Handle(rw http.ResponseWriter, req *http.Request) {
27
    reqID := GetRequestID(req.Context())
28
    ...
29
}
30
31
32
func main() {
33
    handler := WithRequestID(http.HandlerFunc(Handle))
34
    http.ListenAndServe("/", handler)
35
}

Conclusión

La programación basada en contexto ofrece una manera estándar y bien fundamentada de abordar dos problemas comunes: gestionar la durabilidad de los goroutines y transmitir información fuera de banda a través de una cadena de funciones.

Sigue las mejores prácticas y utiliza los contextos en el contexto adecuado (¿viste lo que acabo de hacer?) y tu código mejorará considerablemente.