Gestores de contexto no Go? Não seja ridículo …

Atualização: 22/08/2013 – role para baixo até a atualização onde mostro como esse padrão pode ser usado para construir um gerenciador de contexto Redis para transações atômicas.

Tenho tentado criar casos de uso para a palavra-chave defer em Go. Eu realmente gosto do padrão de quando algum tipo de objeto é aberto como uma conexão tcp ou arquivo, você só precisa se lembrar de adiar o fechamento logo após o referido objeto e não importa o caminho do código que sua função tome, você pode ter certeza de que, você adiou será fechado. Para o registro, não estou propondo que a linguagem deva implementar gerenciadores de contexto, mas sim que seu aplicativo poderia se beneficiar de um gerenciador de contexto para ajudar a minimizar erros / limpeza.

Vemos isso o tempo todo com o código idiomático Go, como no seguinte snippet:

fi, err := os.Open("input.txt")
if err != nil {
panic
(err)
}
// close fi on exit and check for its returned error
defer func
() {
if err := fi.Close(); err != nil {
panic
(err)
}
}()

Então, está tudo muito bem. Mas, você ainda deve se lembrar de adiar um Close em seu objeto aberto. E se levássemos o conceito um pouco mais longe e abstraíssemos o adiamento em um gerenciador de contexto. Vemos esse padrão usado em linguagens como Python com a instrução “with” e, adicionalmente, em linguagens como C # com o que é conhecido como bloco “using”. Basicamente, tudo o que você está fazendo é designar um escopo para que alguma ação ocorra de forma que, quando o escopo terminar, um marcador determinístico diga ao tempo de execução para limpar seus recursos.

Então, para ser bem honesto, nem tenho certeza de que isso faria sentido em uma linguagem como Go. Afinal, é um idioma diferente; para mim, acho que um gerenciador de contexto pode ser útil para ajudar a abstrair alguns dos clichês do Go, como o fechamento e até mesmo alguns dos erros de tratamento do Go.

Considere o exemplo a seguir de como abrir um arquivo em Go e fazer algo trivial como ler todo o seu conteúdo. O código Go típico pode ser semelhante a:

func OpenFile(name string){
fi, err := os.Open(name)
if err != nil{
panic
(err)
}

b
, err := ioutil.ReadAll(fi)
if err != nil{
panic
(err)
}

//Do something with the data in this case just printing it to stdout
fmt
.Println(string(b))

defer func
() {
if err := fi.Close(); err != nil {
panic
(err)
}
}()
}

Portanto, vamos reconhecer algumas coisas com o código acima. Existem três pontos onde o tratamento de erros é feito, o que não é tão divertido, mas provavelmente nos ajuda, desenvolvedores, a escrever um código mais robusto, porque estamos adquirindo um bom hábito de lidar com erros. A outra coisa sobre esse código é que ele tem um adiamento para fechar o arquivo, além de lidar com um possível erro no fechamento. Agora em muitos exemplos na web eu vejo pessoas adiando o Close e não lidando com o erro. Eu sinto que com Go precisamos ser disciplinados, mas não muito? Ei, mesmo fmt.Println retorna um erro …

Então, talvez possamos ser oportunistas aqui e matar dois coelhos com uma cajadada só. Talvez possamos tentar abstrair parte do tratamento de erros e, adicionalmente, ter certeza de adiar nosso Close (automaticamente para o chamador) e também lidar com a possibilidade de um erro ocorrer.

Isso exige um gerenciador de contexto. Algo que felizmente não é muito doloroso de escrever, mas nos ajuda a lidar estrategicamente com a parte essencial do código em que estamos realmente interessados. No caso acima, estou preocupado em obter um * os.File entregue a mim, chamando a função ioutil.ReadAll (), tratando do erro potencial em ReadAll () e simplesmente fazendo algo com os dados e seguindo em frente com minha vida. Para mim, essa é a verdadeira carne do código acima.

Aqui está o conceito de um “gerenciador de contexto” para Go que será responsável por algum tratamento de erros, enquanto ainda permite que meu código seja executado e, por fim, seja limpo automaticamente.

Função do gerenciador de contexto definida abaixo:

func Open(name string, cb func(f io.ReadWriteCloser)){
fi, err := os.Open(name)
if err != nil{
panic
(err)
}
defer func
() {
if err := fi.Close(); err != nil {
panic
(err)
}
}()

cb
(fi)
}

Exemplo de uso abaixo:

someObject := NewObject()

Open("input.txt", func(f io.ReadWriteCloser){
b
, err := ioutil.ReadAll(f)
if err != nil{
log
.Println("Could not read all of the glory.")
log
.Fatal(err)

}

//Close over someObject, now we can call a method on it.
someObject
.ProcessData(string(b))
})

