Diretrizes Básicas do Padrão de Codificação Golang
Utilize defer para liberar recursos
Use defer para liberar recursos como arquivos e travas.
Não recomendado:
p.Lock()
if p.count < 10 {
p.Unlock()
return p.count
}
p.count++
newCount := p.count
p.Unlock()
return newCount
// É fácil esquecer de desbloquear quando existem múltiplos ramos de retorno
Recomendado:
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// Mais legível
O custo adicional do defer é extremamente baixo, portanto, só deve ser evitado quando for possível provar que o tempo de execução da função está no nível de nanossegundos. Utilizar defer para melhorar a legibilidade vale a pena, pois o custo de utilizá-los é negligenciável. Isso é especialmente aplicável a métodos maiores que envolvem mais do que simples acesso à memória, onde o consumo de recursos de outros cálculos supera em muito o de defer
.
O tamanho do canal deve ser de 1 ou não bufferizado
Os canais geralmente devem ter um tamanho de 1 ou ser não bufferizados. Por padrão, os canais são não bufferizados com um tamanho de zero. Qualquer outro tamanho deve ser estritamente revisado. Precisamos considerar como determinar o tamanho, considerar o que impede o canal de escrever sob cargas altas e quando bloqueado, e considerar quais mudanças ocorrem na lógica do sistema quando isso acontece.
Não recomendado:
// Deve ser suficiente para lidar com qualquer situação!
c := make(chan int, 64)
Recomendado:
// Tamanho: 1
c := make(chan int, 1) // ou
// Canal não bufferizado, tamanho é 0
c := make(chan int)
Enums começam a partir de 1
O método padrão de introduzir enums em Go é declarar um tipo personalizado e um grupo constante que usa iota. Uma vez que o valor padrão das variáveis é 0, os enums devem começar tipicamente com um valor não zero.
Não recomendado:
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2
Recomendado:
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3
Em alguns casos, usar o valor zero faz sentido (enums que começam a partir de zero), por exemplo, quando o valor zero é o comportamento padrão ideal.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Utilizando atomic
Utilize operações atômicas do pacote sync/atomic para operar em tipos primitivos (int32
, int64
, etc.) porque é fácil esquecer de usar operações atômicas para ler ou modificar variáveis.
go.uber.org/atomic adiciona segurança de tipo a essas operações ao ocultar o tipo subjacente. Além disso, inclui um conveniente tipo atomic.Bool
.
Abordagem não recomendada:
type foo struct {
running int32 // atomic
}
func (f* foo) start() {
if atomic.SwapInt32(&f.running, 1) == 1 {
// já em execução...
return
}
// iniciar o Foo
}
func (f *foo) isRunning() bool {
return f.running == 1 // corrida!
}
Abordagem recomendada:
type foo struct {
running atomic.Bool
}
func (f *foo) start() {
if f.running.Swap(true) {
// já em execução...
return
}
// iniciar o Foo
}
func (f *foo) isRunning() bool {
return f.running.Load()
}
Evite variáveis globais mutáveis
Utilize a abordagem de injeção de dependência para evitar a alteração de variáveis globais. Isso é aplicável tanto para ponteiros de função quanto para outros tipos de valores.
Abordagem não recomendada 1:
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
Abordagem recomendada 1:
// sign.go
type signer struct {
now func() time.Time
}
func newSigner() *signer {
return &signer{
now: time.Now,
}
}
func (s *signer) Sign(msg string) string {
now := s.now()
return signWithTime(msg, now)
}
Abordagem não recomendada 2:
// sign_test.go
func TestSign(t *testing.T) {
oldTimeNow := _timeNow
_timeNow = func() time.Time {
return someFixedTime
}
defer func() { _timeNow = oldTimeNow }()
assert.Equal(t, want, sign(give))
}
Abordagem recomendada 2:
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
Evite usar identificadores pré-declarados
A Especificação da Linguagem Go destaca diversos identificadores pré-declarados que não devem ser utilizados em projetos Go. Esses identificadores pré-declarados não devem ser reutilizados como nomes em diferentes contextos, pois fazer isso ocultará os identificadores originais no escopo atual (ou em qualquer escopo aninhado), potencialmente levando à confusão do código. No melhor dos casos, o compilador emitirá um erro; no pior dos casos, esse código pode introduzir erros potenciais difíceis de recuperar.
Prática não recomendada 1:
var error string
// `error` sombreia implicitamente o identificador integrado
// ou
func handleErrorMessage(error string) {
// `error` sombreia implicitamente o identificador integrado
}
Prática recomendada 1:
var errorMessage string
// `error` agora aponta para o identificador integrado não sombreado
// ou
func handleErrorMessage(msg string) {
// `error` agora aponta para o identificador integrado não sombreado
}
Prática não recomendada 2:
type Foo struct {
// Embora esses campos tecnicamente não sombream, redefinir `error` ou `string` agora se torna ambíguo.
error error
string string
}
func (f Foo) Error() error {
// `error` e `f.error` visualmente parecem similares
return f.error
}
func (f Foo) String() string {
// `string` e `f.string` visualmente parecem similares
return f.string
}
Prática recomendada 2:
type Foo struct {
// `error` e `string` agora são explícitos.
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
Observe que o compilador não gerará erros ao usar os identificadores pré-declarados, mas ferramentas como go vet
apontarão corretamente esses e outros problemas implicitamente relacionados.
Evite usar init()
Tente evitar usar init()
o máximo possível. Quando init()
for inevitável ou preferido, o código deve tentar:
- Garantir completude independentemente do ambiente ou chamada do programa.
- Evitar depender da ordem ou efeitos colaterais de outras funções
init()
. Embora a ordem doinit()
seja explícita, o código pode mudar, portanto, a relação entre as funçõesinit()
pode tornar o código frágil e propenso a erros. - Evitar acessar ou manipular estados globais ou ambientais, como informações da máquina, variáveis de ambiente, diretórios de trabalho, parâmetros/entradas do programa, etc.
- Evitar I/O, incluindo sistemas de arquivos, redes e chamadas de sistema.
O código que não atende a esses requisitos pode pertencer à chamada main()
(ou em outra parte do ciclo de vida do programa) ou ser escrito como parte do próprio main()
. Em particular, as bibliotecas destinadas a serem usadas por outros programas devem prestar atenção especial à completude em vez de realizar "mágica de inicialização".
Abordagem não recomendada 1:
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
Abordagem recomendada 1:
var _defaultFoo = Foo{
// ...
}
// ou, para melhor testabilidade:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
Abordagem não recomendada 2:
type Config struct {
// ...
}
var _config Config
func init() {
// Ruim: baseado no diretório atual
cwd, _ := os.Getwd()
// Ruim: I/O
raw, _ := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
Abordagem recomendada 2:
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// tratar erro
raw, err := os.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// tratar erro
var config Config
yaml.Unmarshal(raw, &config)
return config
}
Dadas as considerações acima, em alguns casos, init()
pode ser mais preferível ou necessário, incluindo:
- Não pode ser representado como uma única atribuição de uma expressão complexa.
- Ganchos inseríveis, como
database/sql
, registros de tipos, etc.
Prefira especificar a capacidade da fatia ao anexar
Sempre dê prioridade à especificação de um valor de capacidade para make()
ao inicializar uma fatia a ser anexada.
Abordagem não recomendada:
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Abordagem recomendada:
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
Usando Tags de Campo na Serialização de Struct
Ao serializar para JSON, YAML, ou qualquer outro formato que suporte a nomeação de campos com base em tags, as tags relevantes devem ser usadas para anotação.
Não Recomendado:
type Stock struct {
Price int
Name string
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
Recomendado:
type Stock struct {
Price int `json:"price"`
Name string `json:"name"`
// Seguro renomear Name para Symbol.
}
bytes, err := json.Marshal(Stock{
Price: 137,
Name: "UBER",
})
Em teoria, o formato de serialização de uma estrutura é um contrato entre diferentes sistemas. Fazer alterações no formato de serialização da estrutura (incluindo nomes de campos) quebrará esse contrato. Especificar os nomes dos campos em tags torna o contrato explícito e também ajuda a evitar violações acidentais do contrato por meio de refatoração ou renomeação de campos.