Contextos no Go

Introdução

Uma das responsabilidades que possuo onde trabalho atualmente é ensinar e orientar pessoas menos experientes, e uma das principais dúvidas que recebo relacionadas a Go é sobre os contextos.

Citação

Mas eu não aguento mais passar isso em praticamente toda chamada que faço. Outras linguagens não têm isso, por que eu tenho que me preocupar?

- Pessoa desenvolvedora vendo contextos em todo lugar

Para entender a utilidade dos contextos, precisamos estar cientes de que concorrência é algo comum no desenvolvimento atual, mesmo que não percebamos.

Novas tarefas podem surgir das mais diversas formas: processos, threads, goroutines (ou green threads). Além de surgir, elas também podem — e serão — interrompidas.

O Go nos oferece uma forma de sinalizar que um conjunto de tarefas precisa ser cancelado e também de tratar esse sinal no código de um sistema.

Interrupções no dia a dia

Nosso cotidiano é repleto de situações que não saem como o planejado. Muitas vezes, precisamos ter um plano B ou parar para pensar em como lidar com essas mudanças. Nos sistemas que construímos, isso não é diferente — exceto que os computadores fazem exatamente o que pedimos, sem a capacidade de se adaptar sozinhos.

Para ilustrar essa ideia, vamos a uma analogia:

Analogia

Imagine que você vai almoçar em um restaurante. Ao se sentar, escolhe um prato do cardápio, chama o garçom e faz seu pedido.

O garçom anota o pedido e o envia para a cozinha, onde os cozinheiros começam a prepará-lo.

Agora, imagine que, no meio disso, você recebe uma ligação urgente e precisa sair imediatamente. Você cancela o pedido com o garçom.

O que acontece com o prato que estava sendo preparado? Isso depende do funcionamento do restaurante: ele pode descartar tudo o que já foi feito, ou tentar reaproveitar os ingredientes que ainda estão bons.

Assim como o restaurante precisou lidar com o pedido cancelado, em sistemas concorrentes também há momentos em que precisamos interromper uma tarefa em andamento.

No exemplo, você percebeu que precisava sair e não voltaria, então cancelou o pedido com o garçom — que, por sua vez, comunicou o cancelamento à cozinha.

O que isso tem a ver com meu código?

Além do exemplo do restaurante, podemos pensar em situações mais próximas da realidade de quem desenvolve: pode ser o download de um arquivo grande, uma requisição para uma API REST, uma consulta ao banco de dados ou a execução de qualquer tarefa que pode levar muito tempo para completar.

Como você faria para cancelar algo, se fosse necessário? E como notificaria esse cancelamento em efeito cascata para todas as operações relacionadas?

Às vezes, uma simples sinalização é suficiente. Em outros casos, como no restaurante, o cancelamento precisa ser propagado por diferentes partes do sistema — e pode ter consequências importantes que precisam ser tratadas adequadamente.

Cada linguagem, biblioteca ou framework oferece sua própria solução para lidar tanto com o ciclo de vida quanto com o tempo de vida de fluxos e operações. Os desenvolvedores do Go enxergaram essa necessidade — e é aí que entra o pacote context.

Mas antes de falar sobre contextos, vale lembrar: o Go foi criado com a filosofia de que ser explícito é melhor do que esconder complexidade por trás de abstrações implícitas.

Citação

Explícito é melhor que implícito

- Item 2 do Zen of Python

Apesar das coisas nem sempre serem tão explícitas assim no Python, esse é um bom conselho a ser seguido para qualquer linguagem. Inclusive, recomendo também a leitura do Zen of Go.

Entrando no contexto

Agora que entendemos por que é importante poder interromper tarefas, vamos ver como o Go nos ajuda a fazer isso de forma estruturada com o pacote context.

O pacote context define um padrão para lidar com cenários em que sinais de cancelamento, timeouts ou deadlines precisam ser propagados ao longo da execução. Ele permite que fluxos sejam encerrados de forma controlada — o que costumamos chamar de graceful shutdown.