Portanto, a primeira função chamada “Abrir” define o gerenciador de contexto. Observe que ele opera em um ReadWriteCloser, portanto, esperançosamente, podemos encontrar maneiras alternativas de usá-lo em ReadWriteClosers em geral. Neste exemplo, Open nos permite passar duas coisas. O nome de um arquivo e uma função de retorno de chamada a ser executada. Pensei em passar o erro para o retorno de chamada, mas achei que, como estava disposto a transferir parte da responsabilidade de lidar com os erros para o gerenciador de contexto, achei que era desnecessário para o meu caso. Na verdade, no meu caso, fico em pânico se não consigo ler / gravar arquivos. Observe também que o gerenciador de contexto está tratando do fechamento do arquivo para mim junto com seus possíveis erros. Novamente, estou disposto a deixar o gerenciador de contexto lidar com esses erros para mim.

O exemplo de uso é simplesmente o uso do gerenciador de contexto. Chamamos o gerenciador de contexto e passamos nosso nome de arquivo para operar e o retorno de chamada com o ReadWriteCloser. Se tudo correr bem, podemos esperar um identificador * os.File devidamente configurado que está pronto para uso. No meu exemplo de uso, estou simplesmente chamando a função ReadAll (), que também pode retornar um erro. Nesse caso, devo lidar com esse erro localmente para o meu uso do gerenciador de contexto porque optei por usar a função ReadAll. Posso ter usado algo diferente, como um scanner.

Este exemplo é obviamente trivial, mas quanto mais podemos utilizar o gerenciador de contexto, mais podemos reduzir o tratamento de erros e agora não precisamos nos lembrar de adiar nosso método Close. Além disso, como introduzimos um encerramento , agora podemos fazer referência a itens que “encerramos” e fazer uso de tudo o que estiver acessível em nosso gerenciador de contexto. Não posso enfatizar o suficiente o quão crítica é essa ideia. Geralmente é explorado em JavaScript, mas com um uso criterioso podemos nos beneficiar muito com isso.

Este conceito também não se aplica a arquivos simplesmente. Talvez também valha a pena criar um gerenciador de contexto para fazer algo como uma transação do Redis. Talvez, nesse cenário, possamos ter um gerenciador de contexto que nos permite especificar vários comandos que devem ser executados em uma transação e, em seguida, renuncia a nossa conexão Redis de volta para um pool.

Atualização em 22/11/2013

Aqui está um exemplo de um Context Manager para a biblioteca Redigo para fazer uma transação Redis. Este é um exemplo um pouco mais complexo, mas você verá os benefícios de seu uso no bloco de código a seguir.

type Transaction struct {
err error

success func
(interface{})
reply
interface{}
}

func
NewTransaction() *Transaction {
return &Transaction{}
}

func
(t *Transaction) Do(cb func(conn redis.Conn)) *Transaction {
//pool is a global object that has been setup in my app
c
:= pool.Get()
defer c
.Close()
c
.Send("MULTI")
cb
(c)
reply
, err := c.Do("EXEC")
t
.reply = reply
t
.err = err
return t
}

func
(t *Transaction) OnFail(cb func(err error)) *Transaction {
if t.err != nil {
cb
(t.err)
} else {
t
.success(t.reply)
}
return t
}

func
(t *Transaction) OnSuccess(cb func(reply interface{})) *Transaction {
t
.success = cb
return t
}

Exemplo de uso da Transação Redis:

NewTransaction().Do(func(c redis.Conn) {
c
.Send("INCR", "Counter")
c
.Send("DECR", "Counter")
c
.Send("INCR", "Counter")
c
.Send("DECR", "Counter")
c
.Send("INCR", "Counter")
c
.Send("DECR", "Counter")
c
.Send("INCR", "Counter")
c
.Send("DECR", "Counter")
}).OnSuccess(func(reply interface{}) {
log
.Println("Success!")
log
.Println(reply)
}).OnFail(func(err error) {
log
.Println("Oh no, transaction failed, alert user.")
log
.Println(err)
})

Observe como a classe Transaction nos dá uma abstração mais alta sobre como lidar com erros, bem como lidar com a limpeza de devolver nossa conexão ao pool de conexão do Redis. Nesse cenário, tudo que me importa é se minha transação foi concluída ou falhou. E que em ambos os cenários, tenho os meios para lidar com o erro ou a resposta da resposta atômica do Redis. Observação: as transações Redis não se comportam da mesma forma que as transações SQL. Além disso, vamos anotar as coisas que meu gerenciador de contexto está tratando para mim: obtendo uma conexão do pool Redis, retornando a conexão, chamando o manipulador de sucesso ou falha apropriado enquanto injeta o erro ou resposta apropriada, manipulando os comandos Redis para fazendo um comando “MULTI” / “EXEC” antes / depois do meu código.

O exemplo de gerenciador de contexto de transação do Redigo acima é simplesmente um exemplo que pode ser usado com o cliente Redigo de Gary Burd para Go. Se desejar usar o código acima, você precisará visitar a página do github para obter instruções de instalação: https://github.com/garyburd/redigo

Espero que isso tenha sido útil. Além disso, se você achar isso útil, dê-me algum feedback nos comentários abaixo. Acho que poderia haver alguns bons casos de uso para gerenciadores de contexto em Go e ainda estou procurando respostas.

Obrigado,

-Ralph