Qual problema o Contexto resolve?

Context é um pacote em Go desenvolvido para:

1 - Permitir informação com escopo da requisição ser passada para frente durante o ciclo de vida da requisição. Isso permite o acesso a essas informações em várias partes do código, inclusive em funções executadas de maneira concorrente em diferentes goroutines.

2 - Permitir o cancelamento prematuro de código concorrente quando ocorrer algum erro ou o prazo para o código ser executado expirar.

Introdução

Um código concorrente possui diversas goroutines sendo executadas ao mesmo tempo e frequentemente é necessário abortar a execução desse código quando algo dá errado em uma dessas funções, caso contrário recursos serão gastos desnecessariamente.

Uma forma de manter a comunicação entre todas as goroutines sendo executadas é utilizando contexto. Quando uma requisição inicia, um novo contexto é criado. A partir desse contexto, outros podem ser criados, formando uma árvore de contextos. Nessa árvore, quando um contexto finaliza a execução por algum motivo, todos os contextos filhos também são finalizados.

Outra possibilidade de uso do contexto é armazenar informações com escopo da requisição como o requestID. Desse modo, caso ocorrer erro em algum momento do código, esse ID pode ser utilizado na resposta e/ou no log de erro.

O pacote contexto define um tipo com os seguintes métodos:

type Context interface {
	// Deadline returns the time when work done on behalf of this context
	// should be canceled. Deadline returns ok==false when no deadline is
	// set. Successive calls to Deadline return the same results.
	Deadline() (deadline time.Time, ok bool)

	// Done returns a channel that's closed when work done on behalf of this
	// See https://blog.golang.org/pipelines for more examples of how to use
	// a Done channel for cancellation.
	Done() <-chan struct{}

	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// Canceled if the context was canceled
	// or DeadlineExceeded if the context's deadline passed.
	// After Err returns a non-nil error, successive calls to Err return the same error.
	Err() error

	// Value returns the value associated with this context for key, or nil
	// if no value is associated with key. Successive calls to Value with
	// the same key returns the same result.
	//
	Value(key interface{}) interface{}
}

Os métodos responsáveis pela comunicação de sinais de cancelamento são:

1 - Deadline(): Vai retornar a data após a qual essa goroutine deverá parar de executar

2 - Done(): Retorna um channel que será fechado caso a goroutine seja cancelada (Para testar se o channel fechou use: <-ctx.Done())

3 - Err(): Retorna um erro se a goroutine já foi cancelada, não existe outro tipo de erro retornado pelo context.

O método responsável por retornar valores armazenados no contexto é o Value().

Usando contextos para trafegar informações

Não é possível adicionar novos valores a um contexto que já existe e nem alterar um valor já existente. Caso alguma dessas operações sejam necessárias é preciso criar um novo contexto. No exemplo abaixo, newCtx é o novo contexto criado com a chave c e o valor 20. Se for necessário adicionar um outro valor no contexto, basta fazer a mesma coisa utilizando o newCtx como parâmetro.

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
		newCtx := context.WithValue(ctx, "c", 20)

		fmt.Printf("%v\n", newCtx.Value("c"))

		w.Write([]byte(`{}`))
	})

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Apesar de ser possível utilizar contexto para trafegar qualquer tipo de informação, essa prática não é considerada boa e é preciso avaliar com cuidado se há realmente necessidade do tráfego da informação ser através de contexto.

Cenário adequado para tráfegar informações com contexto: Se temos um dado na raíz e precisamos dele em várias partes do código para um caso de uso que pode ocorrer ou não como logs, audit, gerenciamento de goroutines, timeouts e etc.

Cenário não adequado para tráfegar informações com contexto: Se temos a necessidade de passar alguma informação no código e não esse tipo de informação não se encaixa no cenário acima é inapropriado utilizar o contexto porque o código deixa de receber argumentos explícitos que são usados na lógica da aplicação e passa a receber informações implícitas dentro do contexto. Essa característica deixa o código mais difícil de entender e mais difícil de escrever.

Usando contextos para abortar execução de código que esteja demorando muito

Vamos ver como ficaria um exemplo de uso de contexto para abortar a execução de código concorrente. Nosso código será uma API com uma rota de /calcularFrete. O trabalho dessa rota será verificar de forma concorrente o preço do frete em duas APIs diferentes, a API dos correios e a API da sequoia. Nossa API retornará assim que receber a primeira resposta, independente do valor. Outra característica da nossa API é que essa rota terá um prazo máximo de 2 segundos para resposta, ou seja, caso as APIs dos serviços externos demorarem mais que isso nós abortaremos a execução usando contexto.

O primeiro passo a ser feito é criar um contexto com um prazo de 2 segundos. Ao expirar esse prazo, é enviado um sinal para um channel que pode ser escutado com o métodp Done(). Ou seja, utilizamos esse channel para verificar se devemos ou não abortar a execução.

No código foi utilizado sleep para simular o tempo de resposta de cada uma das APIs externas. Para testar os diferentes cenários do código abaixo, basta alterar o tempo de duração dos sleeps. Colocando valores maior que 2 para ambas as APIs de calcular frete conseguimos ver que o timeout ocorre. Já quando uma delas possui um valor menor que 2 o código consegue executar normalmente.

package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func main() {
	http.HandleFunc("/", calculaFrete)
	http.ListenAndServe(":8080", nil)
}

func getFreteCorreios(ctx context.Context) (float32, error) {
	r := make(chan float32)

	// calcula valor do frete usando os Correios como transportadora
	go requestFreteFor("correios", r)

	select {
	case <-ctx.Done():
		// tempo expirou
		return 0, fmt.Errorf("Timeout")
	case c := <-r:
		// deu certo
		return c, nil
	}
}

func getFreteSequoia(ctx context.Context) (float32, error) {
	r := make(chan float32)

	// calcula valor do frete usando a Sequoia como transportadora
	go requestFreteFor("sequoia", r)

	select {
	case <-ctx.Done():
		// tempo expirou
		return 0, fmt.Errorf("Timeout")
	case c := <-r:
		// deu certo
		return c, nil
	}
}

func requestFreteFor(transportadora string, result chan float32) {
	switch transportadora {
	case "correios":
		time.Sleep(5 * time.Second) // aguardando valor do frete usando os Correios como transportadora
		result <- 10
	case "sequoia":
		time.Sleep(3 * time.Second) // aguardando valor do frete usando os Correios como transportadora
		result <- 15
	}
}

func calculaFrete(w http.ResponseWriter, req *http.Request) {
	ctx, cancel := context.WithTimeout(req.Context(), 2*time.Second)
	defer cancel()

	result := make(chan float32)
	go func() {
		value, err := getFreteCorreios(ctx)
		if err != nil {
			// log de erro
			return
		}
		result <- value
	}()

	go func() {
		value, err := getFreteSequoia(ctx)
		if err != nil {
			// log de erro
			return
		}
		result <- value
	}()

	select {
	case <-ctx.Done():
		fmt.Println("Não terminou a tempo")
	case r := <-result:
		fmt.Printf("Terminou a tempo e o valor do frete calculado mais rapido foi: %.2f\n", r)
	}
}