A interface context.Context

Vamos começar olhando a documentação da interface context.Context. Na descrição do tipo, vemos:

Citação

A Context carries a deadline, a cancellation signal, and other values across API boundaries.
Context’s methods may be called by multiple goroutines simultaneously.

Tradução

Um contexto carrega um prazo, um sinal de cancelamento, e outros valores através dos limites da API.
Os métodos do tipo Context podem ser chamados simultaneamente por múltiplas goroutines

Como cancelar contextos?

Se você foi curioso ou curiosa e também deu uma olhada na própria definição da interface, possivelmente notou que não existe um método Cancel() ou algo parecido. Ou seja, um contexto não tem capacidade de ativar um sinal de cancelamento para ele mesmo.

Por que os contextos não podem cancelar a si próprios?

Para explicar o motivo, vamos voltar ao exemplo do restaurante: imagine que o garçom ou outro cliente pudesse cancelar seu pedido, ou pior, todos os pedidos. Você também pode pensar em uma tarefa que inicia várias goroutines em paralelo para realizar o download de vários arquivos — a exposição de uma forma de cancelar dentro do próprio contexto poderia possibilitar o cancelamento de outros downloads iniciados pela mesma tarefa. Seria uma tragédia! 😟

Logo, a existência da possibilidade de cancelar um contexto em qualquer escopo seria terrivelmente perigosa e poderia impactar diversos fluxos de forma imprevisível. Então, caso seja necessário ter um controle fino sobre o tempo de vida de um determinado fluxo da sua aplicação, a recomendação é criar uma nova instância do context.Context. O pacote context já oferece algumas formas para criar contextos — vamos dar uma olhada nelas?

Criando novos contextos

É importante lembrar que, exceto pelos métodos context.Background() e context.TODO(), todos as formas de criação de um contexto exigem um contexto base.

Sempre que um contexto base for cancelado, os cancelamentos serão propagados para todos os contextos derivados.

Nessa seção teremos alguns exemplos mais completos que irão utilizar o código abaixo, esses exemplos também contam com um link para execução no Go Playground.

Estrutura dos exemplos

Para alguns dos exemplos a seguir, decidi seguir a vibe de restaurante e criei duas structs.

A restaurant representando o próprio restaurante, com o metodo order para “receber” os pedidos e encaminhá-los a cozinha:

Go
type restaurant struct {
    kitchen kitchen
}

func (r restaurant) order(ctx context.Context, dish string) {
    fmt.Printf("restaurante: preparando o pedido %q\n", dish)
    r.kitchen.cook(ctx, dish)
    <-ctx.Done() // espera o contexto ser cancelado
    fmt.Printf("restaurante: pedido %q cancelado: %s\n", dish, context.Cause(ctx))
}

A outra, kitchen, representa a cozinha — e tem o método cook representando o ato de cozinhar o prato:

Go
type kitchen struct{}

func (k kitchen) cook(ctx context.Context, dish string) {
    fmt.Printf("cozinha: fingindo que estamos fazendo %q\n", dish)
    <-ctx.Done() // espera o contexto ser cancelado
    fmt.Printf("cozinha: parando de fazer %q: %s\n", dish, context.Cause(ctx))
}

Note que nesse exemplo estamos usando o método ctx.Done(), e a função context.Cause(ctx), vamos falar mais sobre elas na seção lidando com o cancelamento

Sem possibilidade de cancelamento

Decidi organizar as funções de criação de contexto pela possibilidade ou não de cancelamento. Vamos iniciar pelas formas que não oferecem nenhum tipo de mecanismo para cancelamento.

Criando contextos básicos — context.Background

O context.Background cria um novo contexto vazio — ou seja, sem prazos, sem valores e que não pode ser cancelado.

Ele costuma ser usado como ponto de partida quando precisamos passar um contexto, mas ainda não temos um contexto anterior disponível.

