Compreendendo o pacote de contexto em Golang

imagem


O pacote de contexto no Go é útil para interações com APIs e processos lentos, especialmente em sistemas de nível de produção que lidam com solicitações da Web. Com sua ajuda, as goroutines podem ser notificadas da necessidade de concluir seu trabalho.


Abaixo está um pequeno guia para ajudá-lo a usar este pacote em seus projetos, bem como algumas das melhores práticas e armadilhas.


(Nota: o contexto é usado em muitos pacotes, por exemplo, no trabalho com o Docker ).


Antes de começar


Para usar contextos, você deve entender o que são goroutinas e canais. Vou tentar considerá-los brevemente. Se você já está familiarizado com eles, vá diretamente para a seção Contexto.


Gorutin


A documentação oficial diz que "Gorutin é um fluxo leve de execução". As goroutines são mais leves que os threads, portanto, gerenciá-los é relativamente menos intensivo em 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") } 

Se você executar este programa, verá que apenas o Hello from main impresso. De fato, ambas as goroutines começam, mas as main terminam mais cedo. Então, as Goroutines precisam de uma maneira de informar main sobre o final de sua execução, e para que ela espere por isso. Aqui os canais vêm em nosso auxílio.


Canais


Canais são um meio de comunicação entre goroutines. Eles são usados ​​quando você deseja transferir resultados, erros ou outras informações de uma goroutina para outra. Canais são de tipos diferentes, por exemplo, um canal do tipo int recebe números inteiros e um canal de error do tipo recebe erros etc.


Digamos que temos um canal ch do tipo int . Se você quiser enviar algo para o canal, a sintaxe será ch <- 1 . Você pode obter algo do canal como este: var := <- ch , ou seja, pegue o valor do canal e salve-o na variável var .


O código a seguir ilustra como usar os canais para confirmar que as goroutines concluíram seu trabalho e retornaram seus valores para main .


Nota: Os grupos de espera também podem ser usados ​​para sincronização, mas neste artigo selecionei canais para exemplos de código, pois os usaremos posteriormente na seção 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


O pacote de contexto em movimento permite que você transmita dados para o seu programa em algum tipo de "contexto". O contexto, como um tempo limite, prazo ou canal, sinaliza um desligamento e as chamadas retornam.


Por exemplo, se você fizer uma solicitação da Web ou executar um comando do sistema, seria uma boa idéia usar um tempo limite para sistemas de nível de produção. Como se a API que você está acessando é lenta, é improvável que você queira acumular solicitações em seu sistema, pois isso pode levar ao aumento da carga e à diminuição do desempenho ao processar suas próprias solicitações. O resultado é um efeito em cascata.


E aqui o tempo limite ou o contexto do prazo final podem estar certos.


Criação de contexto


O pacote de contexto permite criar e herdar contexto das seguintes maneiras:


context.Background () ctx Contexto


Esta função retorna um contexto vazio. Ele deve ser usado apenas em um nível alto (no principal ou no manipulador de solicitações de nível mais alto). Pode ser usado para obter outros contextos, que discutiremos mais adiante.


 ctx, cancel := context.Background() 

Nota trans .: existe uma imprecisão no artigo original, o exemplo correto de uso de context.Background será o seguinte:


 ctx := context.Background() 

context.TODO () ctx Contexto


Essa função também cria um contexto vazio. E também deve ser usado apenas em alto nível, quando você não tem certeza de qual contexto usar ou se a função ainda não recebe o contexto desejado. Isso significa que você (ou alguém que oferece suporte ao código) planeja adicionar contexto à função posteriormente.


 ctx, cancel := context.TODO() 

Nota trans .: existe uma imprecisão no artigo original; o exemplo correto de uso de context.TODO será o seguinte:


 ctx := context.TODO() 

Curiosamente, dê uma olhada no código , é absolutamente o mesmo que em segundo plano. A única diferença é que, nesse caso, você pode usar as ferramentas de análise estática para verificar a validade da transferência de contexto, que é um detalhe importante, pois essas ferramentas ajudam a identificar possíveis erros em um estágio inicial e podem ser incluídas no pipeline de CI / CD.


A partir daqui :


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

context.WithValue (contexto pai, chave, interface val {}) (contexto ctx, cancele CancelFunc)


Nota Lane: existe uma imprecisão no artigo original, a assinatura correta para o context.WithValue será a seguinte:


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

Essa função pega um contexto e retorna um contexto derivado dele, no qual o valor val está associado à key e passa por toda a árvore de contexto. Ou seja, assim que você criar um contexto WithValue , qualquer contexto derivado receberá esse valor.


Não é recomendável passar parâmetros críticos usando valores de contexto; em vez disso, as funções devem levá-los explicitamente na assinatura.


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

context.WithCancel (Contexto pai) (Contexto ctx, cancele CancelFunc)


Fica um pouco mais interessante aqui. Essa função cria um novo contexto a partir do pai passado para ele. O pai pode ser o contexto de segundo plano ou o contexto passado como argumento para a função.


O contexto derivado e a função desfazer são retornados. Somente a função que a cria deve chamar a função para cancelar o contexto. Você pode passar a função desfazer para outras funções, se desejar, mas isso é fortemente desencorajado. Normalmente, essa decisão é tomada a partir de um mal-entendido do cancelamento do contexto. Por esse motivo, os contextos gerados a partir desse pai podem afetar o programa, o que levará a um resultado inesperado. Em resumo, é melhor NUNCA passar uma função de cancelamento.


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

Nota Lane: No artigo original, o autor, aparentemente, erroneamente para context.WithCancel deu um exemplo com context.WithDeadline . O exemplo correto para context.WithCancel seria:


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

