Gerenciamento de Contexto em Go
Gerenciamento de Contexto em Go
O gerenciamento de contexto em Go é fundamental para o desenvolvimento de aplicações robustas e manuteníveis. Pensei em escrever esse artigo para compartilhar minhas experiências com a prática de armazenar valores em context.Context no Go.
Normalmente o que encontro em códigos que utilizam o context.Context são mais ou menos como esse abaixo:
package usercontext
const (
UserID = "user_id"
PermissionMap = "user_permissions"
)
func store(ctx context.Context, userID, permissions string) context.Context {
ctx = context.WithValue(ctx, UserID, userID)
ctx = context.WithValue(ctx, PermissionMap, permissions)
return ctx
}
func get(ctx context.Context) (string, map[string]string, error) {
var permissionMap map[string]string
userID := ctx.Value(UserID).(string)
permissionStr := ctx.Value(PermissionMap).(string)
err := json.Unmarshal([]byte(permissionStr), &permissionMap)
return userID, permissionMap, err
}
Armazenar valores diretamente no context.Context pode parecer uma solução simples, mas essa prática introduz uma série de desafios que comprometem a robustez e a manutenibilidade do código. Para mitigar os problemas, podemos utilizar tipos estruturados (structs) e chaves de contexto privadas.
Exemplo Prático
package usercontext
import (
"context"
"fmt"
)
type userContextKey struct{}
type UserData struct {
ID string
Name string
Email string
Permissions map[string]string
}
func WithUser(ctx context.Context, user UserData) context.Context {
return context.WithValue(ctx, userContextKey{}, user)
}
func UserFromContext(ctx context.Context) (UserData, bool) {
val := ctx.Value(userContextKey{})
user, ok := val.(UserData)
return user, ok
}
E quais são os benefícios que ganhamos com essa abordagem?
Type Safety
Um dos maiores riscos de armazenar valores diretamente no contexto é a perda da segurança de tipo. O context.Context armazena valores como interface{}, exigindo asserções de tipo para recuperá-los. Se o tipo esperado não corresponder ao tipo armazenado, a aplicação entrará em pânico (panic) em tempo de execução.
Com a abordagem de struct:
- Campos da Struct são Tipados: Dentro de
UserData, cada campo possui um tipo definido, eliminando a necessidade de asserções de tipo para cada valor individualmente. - Verificação em Tempo de Compilação: Qualquer tentativa de acessar um campo inexistente resultará em um erro de compilação, prevenindo panics em produção.
Dev Experience
- Autocompletar da IDE para Campos da Struct: Uma vez que a UserData struct é recuperada, sua IDE oferecerá autocompletion para todos os campos.
- Fácil de Adicionar Novos Campos: Se você precisar adicionar novos dados relacionados ao usuário, basta adicionar um novo campo no
UserData.
Conflitos de Nomenclatura
Ao usar chaves de string para identificar valores no contexto, surge um risco significativo de conflitos com nomes. Em projetos grandes, diferentes partes do código podem utilizar a mesma chave de string para propósitos distintos. A chave userContextKey é um tipo privado e, portanto, é privada ao pacote usercontext, evitando colisões.
Multiple Lookups Vs Única Busca no Contexto
Cada valor armazenado no contexto requer uma busca separada para ser recuperado. Com a abordagem de struct, todos os valores são recuperados em uma única operação, simplificando o código e reduzindo a chance de erros.
Falta de Encapsulamento Vs Encapsulamento por Pacote
A responsabilidade de adicionar e extrair os dados do usuário fica claramente definida e encapsulada dentro do pacote usercontext. Limites de Pacote Claros garantem que todas as operações relacionadas aos dados do usuário no contexto sejam centralizadas e controladas por esse pacote.
Conclusão
O context.Context é uma ferramenta poderosa em Go, essencial para propagar deadlines, cancelamentos e valores. No entanto, o armazenamento de forma desordenada de valores usando chaves de string pode introduzir riscos significativos como panics em tempo de execução.
A adoção de um padrão que utiliza chaves de contexto privadas e tipos estruturados garante a segurança de tipo através de verificações em tempo de compilação, além de oferecer encapsulamento por pacote, evitando colisões e centralizando a lógica de acesso.