É muito comum usá-lo no início da aplicação (como na função main), durante a configuração de serviços ou em testes, onde precisamos de um contexto base para começar a construir outros contextos a partir dele.

Go
ctx := context.Background()

Criando contextos provisórios - context.TODO

O context.TODO pode ser usado quando não se tem certeza de qual outra opção deve ser usada. A intenção dele é ser apenas um placeholder e deve ser substituído.

Assim como o context.Background, ele não pode ser cancelado.

Go
ctx := context.TODO()

Contextos que carregam valores — context.WithValue

Também existe a função context.WithValue, que permite a criação de um contexto com valores armazenados internamente.

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    // Cria um contexto com valor
    ctx := context.WithValue(context.Background(), "orderId", "1")
    dish := "sorvete de cebola"
    fmt.Printf("cliente: pedindo %q\n", dish)

    iceCreamPlace.order(ctx, dish)
}
Text
cliente: pedindo "sorvete de cebola"
restaurante: recebendo pedido de "sorvete de cebola"
cozinha: preparando "sorvete de cebola" (pedido #1)
Atenção

Recomendo bastante cautela ao utilizar contextos dessa forma, embora seja muito útil para passar agentes de métricas, tracing ou dados de um request — como um request id — entre as diferentes camadas, o abuso dessa opção pode causar problemas de clareza no código.

Essa funcionalidade não deve ser utilizada como um dicionário genérico global.

Criando contextos derivados sem cancelamento — context.WithoutCancel

Disponível a partir do Go 1.21

O context.WithoutCancel cria um novo contexto a partir de um contexto base, mas continuará ativo, mesmo que o original tenha sido cancelado.

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    // Cria um contexto com cancelamento
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    // Cria um contexto derivado que ignora o cancelamento do pai
    ctx2 := context.WithoutCancel(ctx)

    dish := "sorvete de cebola"
    fmt.Printf("cliente: pedindo %q\n", dish)

    go func() {
        iceCreamPlace.order(ctx2, dish)
    }()

    fmt.Println("cliente: esperando o pedido ficar pronto")
    time.Sleep(1 * time.Second)
    fmt.Printf("cliente: cancelando %q\n", dish)
    cancel() // Cancela o contexto pai, mas ctx2 não será cancelado

    time.Sleep(2 * time.Second)
    fmt.Printf("cliente: verificando status do pedido %q\n", dish)
}

Com possibilidade de cancelamento

Criando contextos com cancelamento — context.WithCancel e context.WithCancelCause

A função context.WithCancel possui dois retornos. O primeiro é o próprio contexto, que deve ser repassado nas chamadas em que ele se faz necessário, já o segundo é uma função de cancelamento que, ao ser chamada, irá cancelar o contexto criado.

Aviso

É sempre uma boa prática utilizar o defer nas funções de cancelamento, pois ajuda a evitar o vazamento de goroutines.

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    dish := "sorvete de cebola"
    fmt.Printf("cliente: pedindo %q\n", dish)

    go func() {
        iceCreamPlace.order(ctx, dish)
    }()

    fmt.Println("cliente: esperando o pedido ficar pronto")
    time.Sleep(1 * time.Second)
    fmt.Printf("cliente: cancelando %q\n", dish)
    cancel()
    time.Sleep(1 * time.Second)
}
Text
cliente: pedindo "sorvete de cebola"
cliente: esperando o pedido ficar pronto
restaurante: preparando o pedido "sorvete de cebola"
cozinha: fingindo que estamos fazendo "sorvete de cebola"
cliente: cancelando "sorvete de cebola"
cozinha: parando de fazer "sorvete de cebola": context canceled
restaurante: pedido "sorvete de cebola" cancelado: context canceled