context.WithDeadline (Contexto pai, d time.Time) (Contexto ctx, cancele CancelFunc)


Essa função retorna um contexto derivado de seu pai, que é cancelado após um prazo ou chama a função de cancelamento. Por exemplo, você pode criar um contexto que é automaticamente cancelado em um horário específico e passa para as funções filho. Quando esse contexto é cancelado após o prazo, todas as funções que possuem esse contexto devem ser notificadas por notificação.


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

context.WithTimeout (Contexto pai, timeout time.Duration) (Contexto ctx, cancele CancelFunc)


Essa função é semelhante ao context.WithDeadline. A diferença é que o período de tempo é usado como entrada. Esta função retorna um contexto derivado que é cancelado quando a função de cancelamento é chamada ou após um tempo.


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

Nota Lane: No artigo original, o autor, aparentemente, erroneamente para context.WithTimeout deu um exemplo com context.WithDeadline . O exemplo correto para context.WithTimeout seria este:


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

Recepção e uso de contextos em suas funções


Agora que sabemos como criar contextos (Background e TODO) e como gerar contextos (WithValue, WithCancel, Deadline e Timeout), vamos discutir como usá-los.


No exemplo a seguir, você pode ver que a função que pega o contexto inicia a goroutine e espera que ela retorne ou cancele o contexto. A instrução select nos ajuda a determinar o que acontece primeiro e encerrar a função.


Depois de fechar o canal Concluído <-ctx.Done() , a caixa do case <-ctx.Done(): é selecionada. Assim que isso acontecer, a função deve interromper o trabalho e se preparar para um retorno. Isso significa que você deve fechar todas as conexões abertas, liberar recursos e retornar da função. Há momentos em que a liberação de recursos pode atrasar o retorno, por exemplo, a limpeza trava. Você deve manter isso em mente.


O exemplo a seguir nesta seção é um programa go completo que ilustra tempos limite e desfaz funções.


 // ,  -      // ,   -    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") } } 

Exemplo


Como vimos, usando contextos, você pode trabalhar com prazos, tempos limites e também chamar a função cancel, deixando assim clara para todas as funções usando um contexto derivado que você precisa concluir seu trabalho e executar o retorno. Considere um exemplo:


função main :


  • Cria um contexto de função de cancelamento
  • Chama a função cancelar após um tempo limite arbitrário

Função doWorkContext :


  • Cria um contexto derivado com um tempo limite
  • Esse contexto é cancelado quando a função principal chama cancelFunction, o tempo limite expira ou doWorkContext chama cancelFunction.
  • Executa goroutine para executar alguma tarefa lenta, passando o contexto resultante
  • Aguarda a conclusão de goroutines ou o contexto a ser cancelado do main, o que ocorrer primeiro

Função sleepRandomContext :


  • Lança goroutine para executar alguma tarefa lenta
  • Aguarda a conclusão da goroutina, ou
  • Aguarda que o contexto seja cancelado pela função principal, tempo limite ou chame sua própria função cancelFunction

função sleepRandom :


  • Adormece em tempo aleatório

Este exemplo usa o modo de suspensão para simular um tempo de processamento aleatório, mas, na realidade, você pode usar canais para sinalizar essa função sobre o início da limpeza e aguardar a confirmação do canal de que a limpeza foi concluída.


Caixa de areia (parece que o tempo aleatório que eu uso na caixa de areia é praticamente inalterado. Tente isso no computador local para ver a aleatoriedade)


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) } 

Armadilhas


Se a função usar contexto, verifique se as notificações de cancelamento são tratadas corretamente. Por exemplo, esse exec.CommandContext não fecha o canal de leitura até que o comando conclua todos os garfos criados pelo processo ( Github ), ou seja, que o cancelamento do contexto não retorne imediatamente da função se você esperar com cmd.Wait (), até que todos os garfos do comando externo concluam o processamento.


Se você usar um tempo limite ou tempo limite com um tempo de execução máximo, ele pode não funcionar conforme o esperado. Nesses casos, é melhor implementar tempos limite usando time.After .


Melhores práticas


  1. context.Background deve ser usado apenas no nível mais alto, como a raiz de todos os contextos derivados.
  2. context.TODO deve ser usado quando você não tiver certeza do que usar ou se a função atual usará o contexto no futuro.
  3. Os cancelamentos de contexto são recomendados, mas essas funções podem levar algum tempo para serem limpas e fechadas.
  4. context.Value deve ser usado com a menor moderação possível e não deve ser usado para passar parâmetros opcionais. Isso torna a API incompreensível e pode levar a erros. Tais valores devem ser passados ​​como argumentos.
  5. Não armazene contextos em uma estrutura, passe-os explicitamente em funções, de preferência como o primeiro argumento.
  6. Nunca passe um contexto nulo como argumento. Em caso de dúvida, use TODO.
  7. A estrutura de Context não possui um método de cancelamento, porque somente a função que gera o contexto deve cancelá-lo.

Do tradutor


Em nossa empresa, usamos ativamente o pacote Context ao desenvolver aplicativos de servidor para uso interno. Mas esses aplicativos para o funcionamento normal, além do Contexto, exigem elementos adicionais, como:


  • Registo
  • Processamento de sinal para término, recarregamento e rotação do aplicativo
  • Trabalhar com arquivos pid
  • Trabalhar com arquivos de configuração
  • E outro

Portanto, em algum momento, decidimos resumir toda a nossa experiência e criamos pacotes auxiliares que simplificam bastante os aplicativos de gravação (especialmente aplicativos que possuem APIs). Publicamos nossos desenvolvimentos em domínio público e qualquer pessoa pode usá-los. A seguir, estão alguns links para pacotes úteis para resolver esses problemas:



Leia também outros artigos em nosso blog:


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


All Articles