Categoria: Go

  • Dicas para organização de projetos


    A estruturação de projetos Go pode representar um desafio, especialmente devido à ausência de padrões rígidos ou convenções oficiais. Essa flexibilidade proporciona autonomia ao desenvolvedor, mas também exige atenção e cuidado para garantir clareza e manutenção eficiente do código.

    Este artigo apresenta diretrizes amplamente reconhecidas pela comunidade Go, com o objetivo de auxiliar na organização de projetos.


    Project-layout

    Um dos modelos mais adotados é o proposto pelo repositório https://github.com/golang-standards/project-layout, que sugere uma estrutura de diretórios em alto nível, baseada em práticas consolidadas de projetos reais.

    Alguns dos diretórios recomendados são:

    • /cmd – Contém os arquivos principais da aplicação, como main.go. Cada subdiretório deve corresponder ao nome do executável. A função main deve ser concisa, delegando responsabilidades para pacotes internos.
    • /internal – Abriga o código privado da aplicação, não acessível por outros projetos. A partir do Go 1.4, o compilador restringe a importação de pacotes fora da árvore de diretórios.
    • /pkg – Reúne código que pode ser reutilizado por aplicações externas e por isso o código presente deve ser bem consistente para evitar erros em quem utiliza.
    • /api – Armazena documentação e definições de APIs, como arquivos Swagger, esquemas JSON e protocolos.
    • /web – Contém recursos para aplicações web, como arquivos estáticos e templates.
    • /configs – Inclui arquivos de configuração, como parâmetros de servidor, banco de dados e chaves de API.
    • /build – Define instruções de compilação e implantação, incluindo Dockerfiles e configurações de integração contínua.
    • /tools – Reúne ferramentas auxiliares utilizadas no projeto.
    • /test – Contém dados e funções de apoio para testes de integração. Os testes unitários devem permanecer no mesmo pacote das funções testadas.


    ⚠️ Recomenda-se evitar o uso do diretório /src, por tratar-se de uma convenção oriunda da linguagem Java, não alinhada às práticas do Go.


    Organização de pacotes

    A linguagem Go não adota o conceito de subpacotes. Dessa forma, a organização dos pacotes deve ser orientada à clareza e à funcionalidade, facilitando a compreensão por outros desenvolvedores. Por exemplo:

    shopapp/

    ├── go.mod
    ├── main.go

    ├── internal/
    │ ├── user/
    │ │ ├── model.go
    │ │ └── service.go
    │ │
    │ ├── order/
    │ │ ├── model.go
    │ │ ├── service.go
    │ │ └── validation/
    │ │ └── validation.go


    Recomendações

    • Evite a criação excessiva de pacotes nas fases iniciais do projeto. Uma estrutura simples e contextual tende a ser mais eficaz.
    • Reduza ao máximo a exposição de tipos e funções exportáveis. Essa prática minimiza o acoplamento entre pacotes e facilita futuras refatorações.
    • Em caso de dúvida sobre a necessidade de exportação de um elemento, opte por mantê-lo privado.
    • Nomeie os pacotes com base no que eles oferecem, e não apenas no conteúdo que armazenam. Isso contribui para uma nomenclatura mais intuitiva, sempre lembrando que o nome do pacote é utilizado no uso de um elemento exportado, como:
    import (
      "net/http"
      "github.com/gin-gonic/gin"
    )
    
    func main() {
      r := gin.Default()
      // ...
    }
    


    Para saber mais

    https://go.dev/doc/modules/layout

    https://medium.com/golang-learn/go-project-layout-e5213cdcfaa2

    https://blog.sgmansfield.com/2016/01/an-analysis-of-the-top-1000-go-repositories

    https://travisjeffery.com/b/2019/11/i-ll-take-pkg-over-internal

    Referência: HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

  • Lidando com parâmetros opcionais

    Parâmetros opcionais são argumentos que não precisam ser fornecidos ao chamar uma função. Eles possuem valores padrão que são utilizados quando nenhum valor é passado, trazendo flexibilidade ao código.

    Diferente de linguagens como Python e PHP, o Go não oferece suporte nativo a parâmetros opcionais.

    Para se obter esse comportamento no Go, apresento três soluções.


    1. Struct de parâmetros

    A primeira abordagem é utilizar structs para encapsular os parâmetros opcionais, enquanto os obrigatórios permanecem na assinatura da função:

    type Config struct {
        Port int
    }
    
    func NewServer(addr string, cfg Config) {
    }
    

    Essa técnica facilita a adição de novos parâmetros sem quebrar a compatibilidade com chamadas existentes.

    Entretanto, é importante lembrar que quando criamos uma struct sem passar valores para seus campos, eles são inicializados com seus zero values:

    • 0 para inteiros
    • 0.0 para floats
    • "" para strings
    • nil para slices, maps, channels, ponteiros, interfaces e funções

    Se for necessário distinguir entre um valor 0 em um inteiro passado pelo cliente e o zero value do tipo, pode-se usar ponteiros, pois seu zero value será nil.

    type Config struct {
        Port *int
    }
    

    Apesar de funcional, essa abordagem exige a criação explícita de variáveis para referência:

    port := 0
    config := httplib.Config{
        Port: &port,
    }
    

    Outro ponto de atenção ao ponteiro é que se ele não for bem tratado existe o risco do programa gerar um panic por nil pointer exception.

    Outro ponto negativo de utilizar uma struct como parâmetro opcional é que, se esse parâmetro não for utilizado, ainda será necessário passar uma struct vazia na chamada da função:

    httplib.NewServer("localhost", httplib.Config{})
    


    11. Builder

    O padrão de projeto Builder delega a criação e validação da struct Config para uma struct intermediária ConfigBuilder, que expõe métodos para configurar os campos:

    type ConfigBuilder struct {
        port *int
    }
    
    func (b *ConfigBuilder) Port(port int) *ConfigBuilder {
        b.port = &port
        return b
    }
    
    func (b *ConfigBuilder) Build() (Config, error) {
        // lógica de validação e preenchimento
    }
    

    Um exemplo de uso pelo cliente seria:

    builder := httplib.ConfigBuilder{}
    builder.Port(8080)
    
    cfg, err := builder.Build()
    if err != nil {
        return err
    }
    
    server, err := httplib.NewServer("localhost", cfg)
    if err != nil {
        return err
    }
    

    Essa abordagem também resolve o problema de compatibilidade e facilita validações complexas, mas adiciona uma camada extra de abstração.


    111. Function Optional Pattern

    O Function Optional Pattern é um padrão onde uma função principal recebe uma lista de funções opcionais como argumento.

    Essas funções extras são usadas para modificar ou estender o comportamento da função principal, mas sem serem obrigatórias.

    Se forem passadas, elas são executadas em determinado ponto do código; caso contrário, a função segue com o comportamento padrão.

    No Go, esse padrão pode ser aplicado com o uso de parâmetros variádicos, que permitem que uma função receba zero ou mais argumentos de um mesmo tipo.

    Um parâmetro variádico é declarado com ... antes do tipo e os argumentos são tratados como um slice dentro da função.

    func sum(numbers ...int) int {
        sum := 0
        for _, n := range numbers {
            sum += n
        }
        return sum
    }
    


    Passo a passo para usar o padrão

    1. Criar uma struct interna de configuração

    type options struct {
        port *int
    }
    


    2. Definir um tipo de função que recebe um ponteiro para a struct criada

    type Option func(*options) error
    


    3. Criar funções públicas que retornam o tipo definido

    Essas funções alterarão os campos da struct options.

    Esse padrão permite criar quantas funções WithX forem necessárias (WithTimeout, WithTLS, etc.), mantendo a função principal limpa.

    func WithPort(port int) Option {
        return func(o *options) error {
            if port < 0 {
                return errors.New("port should be positive")
            }
            o.port = &port
            return nil
        }
    }
    


    4. Definir a função principal com parâmetros variádicos

    func NewServer(addr string, opts ...Option) (*http.Server, error)
    


    5. Processar a lista de funções opcionais dentro da função

    func NewServer(addr string, opts ...Option) (*http.Server, error) {
        var o options
        for _, opt := range opts {
            if err := opt(&o); err != nil {
                return nil, err
            }
        }
    
        // ...
    }
    



    Com essa solução podemos chamar a função NewServer somente com o parâmetro obrigatório addr:

    server, err := httplib.NewServer("localhost")
    

    Ou com vários parâmetros opcionais:

    server, err := httplib.NewServer("localhost",
    httplib.WithPort(8080),
    httplib.WithTimeout(time.Second))
    


    Embora o Go não ofereça suporte nativo a parâmetros opcionais, existem padrões eficazes para contornar essa limitação, como o uso de structs, builders e funções variádicas. A escolha da abordagem ideal depende da complexidade da configuração, da necessidade de validação e da escalabilidade do código.


    Referência: HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

  • Atenção ao usar embedded types

    Ao criar structs em Go, é possível incorporar outras structs para compor seus campos. Essa técnica permite reutilizar structs e promover seus métodos e atributos, mas exige cuidado.

    Para incorporar uma struct, basta declará-la como campo de outra struct sem nomear o tipo, como no exemplo abaixo:

    type Foo struct {
        Bar
    }
    
    type Bar struct {
        Baz int
    }
    

    Nesse caso, Bar é um embedded type de Foo. Com isso, os campos de Bar podem ser acessados diretamente por Foo de duas formas:

    1. Direta: Foo.Baz = 45
    2. Indireta: Foo.Bar.Baz = 51

    A técnica também se aplica a interfaces. Veja o exemplo do pacote io, nativo do Go:

    type ReadWriter interface {
        Reader
        Writer
    }
    

    Aqui, ReadWriter é uma composição das interfaces Reader e Writer. Para implementá-la, uma struct precisa implementar os métodos de ambas:

    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    
    type Writer interface {
        Write(p []byte) (n int, err error)
    }
    

    Com isso, qualquer tipo que implemente ReadWriter pode ser usado onde se espera um Reader ou Writer.


    O ponto de atenção e cuidado

    Ao incorporar uma struct, seus métodos são promovidos para o tipo que a recebe. Isso pode causar confusão ou permitir acessos indesejados.

    Veja o exemplo abaixo, que representa um banco de dados em memória com controle de concorrência via sync.Mutex:

    type InMem struct {
        sync.Mutex
        m map[string]string
    }
    
    func New() *InMem {
        return &InMem{m: make(map[string]string)}
    }
    
    func (i *InMem) Get(key string) (string, bool) {
        i.Lock()
        v, ok := i.m[key]
        i.Unlock()
        return v, ok
    }
    

    Até aqui, tudo certo. Mas como sync.Mutex é um embedded type, o seguinte código também é válido:

    m := inmem.New()
    m.Lock()
    

    Isso permite que o mutex seja acessado diretamente de fora da struct, o que não faz sentido para o encapsulamento desejado.

    Para evitar esse tipo de acesso, declare o campo como privado, sem incorporação:

    type InMem struct {
        mu sync.Mutex // Mutex não incorporado
        m map[string]string
    }
    

    Assim, o mutex só pode ser acessado dentro dos métodos da própria struct.


    Boas práticas

    • Evite promover campos e métodos que não devem ser acessados externamente.
    • Não use embedded types apenas por estética ou para simplificar o acesso.
    • Em structs públicas, a incorporação pode gerar complexidade futura: novos campos ou métodos na struct interna podem quebrar regras da externa.
    • Sempre avalie se há uma solução melhor sem usar embedded types.

    Em geral, o uso de embedded types é raro e, quando necessário, deve ser bem justificado. Na maioria dos casos, seu uso é apenas por conveniência.


    Referência: HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

  • Descomplicando generics

    Generics são um recurso que permite escrever funções e estruturas de dados que funcionam com qualquer tipo, garantindo reutilização de código e segurança de tipos em tempo de compilação.

    Esse recurso difere do uso dos tipos any/interface{} em Go, que aceitam valores de qualquer tipo, mas sem validação em tempo de compilação. Nesse caso, a verificação ocorre apenas em tempo de execução, o que aumenta o risco de erros inesperados durante a execução do programa.

    Para mais informações: https://arturbaccarin.dev.br/any-interface-e-qualquer-coisa-e-esse-e-o-problema/.

    Generics foram adicionados na versão 1.18.

    Para exemplificar, vamos criar uma função que recebe um map como parâmetro e retorna um slice contendo suas chaves:


    Porém, o tipo da chave está fixado como string. Se precisarmos realizar a mesma operação para um map[int], uma das opções não recomendadas seria duplicar o código e utilizar any nos parâmetros de entrada e saída.


    Essa solução escala mal, pois, se for necessário adicionar mais um tipo, será preciso duplicar ainda mais o código do laço for.

    Outro ponto de falha é o uso de any, que elimina um dos principais benefícios do Go: ser uma linguagem fortemente tipada. Nesse caso, a função pode ser chamada com qualquer tipo de variável e eventuais erros só serão detectados em tempo de execução, quando a função retornar um resultado inesperado.

    Por não conhecermos o tipo do parâmetro de entrada, somos obrigados a retornar um slice de any, que terá que ser tratado pelo código que chama a função. Isso pode causar erros e aumentar o custo de processamento, devido à necessidade de realizar type assertion, ou seja, extrair o valor do any e convertê-lo para seu tipo original.

    Esse problema é resolvido com os generics. Na sua estrutura, é necessário definir um parâmetro de tipo junto à declaração do nome da função, como visto abaixo na função Filter.


    Agora, vamos refatorar nosso código que extrai as chaves de um map.

    O primeiro ponto de atenção é que não é possível criar um mapa com chaves de qualquer tipo. É obrigatório que o tipo da chave seja comparável, ou seja, que suporte as operações == e !=.

    Por esse motivo, utilizamos o tipo comparable na refatoração, caso contrário teremos o erro invalid map key type K (missing comparable constraint).


    Outra possibilidade é criar restrições de tipo para os generics, como mostrado no exemplo abaixo.


    Se tentarmos utilizar a função com um map[float64]string, por exemplo, o erro float64 does not satisfy typeConstraint (float64 missing in int | ~string) será exibido durante a compilação.

    Observando a typeConstraint, foi definido ~string em vez de apenas string. O til (~) indica que, nessa restrição, é possível utilizar tipos personalizados baseados em string, como mostrado a seguir. Já o int sem o til permite apenas variáveis do tipo int.

    Se o til for removido, ocorrerá o erro Foo does not satisfy typeConstraint (possibly missing ~ for string in typeConstraint) em tempo de compilação.

    Outra utilização dos generics é com structs, especialmente quando é necessário criar estruturas de dados como filas, pilhas ou até árvores. No exemplo abaixo, é criada uma pilha genérica:

    Por fim, é importante destacar que os generics não podem ser utilizados diretamente em métodos de structs, como mostrado a seguir. Caso isso ocorra, o compilador emitirá o erro method must have no type parameters.


    Embora os generics possam ser extremamente úteis em situações específicas, é essencial usá-los com cautela. Em muitos casos, a decisão de não utilizá-los se assemelha à decisão de não usar interfaces, pois ambas introduzem um nível de abstração que, se for desnecessário, pode tornar o código mais difícil de entender e manter.

    Generics são uma ferramenta poderosa, mas, como toda abstração, envolvem um custo. Se forem utilizados de forma prematura ou sem uma justificativa clara, podem adicionar complexidade sem trazer benefícios reais. Em vez de recorrer a parâmetros de tipo logo no início, devemos primeiro focar em resolver os problemas concretos de forma simples e direta.

    Como regra: só considere o uso de generics quando perceber repetição de código entre diferentes tipos. Até lá, mantenha seu código simples, claro e voltado às necessidades reais do projeto.


    Referência: HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

  • Any/interface{} é qualquer coisa e esse é o problema

    Em Go, interface{} representa uma interface vazia. Isso significa que ela não define nenhum método e, por isso, qualquer tipo de dado em Go a implementa automaticamente. Em outras palavras, ela pode armazenar qualquer tipo e valor.

    Esse recurso é útil quando não se sabe, antecipadamente, qual tipo será utilizado como em situações que envolvem deserialização de JSON ou comunicação genérica entre componentes.

    No Go 1.18, foi introduzido o tipo any, que nada mais é do que um alias para interface{}.

    Observação: A partir deste ponto do texto, usarei apenas any, então lembre-se: any e interface{} são equivalentes.

    Na maioria dos casos, o uso do any é considerado uma generalização excessiva (overgeneralization).

    Ao utilizá-lo, perde-se a verificação de tipos em tempo de compilação. Isso significa que, para acessar o valor armazenado em um any, geralmente é necessário realizar uma type assertion. Ou seja, é preciso checar se o valor dentro do any é realmente do tipo esperado, o que pode tornar o código mais complexo e propenso a erros.

    A type assertion, que consiste em extrair o valor de dentro de um any e convertê-lo de volta para seu tipo original, pode também gerar sobrecarga de desempenho, especialmente se for usada com frequência no código.

    O any também pode prejudicar a legibilidade e a manutenibilidade do código, já que não se sabe, com clareza, qual tipo de dado está sendo manipulado.

    No exemplo abaixo, não há nenhum erro em nível de compilação no código; no entanto, futuros desenvolvedores que precisarem utilizar a struct Store terão que ler a documentação ou se aprofundar no código para entender como utilizar seus métodos.

    Retornar valores do tipo any também pode ser prejudicial, pois não há nenhuma garantia, em tempo de compilação, de que o valor retornado pelo método será utilizado de forma correta e segura, o que pode, inclusive, levar a um panic durante a execução.

    Ao utilizar any, perdemos os benefícios do Go ser uma linguagem estaticamente tipada, como a verificação de tipo em tempo de compilação, a detecção precoce de erros e a otimização de desempenho, já que o compilador não precisa realizar verificações de tipo em tempo de execução.

    Em vez de utilizar any nas assinaturas dos métodos, recomenda-se criar métodos específicos, mesmo que isso resulte em alguma duplicação, como múltiplas versões de métodos Get e Set.

    Ter mais métodos não é necessariamente um problema, pois os clientes que os utilizarão podem criar suas próprias abstrações por meio de interfaces.

    Lembrando que o ideal é que as interfaces sejam definidas pelo consumidor, como explicado em: https://arturbaccarin.dev.br/interface-e-coisa-de-consumidor/.


    Mas o uso de any é sempre ruim? A resposta é: não.

    Na programação, raramente existe uma regra sem exceções.

    Como mencionado no início do texto, há situações em que o uso de any é justificado, especialmente quando não é possível conhecer antecipadamente o tipo dos dados com os quais se estará lidando, como em:

    Serialização e deserialização de JSON quando não se conhece o formato exato do JSON em tempo de compilação.


    Cache genérico para armazenar qualquer tipo de valor.


    Logger genérico que aceita qualquer tipo de valor.


    Embora o uso de any ofereça flexibilidade, ele deve ser evitado sempre que for possível utilizar tipos específicos. Fora de casos bem específicos, prefira manter a tipagem forte do Go para garantir segurança, legibilidade e manutenibilidade do código.


    Referência: HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

  • Interface é coisa de consumidor

    Interfaces oferecem uma forma de especificar o comportamento de um objeto, mesmo que ele ainda não exista.

    Elas podem ser utilizadas quando há necessidade de replicar comportamentos, promover o desacoplamento entre componentes ou restringir certas funcionalidades.

    Mas surge a pergunta: onde devemos criá-las?

    A resposta, na maioria dos casos, é: do lado de quem vai utilizá-las (lado do consumidor).

    É comum vermos interfaces sendo definidas junto com suas implementações concretas (lado do produtor), mas essa prática não é a mais recomendada em Go. Isso porque ela pode forçar o consumidor a depender de métodos que não precisa, dificultando a flexibilidade e o reuso.

    É o consumidor quem deve decidir se precisa de uma abstração e qual deve ser sua forma.

    Essa abordagem segue o Princípio de Segregação de Interface (Interface Segregation Principle, a letra “I” do SOLID), que estabelece que um cliente não deve ser forçado a depender de métodos que não utiliza.

    Portanto, a melhor prática é: o produtor fornece implementações concretas e o consumidor decide como usá-las, inclusive se deseja abstraí-las por meio de interfaces.


    Exemplo

    Imagine um sistema em que precisamos gerar um recibo de pedido e para isso precisamos buscar os nomes do cliente e do produto com base em seus respectivos IDs.

    Cenário 1 – Prefira: Os produtores fornecem a implementação concreta para acesso a esses dados e o consumidor define as interfaces com os métodos que ele utilizará para gerar o recibo.


    Cenário 2 – Evite: As interfaces são criadas no lado dos produtores e o pacote receipt acaba tendo acesso a métodos que não precisa, o que gera acoplamento desnecessário e fere o princípio de segregação de interface.


    Os códigos apresentados estão disponíveis em: https://github.com/arturbaccarin/foi-o-go-que-me-deu/tree/main/interface-e-coisa-de-consumidor


    Ponto de exceção

    Em alguns contextos específicos, pode fazer sentido definir a interface no lado do produtor. Nesses casos, o ideal é mantê-la o mais enxuta possível, para favorecer a reutilização e facilitar sua composição com outras interfaces.

    Por exemplo: um pacote que fornece um cliente HTTP para consumo de uma API externa, como um serviço de pagamentos. Esse pacote pode expor uma interface com métodos como CreateCharge, RefundPayment e GetTransactionStatus, permitindo que qualquer sistema que consuma o pacote saiba exatamente quais operações são suportadas.

    Como o próprio pacote é o especialista no domínio da API externa, ele pode definir uma interface enxuta e bem estruturada, facilitando tanto o uso quanto a substituição por implementações mockadas.


    Leitura complementar

    As abstrações devem ser descobertas e não criadas. Isso significa que não se deve criar abstrações se não há uma real necessidade no momento. Criam-se interfaces quando necessário e não quando se prevê que elas serão utilizadas.

    Quando se criam muitas interfaces, o fluxo do código fica mais complexo, dificultando a leitura e o entendimento do sistema.

    Se não existe uma razão para definir uma interface, e é incerto que ela melhorará o código, devemos questionar o motivo da existência dela e por que não simplesmente chamar a implementação concreta.

    É comum que desenvolvedores exagerem (overengineer), tentando adivinhar qual seria o nível perfeito de abstração com base no que acreditam que será necessário no futuro. Porém, isso deve ser evitado, pois, na maioria dos casos, polui o código com abstrações desnecessárias.

    Referência: HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

  • Go com clareza e estilo Google

    Uma das perguntas mais frequentes sobre Go é se existe algum guia de estilo para a escrita de código, como a nomenclatura de variáveis, funções e pacotes, semelhante ao PEP 8 do Python.

    Essa dúvida, inclusive, está presente na página de Frequently Asked Questions (FAQ) da documentação oficial da linguagem, na seguinte pergunta:

    Is there a Go programming style guide?

    E a resposta é:

    There is no explicit style guide, although there is certainly a recognizable “Go style”. (Não existe um guia de estilo explícito, embora haja certamente um “estilo Go” reconhecível.)

    O que existe são convenções para orientar sobre nomenclatura, layout e organização de arquivos.

    O gofmt, ferramenta oficial da linguagem de programação Go, formata automaticamente o código-fonte de acordo com o estilo padrão da linguagem. Embora não seja obrigatório, esse estilo é amplamente adotado pela comunidade como o jeito certo de escrever código Go. O gofmt pode ser integrado a IDEs como o Visual Studio Code, permitindo que a formatação seja aplicada automaticamente sempre que o arquivo for salvo.

    Existem também documentos que contêm conselhos sobre esses tópicos, como:

    O guia mais completo que encontrei até hoje é o Go Style Decisions, publicado pelo Google (vale lembrar que a linguagem Go surgiu dentro da própria empresa). Esse guia é, inclusive, referenciado na documentação oficial do Go, na página Go Code Review Comments.

    Considero esse documento minha principal referência quando o assunto é estilo, e abaixo destaco os pontos mais relevantes sobre nomenclatura, segundo a minha visão pessoal:


    Underscores _

    No código Go, utiliza-se CamelCase em vez de underscores, como no snake_case, para nomes compostos por várias palavras dos identificadores.

    Nota: Os nomes dos arquivos de código-fonte não são identificadores Go e, portanto, não precisam seguir as mesmas convenções de nomenclatura. Eles podem conter underscores, como, por exemplo, my_api_handlers.go (isso é até o recomendado para esses casos).


    Pacotes

    Os nomes dos pacotes devem ser curtos e conter somente letras minúsculas.

    Se forem compostos por múltiplas palavras, elas devem ser escritas juntas, sem separação ou caracteres especiais.


    Se for necessário importar um pacote cujo nome contenha underscore (geralmente código de terceiros), ele deve ser renomeado durante a importação para um nome mais adequado seguindo as boas práticas.


    Seja consistente usando sempre o mesmo nome para o mesmo pacote importado em diferentes arquivos.

    Evite usar nomes que também possam ser utilizados por variáveis, a fim de prevenir sombreamento no código.


    Exceção a regra:

    Quando for necessário testar um pacote como um usuário externo, ou seja, testando apenas sua interface pública, é preciso adicionar o sufixo *_test ao nome do pacote nos arquivos de teste.

    Isso força a importação explícita do pacote, permitindo a realização do chamado teste de caixa-preta (black box testing).

    Para saber mais, acesse: https://pkg.go.dev/testing.


    Receptores (receivers)

    Eles devem ser curtos, sendo uma ou duas letras do próprio nome, e usados de forma igual para todos os métodos daquele tipo.

    PrefiraEvite
    func (t Tray)func (tray Tray)
    func (ri *ResearchInfo)func (info *ResearchInfo)
    func (w *ReportWriter)func (this *ReportWriter)
    func (s *Scanner)func (self *Scanner)


    Constantes

    Deve ser utilizado CamelCase, assim como os outros nomes.

    Constantes exportadas começam com letra maiúscula, enquanto constantes não exportadas começam com letra minúscula.

    Isso se aplica mesmo que contrarie convenções de outras linguagens, como em Java, onde as constantes são geralmente todas maiúsculas e com underscore. Por exemplo, MAX_RETRIES se tornaria MaxRetries em Go.

    Além disso, não é necessário começar o nome com a letra “k”.


    Siglas

    Palavras em nomes que são siglas ou acrônimos (ex: URL e OTAN) devem manter a capitalização (todo maiúsculo ou tudo minúsculo).

    Em nomes com múltiplas siglas (ex: XMLAPI, pois contém XML e API), cada letra de uma mesma sigla deve ter a mesma capitalização, mas as siglas diferentes podem usar capitalizações distintas.

    Se a sigla contiver letras minúsculas (ex: DDoS, iOS, gRPC), ela deve manter a forma original, a menos que precise mudar a primeira letra para exportação (como em linguagens de programação).

    NomeEscopoPrefiraEvite
    XML APIExportadaXMLAPIXmlApi, XMLApi, XmlAPI, XMLapi
    XML APINão exportadaxmlAPIxmlapi, xmlApi
    iOSExportadaIOSIos, IoS
    iOSNão exportadaiOSios
    gRPCExportadaGRPCGrpc
    gRPCNão exportadagRPCgrpc
    DDoSExportadaDDoSDDOS, Ddos
    DDoSNão exportadaddosdDos, dDOS
    IDExportadaIDID
    IDNão exportadaidiD
    DBExportadaDBDb
    DBNão exportadadbdB


    Getters

    Nomes de funções e métodos não devem utilizar o prefixo “Get” ou “get”, a menos que o conceito envolva a palavra “get” de forma natural (como em uma requisição HTTP GET).

    Prefira começar o nome diretamente com o substantivo. Por exemplo, use Counts em vez de GetCounts.

    Se a função envolve um cálculo complexo ou a execução de uma chamada remota, é recomendável usar palavras como Compute (calcular) ou Fetch (buscar), em vez de Get, para deixar claro ao desenvolvedor que a execução da função pode demorar, bloquear ou falhar.


    Repetição

    Um código deve evitar repetições desnecessárias, que podem ocorrer de diferentes formas, especialmente na nomeação de pacotes, variáveis, constantes ou funções exportadas.

    Nas funções, não repita o nome do pacote:


    Mais exemplos:

    PrefiraEvite
    widget.Newwidget.NewWidget
    widget.NewWithNamewidget.NewWidgetWithName
    db.Loaddb.LoadFromDatabase
    gtutil.CountGoatsTeleported
    goatteleport.Count
    goatteleportutil.CountGoatsTeleported
    mtpb.MyTeamMethodRequest
    myteampb.MethodRequest
    myteampb.MyTeamMethodRequest


    Nos métodos, não repita o nome do receptor:


    Não repita o nome das variáveis passadas por parâmetros:


    Não repita os nomes e tipos dos valores de retorno:


    Quando for necessário remover a ambiguidade de funções de nomes similares, é aceitável incluir informações adicionais.


    Nome da variáveis VS tipo

    O compilador sempre conhece o tipo de uma variável, e na maioria dos casos, o tipo também é claro para o desenvolvedor com base em como a variável é utilizada.

    Só é necessário especificar o tipo de uma variável quando seu valor aparecer duas vezes no mesmo escopo.


    Se o valor aparece em múltiplas formas, isso pode ser esclarecido com o uso de uma palavra adicional, como raw (bruto) e parsed (processado).


    Contexto externo VS nomes locais

    Nomes que incluem informações já presentes no contexto ao redor geralmente adicionam ruído desnecessário, sem agregar benefícios.

    O nome do pacote, do método, do tipo, da função, do caminho de importação e até mesmo o nome do arquivo já fornecem contexto suficiente para qualificar automaticamente todos os nomes dentro deles.


    Leitura complementar

    Qual seria um tamanho bom de um nome de variável, constantes e função?

    A regra é que o tamanho do nome deve ser proporcional ao escopo dele e inversamente proporcional ao número de vezes que ele é usado dentro desse escopo.

    Uma variável com escopo de arquivo pode precisar de várias palavras, enquanto uma variável dentro de um bloco interno (como um IF ou um FOR) pode ser um nome curto ou até uma única letra, para manter o código claro e evitar informações desnecessárias.

    Aqui está uma orientação (não é regra) do que seria o tamanho de cada escopo em linhas.

    • Escopo pequeno: uma ou duas pequenas operações, entre 1 e 7 linhas.
    • Escopo médio: algumas pequenas operações ou uma grande operação, entre 8 e 15 linhas.
    • Escopo grande: uma ou algumas operações grandes, entre 15 e 25 linhas.
    • Escopo muito grande: várias operações que podem envolver diferentes responsabilidades, mais de 25 linhas.

    Um nome que é claro em um escopo pequeno (por exemplo, c para um contador) pode não ser suficiente em um escopo maior, exigindo mais clareza para lembrar ao desenvolvedor de seu propósito.

    A especificidade do conceito também pode ajudar a manter o nome de uma variável conciso. Por exemplo, se houver apenas um banco de dados em uso, um nome curto como db, que normalmente seria reservado para escopos pequenos, pode continuar claro mesmo em um escopo maior.

    Nomes de uma única palavra, como count e options, são um bom ponto de partida. Caso haja ambiguidade, palavras adicionais podem ser incluídas para torná-los mais claros, como userCount e projectCount.

    E como dica final: evite remover letras para reduzir o tamanho do nome, como em Sbx em vez de Sandbox, pois isso pode prejudicar a clareza. No entanto, há exceções, como nomes amplamente aceitos pela comunidade de forma reduzida, como db para database e ctx para context.

  • Ordenando com elegância no Go com o sort


    A ordenação de elementos em um array é uma tarefa frequentemente utilizada em diversos programas. Embora existam muitos algoritmos de ordenação bem conhecidos, ninguém quer ficar copiando blocos de código de um projeto para outro.

    Por isso, o Go oferece a biblioteca nativa sort.

    Ela permite ordenar arrays in-place (a ordenação é feita diretamente na estrutura de dados original, sem criar uma cópia adicional do array) com qualquer critério de ordenação, utilizando uma abordagem baseada em interfaces.

    A função sort.Sort não faz suposições sobre a estrutura dos dados; ela apenas exige que o tipo a ser ordenado implemente a interface sort.Interface, que define três métodos essenciais apresentados a seguir.


    Um ótimo exemplo inicial é o tipo sort.StringSlice, fornecido pela própria biblioteca sort. Ele já implementa a interface sort.Interface, conforme mostrado abaixo:


    Seguindo essas regras, é possível ordenar structs com base em seus campos como no exemplo a seguir, que ordena uma lista de alunos pela nota:


    Com a estrutura já definida, também é possível verificar se o array está ordenado usando sort.IsSorted, além de realizar a ordenação em ordem reversa com sort.Reverse. As funções são demonstradas abaixo, utilizando a mesma estrutura do exemplo anterior.


    A biblioteca também oferece outras funções bastante úteis:

    • Ints(x []int): ordena uma slice de inteiros em ordem crescente.
    • IntsAreSorted(x []int) bool: verifica se o slice x está ordenada em ordem crescente.
    • Strings(x []string): ordena um slice de strings em ordem crescente.
    • StringsAreSorted(x []string) bool: verifica se a slice x está ordenada em ordem crescente.


    ATENÇÃO

    Na própria documentação do pacote sort, na data de escrita deste artigo, existe uma nota na função sort.Sort que diz o seguinte:

    Em muitas situações, a função mais recente slices.SortFunc é mais ergonômica e apresenta melhor desempenho.

    Essa mesma nota se encontra na função sort.IsSorted:

    Em muitas situações, a função mais recente slices.IsSortedFunc é mais ergonômica e apresenta melhor desempenho.

    Em ambos os casos, recomendo avaliar qual abordagem adotar com base no que for mais vantajoso para a sua situação: a simplicidade das funções do pacote sort ou a possível otimização oferecida pelo pacote slices.

    Para descobrir qual delas apresenta melhor desempenho no seu contexto, utilize os benchmarks do Go (fica aqui o dever de casa para você, caro leitor, dar uma conferida nisso).

    A partir do Go 1.22, algumas funções do pacote sort já utilizam a biblioteca slices internamente, como é o caso de sort.Ints, que usa slices.Sort, e sort.IntsAreSorted, que usa slices.IsSorted.

    Para saber mais, confira as documentações oficiais:

    sort: https://pkg.go.dev/sort

    slices: https://pkg.go.dev/slices


    Referências:

    DONOVAN, Alan A. A.; KERNIGHAN, Brian W.. The Go Programming Language. Crawfordsville: Addison-Wesley, 2016.

    Documentação oficial da biblioteca sort: https://pkg.go.dev/sort

    Documentação oficial da biblioteca slices: https://pkg.go.dev/slices

  • Antes do main existe o init

    As funções init são funções especiais que são executadas antes de qualquer função no código.

    Elas são a terceira etapa na ordem de inicialização de um programa em Go, sendo:

    1. Os pacotes importados são inicializados;
    2. As variáveis e constantes globais do pacote são inicializadas;
    3. As funções init são executadas.

    Seu uso mais comum é preparar o estado do programa antes da execução principal na main, como por exemplo: verificar se variáveis de configuração estão corretamente definidas, checar a existência de arquivos necessários ou até mesmo criar recursos ausentes.

    É possível declarar várias funções init em um mesmo pacote e em pacotes diferentes, desde que todas utilizem exatamente o nome init.

    Quando isso ocorre, a ordem de execução delas é a seguinte dependendo do caso:

    Em pacotes com dependência entre si

    Se o pacote A depende do pacote B, a função init do pacote B será executada antes da função init do pacote A.

    O Go garante que todos os pacotes importados sejam completamente inicializados antes que o pacote atual comece sua própria inicialização.

    Essa dependência entre pacotes também pode ser forçada usando o identificador em branco, ou blank identifier (_), como no exemplo abaixo que o pacote foo será importado e inicializado, mesmo que não seja utilizado diretamente.

    Múltiplas funções init no mesmo arquivo

    Quando existem várias funções init no mesmo arquivo, elas são executadas na ordem em que aparecem no código.

    Múltiplas funções init em arquivos diferentes do mesmo pacote

    Nesse caso, a execução segue a ordem alfabética dos arquivos. Por exemplo, se um pacote contém dois arquivos, a.go e b.go e ambos possuem funções init, a função em a.go será executada antes da função em b.go.

    No entanto, não devemos depender da ordem de execução das funções init dentro de um mesmo pacote. Isso pode ser arriscado, pois renomeações de arquivos podem alterar a ordem da execução, impactando o comportamento do programa.

    Apesar de úteis, as funções init possuem algumas desvantagens e pontos de atenção que devem ser levados em conta na hora de escolher usá-las ou não:

    1. Elas podem dificultar o controle e tratamento de erros, pois já que não retornam nenhum valor, nem de erro, uma das únicas formas de tratar problemas em sua execução é via panic, que causa a interrupção da aplicação.
    2. Podem complicar a implementação de testes, por exemplo, se uma dependência externa for configurada dentro de init, ela será executada mesmo que não seja necessária para o escopo dos testes unitários. Além de serem executadas antes dos casos de teste, o que pode gerar efeitos colaterais inesperados.
    3. A alteração do valor de variáveis globais dentro da função init pode ser uma má prática em alguns contextos:
      • Dificulta testes: como o estado global já foi definido automaticamente pela init, é difícil simular diferentes cenários ou redefinir esse estado nos testes.
      • Aumenta o acoplamento: outras partes do código passam a depender implicitamente do valor dessas variáveis globais, tornando o sistema menos modular.
      • Reduz previsibilidade: como a inicialização acontece automaticamente e sem controle do desenvolvedor, fica mais difícil entender ou modificar o fluxo de execução do programa.
      • Afeta reutilização: bibliotecas que dependem de init com variáveis globais são menos reutilizáveis, pois forçam comportamentos no momento da importação.

    Em resumo, as funções init são úteis para configurações iniciais, mas seu uso deve ser criterioso, pois podem dificultar testes, tratamento de erros e tornar o código menos previsível.

    Referência: HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

  • A linha invisível que guia seu código

    Um modelo mental, no contexto de software, é a representação interna que um desenvolvedor constrói para compreender como um sistema ou trecho de código funciona. Ele não é visível, mas sim uma estrutura de raciocínio que permite prever o comportamento do sistema com base no conhecimento adquirido até aquele momento.

    Durante a leitura ou escrita de código, o desenvolvedor precisa manter esse modelo atualizado para entender, por exemplo, quais funções interagem entre si, como os dados fluem e quais efeitos colaterais podem ocorrer.

    Códigos com boa legibilidade exigem menos esforço cognitivo para manter esses modelos mentais coerentes e atualizados.

    Um dos fatores que contribuem para uma boa legibilidade é o alinhamento do código.

    Na Golang UK Conference de 2016, Mat Ryer apresentou o conceito de line of sight in code (“linha de visão no código”, em tradução literal). Ele define linha de visão como “uma linha reta ao longo da qual um observador tem visão desobstruída”.

    Aplicado ao código, isso significa que uma boa linha de visão não altera o comportamento da função, mas torna mais fácil para outras pessoas entenderem o que está acontecendo.

    A ideia é que o leitor consiga acompanhar o fluxo principal de execução olhando para uma única coluna, sem precisar pular entre blocos, interpretar condições ou navegar por estruturas aninhadas.

    Uma boa prática é alinhar o caminho feliz da função à esquerda do código. Isso facilita a visualização imediata do fluxo esperado.

    O caminho feliz é o fluxo principal de execução de uma função ou sistema, em que tudo ocorre como esperado sem erros, exceções ou desvios.

    Na imagem a seguir é possível visualizar um exemplo de caminho feliz:

    De modo geral, quanto mais níveis de aninhamento uma função possui, mais difícil ela se torna de ler e entender, além de ocultar o caminho feliz, como podemos ver na imagem abaixo:

    Mat Ryer em seu artigo Code: Align the happy path to the left edge apresenta mais dicas para uma boa linha de visão, sendo elas:

    Retorne o mais cedo possível de uma função. Essa prática melhora a legibilidade porque reduz o aninhamento e mantém o caminho feliz limpo e direto.

    Evite usar else para retornar valores, especialmente quando o if já retorna algo. Em vez disso, inverta a condição if (flip the if) e retorne mais cedo, deixando o fluxo principal fora do else.

    Coloque o retorno do caminho feliz (o sucesso) como a última linha da função. Isso ajuda a deixar o fluxo principal claro e previsível. Quem lê sabe que, se nada der errado, o sucesso acontece no final.

    Separe partes da lógica em funções auxiliares para que as funções principais fiquem curtas, claras e fáceis de entender. Funções muito longas e cheias de detalhes dificultam a leitura e a manutenção.

    Se você tem blocos de código muito grandes e indentados (por exemplo, dentro de um if, for ou switch), considere extrair esse bloco para uma nova função. Isso ajuda a manter a função principal mais plana, legível e fácil de seguir, evitando profundidade excessiva e “efeito escada” no código.

    Em resumo, cuidar da legibilidade do código é essencial para manter modelos mentais claros. Práticas como alinhar o caminho feliz, evitar aninhamentos profundos e extrair funções tornam o código mais fácil de entender, manter e evoluir.

    Referências:

    HARSANYI, Teiva. 100 Go mistakes and how to avoid them. Shelter Island: Manning, 2022.

    RYER, Mat. Code: Align the happy path to the left edge. 2016. Disponível em: https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88. Acesso em: 05 jul. 2025.