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:

  1. Garantir completude independentemente do ambiente ou chamada do programa.
  2. Evitar depender da ordem ou efeitos colaterais de outras funções init(). Embora a ordem do init() seja explícita, o código pode mudar, portanto, a relação entre as funções init() pode tornar o código frágil e propenso a erros.
  3. 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.
  4. 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.