Também é possível evidenciar a causa do cancelamento utilizando a função context.WithCancelCause.

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    ctx, cancel := context.WithCancelCause(context.Background())
    defer cancel(nil) // retorna o erro original: "context canceled"
    dish := "sorvete de cebola"
    fmt.Printf("cliente: pedindo %q\n", dish)

    go func() {
        iceCreamPlace.order(ctx, dish)
    }()

    fmt.Println("cliente: esperando o pedido ficar pronto")
    time.Sleep(1 * time.Second)
    cause := errors.New("cancelado pelo cliente")
    cancel(cause)
    time.Sleep(1 * time.Second)
}
Text
cliente: pedindo "sorvete de cebola"
cliente: esperando o pedido ficar pronto
restaurante: preparando o pedido "sorvete de cebola"
cozinha: fingindo que estamos fazendo "sorvete de cebola"
cozinha: parando de fazer "sorvete de cebola": cancelado pelo cliente
restaurante: pedido "sorvete de cebola" cancelado: cancelado pelo cliente

Com prazo de validade absoluto — context.WithDeadline e context.WithDeadlineCause

A context.WithDeadline, que recebe um time.Time e irá cancelar o contexto automaticamente após a data informada.

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    // Define um deadline para 2 segundos no futuro
    deadline := time.Now().Add(2 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()
    dish := "sorvete de cebola"
    fmt.Printf("cliente: pedindo %q\n", dish)

    go func() {
        iceCreamPlace.order(ctx, dish)
    }()

    fmt.Println("cliente: esperando o pedido ficar pronto")
    time.Sleep(3 * time.Second) // espera mais do que o deadline
    fmt.Printf("cliente: verificando status do pedido %q\n", dish)
}
Text
cliente: pedindo "sorvete de cebola"
cliente: esperando o pedido ficar pronto
restaurante: preparando o pedido "sorvete de cebola"
cozinha: fingindo que estamos fazendo "sorvete de cebola"
cozinha: parando de fazer "sorvete de cebola": context deadline exceeded
restaurante: pedido "sorvete de cebola" cancelado: context deadline exceeded
cliente: verificando status do pedido "sorvete de cebola"

Também podemos informar a causa do cancelamento trocando context.WithDeadline por context.WithDeadlineCause:

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    deadline := time.Now().Add(2 * time.Second)
    cause := errors.New("hora de levar minha avó pra aula de judô")
    ctx, cancel := context.WithDeadlineCause(context.Background(), deadline, cause)
    defer cancel()

    // restante do código
}
Text
cliente: pedindo "sorvete de cebola"
cliente: esperando o pedido ficar pronto
restaurante: preparando o pedido "sorvete de cebola"
cozinha: fingindo que estamos fazendo "sorvete de cebola"
cozinha: parando de fazer "sorvete de cebola": hora de levar minha avó pra aula de judô
restaurante: pedido "sorvete de cebola" cancelado: hora de levar minha avó pra aula de judô
cliente: verificando status do pedido "sorvete de cebola"

Com prazo de validade relativo — context.WithTimeout e context.WithTimeoutCause

A context.WithTimeout, que recebe um time.Duration e irá cancelar o contexto após o período informado.

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    // Define um timeout de 2 segundos
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    dish := "sorvete de cebola"
    fmt.Printf("cliente: pedindo %q\n", dish)

    go func() {
        iceCreamPlace.order(ctx, dish)
    }()

    fmt.Println("cliente: esperando o pedido ficar pronto")
    time.Sleep(3 * time.Second) // espera mais do que o timeout
    fmt.Printf("cliente: verificando status do pedido %q\n", dish)
}
Text
cliente: pedindo "sorvete de cebola"
cliente: esperando o pedido ficar pronto
restaurante: preparando o pedido "sorvete de cebola"
cozinha: fingindo que estamos fazendo "sorvete de cebola"
cozinha: parando de fazer "sorvete de cebola": context deadline exceeded
restaurante: pedido "sorvete de cebola" cancelado: context deadline exceeded
cliente: verificando status do pedido "sorvete de cebola"
Note que a saída é igual a do exemplo com context.WithDeadline

Aqui também é possível informar uma causa:

