Boas práticas em GO: como organizar códigos e projetos em Go

Como organizar o código Go de forma clara, eficiente e fácil de manter? Neste artigo, vamos conhecer as armadilhas comuns e como evitá-las, além das boas práticas para estruturar projetos e usar recursos como interfaces, funções init e pacotes utilitários.
Muitas pessoas programam em Go de forma ineficiente porque aplicam padrões de outras linguagens sem considerar as particularidades do Go.
É comum que pessoas desenvolvedoras de linguagens orientadas a objetos exagerem no uso de interfaces e abstrações desnecessárias, deixando o código mais complexo do que deveria.
Outra armadilha comum é não seguir a organização idiomática de pacotes, misturando responsabilidades ou criando uma estrutura excessivamente fragmentada, dificultando a manutenção e a escalabilidade do projeto.
Continue lendo o artigo para descobrir como evitar os erros mais comuns e aprender a adotar práticas que vão tornar seu desenvolvimento mais ágil e melhorar seu código Go.
Sombreamento de variáveis
Declarar uma variável com o mesmo nome em um bloco interno esconde a variável do bloco externo. Isso pode causar confusão e bugs difíceis de encontrar.
nome := "João" // Nome externo
{
nome := "Maria" // Nome interno, sombreia o externo
fmt.Println(nome) // Imprime "Maria"
}
fmt.Println(nome) // Imprime "João" - o nome externo não foi alterado
Solução: Use nomes diferentes para variáveis em blocos internos.
package main
import "fmt"
func main() {
nomeExterno := "João" // Nome externo
{
nomeInterno := "Maria" // Nome interno, não sombreia o externo
fmt.Println(nomeInterno) // Imprime "Maria"
}
fmt.Println(nomeExterno) // Imprime "João" - o nome externo não foi alterado
}

Código aninhado em excesso
Muitos níveis de if/else dificultam a leitura e compreensão do código.
func process(n int) {
if n > 0 {
if n%2 == 0 {
fmt.Println("Número positivo e par")
} else {
fmt.Println("Número positivo e ímpar")
}
} else {
fmt.Println("Número não é positivo")
}
}
Solução: Simplifique as condições, retorne antecipadamente, quando possível, e alinhe o caminho principal (o código que executa na maioria das vezes) à esquerda, como no exemplo abaixo:
func process(n int) {
if n <= 0 {
fmt.Println("Número não é positivo")
return
}
if n%2 == 0 {
fmt.Println("Número positivo e par")
return
}
fmt.Println("Número positivo e ímpar")
}
Mau uso de funções init
Funções init são executadas automaticamente ao iniciar um pacote. Evite usá-las para tarefas complexas ou que possam falhar, pois o tratamento de erros é limitado.
var config Config
func init() {
config = loadConfig() // Pode falhar e não tem tratamento de erro adequado
}
Solução: Prefira funções normais para inicializações.
var config Config
func initialize() error {
var err error
config, err = loadConfig()
return err
}
Excesso de getters e setters
Em Go, não é idiomático usar getters e setters para todos os campos de uma struct, como no exemplo abaixo:
type User struct {
name string
}
func (u *User) GetName() string {
return u.name
}
func (u *User) SetName(name string) {
u.name = name
}
Solução: Acesse os campos diretamente, a menos que haja uma lógica específica para encapsular.
type User struct {
Name string // Campos exportados (maiúscula inicial) são acessíveis externamente
}
Poluição de interfaces
Criar interfaces desnecessárias torna o código mais complexo. Interfaces devem ser descobertas conforme a necessidade, e não criadas antecipadamente.
type Database interface {
Save(data string)
}
type SQLDatabase struct{}
func (s SQLDatabase) Save(data string) {
fmt.Println("Salvando no banco")
}
Solução: Prefira interfaces pequenas e com propósito claro.
type SQLDatabase struct{}
func (s SQLDatabase) Save(data string) {
fmt.Println("Salvando no banco")
}
Interfaces no lado do produtor
Quando a interface é definida no pacote produtor, qualquer mudança na interface pode exigir alterações em todos os pacotes que a utilizam. Isso cria um acoplamento desnecessário entre o produtor e os consumidores.
package storage
type Storage interface {
Save(data string)
}
Solução: Defina interfaces no pacote que as utiliza (consumidor), não no pacote que implementa os tipos concretos (produtor). Isso dá mais flexibilidade ao consumidor.
package service
type Storage interface {
Save(data string)
}
any
não diz nada
Em Go, any
é um alias para interface{}
e representa qualquer tipo.
Ele foi introduzido no Go 1.18 para tornar o código mais legível e intuitivo, substituindo o uso explícito de interface{}
para indicar valores de qualquer tipo.
Entretanto, any
pode receber qualquer tipo, mas isso torna o código menos expressivo.
func PrintValue(value any) {
fmt.Println(value)
}
Solução: Use tipos concretos sempre que possível.
func PrintValue(value string) {
fmt.Println(value)
}
Genéricos
Genéricos permitem escrever código que funciona com diferentes tipos. Use-os com cautela, apenas quando necessário para evitar repetição de código com tipos diferentes.
func Print[T any](value T) {
fmt.Println(value)
}
Solução: Não use genéricos quando tornar o código mais complexo.
package main
import "fmt"
func PrintInt(value int) {
fmt.Println(value)
}
func PrintString(value string) {
fmt.Println(value)
}
func main() {
PrintInt(42)
PrintString("Olá, mundo!")
}
Desorganização do projeto
Organize o projeto em pacotes com nomes claros e concisos.
package main
type User struct{}
func saveUser() {}
func main() { ………
Evite pacotes muito pequenos ou muito grandes. Separe código público de código privado.
/project
/cmd
/internal
/pkg
/service
/repository
Ignorar colisões de nomes de pacotes
Evite nomes de variáveis que colidam com nomes de pacotes.
package utils
func ConvertToUpper(s string) string {
return strings.ToUpper(s)
}
Solução: Use nomes diferentes ou aliases para pacotes.
package stringutil
func ToUpper(s string) string {
return strings.ToUpper(s)
}
Não usar linters
Linters são ferramentas que analisam o código em busca de erros e problemas de estilo.
go run main.go
Solução: Use linters para garantir a qualidade do código.
golangci-lint run
Conclusão
Escreva código Go simples, claro e bem organizado. Evite abstrações desnecessárias e use as ferramentas disponíveis para garantir a qualidade do código.
Lembre-se que a consistência é fundamental para facilitar a manutenção do projeto.
Quer aprofundar ainda nessa linguagem? Aprenda a programar em Go com boas práticas na Alura! Conheça as formações Linguagem Go e Aprenda a programar em Go.