Go
func main() {
    iceCreamPlace := restaurant{kitchen: kitchen{}}

    // Define um timeout de 2 segundos
    cause := errors.New("desisti de esperar")
    ctx, cancel := context.WithTimeoutCause(context.Background(), 2*time.Second, cause)
    defer cancel()
    dish := "sorvete de cebola"
    fmt.Printf("cliente: pedindo %q\n", dish)

    go func() {
        iceCreamPlace.order(ctx, dish)
    }()

    fmt.Println("cliente: esperando o pedido ficar pronto")
    time.Sleep(3 * time.Second) // espera mais do que o timeout
    fmt.Printf("cliente: verificando status do pedido %q\n", dish)
}
Text
cliente: pedindo "sorvete de cebola"
cliente: esperando o pedido ficar pronto
restaurante: preparando o pedido "sorvete de cebola"
cozinha: fingindo que estamos fazendo "sorvete de cebola"
cozinha: parando de fazer "sorvete de cebola": desisti de esperar
restaurante: pedido "sorvete de cebola" cancelado: desisti de esperar
cliente: verificando status do pedido "sorvete de cebola"

Criando seu próprio contexto

Por se tratar de uma interface, você pode criar sua própria implementação. Pessoalmente não recomendo seguir por esse caminho, pois nesses meus quase 10 anos de Go eu ainda não vi nenhum cenário que as interfaces fornecidas pela biblioteca padrão não foram suficientes.

Lidando com o cancelamento

Não, não estamos falando de redes sociais

Você pode ativamente verificar se o contexto já foi cancelado, essa abordagem é útil para impedir que o fluxo prossiga. A forma mais comum é realizar a verificação ao início da função, mas em alguns momentos pode ser importante verificar ao final, para garantir que o fluxo não irá continuar.

Verificando se o contexto já foi cancelado — ctx.Err

Você pode simplesmente verificar se o retorno de ctx.Err() é não-nulo. A função retornará nil caso o contexto ainda não tenha sido cancelado e algum erro caso o cancelamento tenha ocorrido.

Go
// verificação antes do processamento ocorrer
if ctx.Err() != nil {
    return fmt.Errorf("falha antes de iniciar: %w", ctx.Err())
}

// implementação do código

// verificação após o processamento ter ocorrido
if ctx.Err() != nil {
    return fmt.Errorf("falha após a execução: %w", ctx.Err())
}

Identificando a causa do cancelamento — context.Cause

Disponível a partir do Go 1.21

A partir do Go 1.21 existe a opção de associar um error como causa do cancelamento de um contexto e que pode ser obtido utilizando a função context.Cause.

Caso o contexto tenha sido cancelado e exista uma causa não-nula, o valor retornado será o erro enviado como causa no momento do cancelamento. Já, se não existir uma causa específica, o valor será o mesmo da chamada ctx.Err(), que vimos anteriormente.

Go
if ctx.Err() != nil {
    // o contexto foi cancelado, só vamos retornar um erro informando o motivo, ok?
    return fmt.Errorf("failure before start long process: %w", context.Cause(ctx))
}

Escutando o sinal de cancelamento — ctx.Done

Além da verificação ativa utilizando o ctx.Err(), também é possível ouvir um sinal de cancelamento por meio de um canal do tipo <-chan struct{}, retornado pela chamada ctx.Done().

Canais

Os canais — do inglês channels — são condutores de informação seguros em ambientes concorrentes. Isso significa que podem ser usados por várias goroutines ao mesmo tempo, sem risco de condições de corrida.

Para entender melhor como canais funcionam, recomendo visitar o a seção do tour sobre concorrência:
Tour do Go — Concurrency.

Structs vazias (struct{})

A struct{} é uma estrutura vazia — ela não consome memória. Por isso, o canal retornado por ctx.Done() é usado apenas para sinalizar o cancelamento, sem carregar dados.

Saiba mais sobre structs vazias em:
The empty struct — Dave Cheney

Quando a chamada <-ctx.Done() é feita, o código aguarda o recebimento através do canal, bloqueando a execução da goroutine até receber algum conteúdo.

Go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Goroutine que aguarda o cancelamento do contexto
    go func() {
        <-ctx.Done() // aguardando o recebimento da mensagem de cancelamento
        fmt.Println("contexto cancelado após recebimento do canal!")
    }()

    fmt.Println("fazendo algo importante...")
    time.Sleep(1 * time.Second)
    fmt.Println("cancelando o contexto")
    cancel()
    time.Sleep(1 * time.Second) // aguardando callback ser executado
}
Text
fazendo algo importante...
cancelando o contexto
contexto cancelado após recebimento do canal!

Pela natureza bloqueante da chamada, geralmente usamos uma cláusula select para escolher entre o resultado do ctx.Done() e algum outro canal.

Go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Goroutine que aguarda o cancelamento do contexto
    go func() {
        fmt.Println("goroutine: aguardando cancelamento ou conclusão...")
        select {
        case <-ctx.Done(): // Escuta o cancelamento do contexto
            fmt.Println("goroutine: contexto cancelado!")
        case <-time.After(2 * time.Second): // Simula uma tarefa longa
            fmt.Println("goroutine: tarefa concluída!")
        }
    }()

    fmt.Println("fazendo algo importante...")
    time.Sleep(1 * time.Second)
    fmt.Println("cancelando o contexto")
    cancel()
    time.Sleep(1 * time.Second) // Aguarda para garantir que a goroutine finalize
}
Text
fazendo algo importante...
goroutine: aguardando cancelamento ou conclusão...
cancelando o contexto
goroutine: contexto cancelado!

Usando callbacks para tratar um contexto cancelado — context.AfterFunc

Disponível a partir do Go 1.21

A função context.AfterFunc(ctx Context, f func()) recebe um context ctx, que ao ser cancelado executa a função f. Dessa forma, a função f age como um callback acionado no cancelamento, útil em tarefas paralelas que não precisam retornar um erro — mas precisam fazer algum tratamento quando o contexto for cancelado.

Essa chamada retorna uma func() bool que pode ser chamada para desfazer a associação dela com o contexto ctx, fazendo com que ela não seja mais chamada caso o contexto seja cancelado. Ela irá retornar true .

Go
package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Callback que será chamado quando o contexto for cancelado
    callback := func() {
        fmt.Println("callback: contexto cancelado!")
    }
    stop := context.AfterFunc(ctx, callback)
    defer stop()

    callback2 := func() {
        fmt.Println("callback2: não serei executado")
    }
    stop2 := context.AfterFunc(ctx, callback2)
    fmt.Println("Conseguimos cancelar o callback2?", stop2())
    fmt.Println("Conseguimos cancelar o callback2?", stop2(), "(já está cancelado)")

    cancel()

    time.Sleep(1 * time.Second) // aguardando callbacks serem executados
}
Text
Conseguimos cancelar o callback2? true
Conseguimos cancelar o callback2? false (já está cancelado)
callback: contexto cancelado!

Referências e material adicional

Estamos chegando ao final e não poderia deixar aqui algumas sugestões de material para você consolidar e até se aprofundar sobre concorrência, contextos e goroutines. Todos eles estão em inglês, mas você pode recorrer ao Google Tradutor ou alguma IA.

Conclusão

Talvez você já tenha lido a documentação do pacote context anteriormente e sentido dificuldades em entender de quando e onde os contextos podem ou devem ser usados. Isso pode ter acontecido porque a documentação se concentra em informar quais funcionalidades existem, mas não os casos de uso prático em que elas se aplicam.

Com esse artigo, foquei em tentar introduzir o tema com alguns exemplos práticos e analogias — e espero que isso tenha te ajudado a entender melhor sobre o tema.

Se você tiver dúvidas, sugestões ou apenas quiser trocar uma ideia, sinta-se à vontade para entrar em contato através dos comentários ou pelos links abaixo:

Estou sempre aberto a feedbacks e novas ideias! 🚀

Até uma próxima 👋!