Categoria: Go

  • Armadilhas comuns ao copiar slices

    A função padrão copy do Go é utilizada para copiar elementos de um slice para outro.

    Apesar de simples, alguns erros comuns podem acontecer durante o seu uso.


    Atenção ao tamanho do slice de destino

    O primeiro ponto importante está relacionado ao tamanho do slice de destino.

    Para que a cópia funcione corretamente, o slice de destino precisa ter comprimento (len) maior ou igual ao número de elementos que serão copiados.

    Veja o exemplo abaixo:

    foo := []int{1, 2, 3}
    bar := make([]int, 0)
    
    copy(bar, foo)
    fmt.Println(bar) // Output: []
    

    Mesmo chamando copy, o slice bar continua vazio. Isso acontece porque ele tem comprimento zero — não há espaço para receber os elementos.

    O ideal é criar o slice de destino com o mesmo tamanho do slice de origem:

    foo := []int{1, 2, 3}
    bar := make([]int, len(foo))
    
    copy(bar, foo)
    fmt.Println(bar) // Output: [1 2 3]
    


    Ordem correta dos parâmetros

    Outro erro comum é inverter a ordem dos parâmetros. A assinatura correta da função é:

    copy(destino, origem)
    


    Usando append para copiar slices

    O copy não é a única forma de copiar slices. O append também pode ser usado:

    foo := []int{1, 2, 3}
    bar := append([]int(nil), foo...)
    
    fmt.Println(bar) // Output: [1 2 3]
    

    Apesar disso, o uso de copy costuma ser mais idiomático, pois deixa mais clara a intenção de copiar dados.

    Além disso, o append pode introduzir comportamentos inesperados se não for usado com cuidado.

    Um comportamento inesperado com append

    Analise o exemplo abaixo:

    s1 := []int{1, 2, 3}
    s2 := s1[1:2]
    s3 := append(s2, 10)
    
    fmt.Println(s1) // Output: [1 2 10]
    fmt.Println(s2) // Output: [2]
    fmt.Println(s3) // Output: [2 10]
    

    Mesmo sem modificar s1 diretamente, seu conteúdo é alterado.

    Isso acontece porque slices em Go compartilham o mesmo array subjacente.

    Quando criamos s2 := s1[1:2], o slice s2 aponta para a mesma área de memória de s1, apenas com um deslocamento e um tamanho diferente.

    Como s2 ainda possui capacidade suficiente para crescer dentro do array original de s1, o append(s2, 10) reutiliza esse array, sobrescrevendo o valor na posição seguinte — que corresponde ao índice 2 de s1.

    O resultado é:

    • s3 aponta para o mesmo array, agora com os valores [2, 10]
    • s1 é modificado para [1, 2, 10]

    Como evitar esse tipo de efeito colateral

    Existem duas abordagens principais.

    1. Criar uma cópia explícita com copy

    s1 := []int{1, 2, 3}
    
    s2 := make([]int, 2)
    copy(s2, s1[:2])
    
    s3 := append(s2, 10)
    

    Essa abordagem é segura, mas tem dois pontos negativos:

    • deixa o código um pouco mais verboso
    • adiciona uma cópia extra, o que pode ser relevante para slices grandes

    2. Usar a full slice expression

    A segunda opção é limitar explicitamente a capacidade do slice usando a full slice expression:

    s1 := []int{1, 2, 3}
    
    s2 := s1[:2:2]
    s3 := append(s2, 10)
    

    A expressão s[low:high:max] define não apenas o comprimento, mas também a capacidade do slice.

    Ao limitar a capacidade de s2 para 2, garantimos que qualquer append obrigatoriamente aloque um novo array, evitando efeitos colaterais no slice original — e sem a necessidade de realizar uma cópia manual.

    Ao trabalhar com slices em Go, é fundamental lembrar que eles compartilham memória. Se um slice resultante tiver comprimento menor que sua capacidade, chamadas a append podem modificar dados inesperadamente.


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

  • Slice nulo vs Slice vazio

    Ao trabalhar com slices em Go, é comum precisar inicializá-los sem elementos. Nesse cenário, temos duas opções principais:

    • Inicializar como nulo
    • Inicializar com tamanho zero


    Formas de inicialização

    var s []string        // nil = true
    s = []string(nil)     // nil = true
    s = []string{}        // nil = false
    s = make([]string, 0) // nil = false
    

    Em todos os casos, o slice é vazio, ou seja, len(s) == 0.

    A diferença está na alocação: slices nulos não ocupam memória, enquanto slices de tamanho zero já possuem uma estrutura alocada.


    Nulo vs. Zero Allocation

    • Slices nulo: não possuem backing array, são mais leves e não exigem alocação inicial.
    • Slices vazios (len=0): já possuem uma referência a um array interno, mesmo que sem elementos.

    Essa distinção pode impactar performance em cenários de alto volume, como funções que retornam slices frequentemente.


    Quando usar nulo

    Se não há necessidade de alocar memória antecipadamente, prefira inicializações nulas.

    Um exemplo prático:

    func f() []string {
        var s []string
        
        if foo() {
            s = append(s, "foo")
        }
    
        return s
    }
    

    Nesse caso, se a condição não for satisfeita, a função retorna um slice nulo, sem alocação desnecessária.

    Entre as opções de inicialização nula, a forma mais comum é: var s []string em vez de s = []string(nil).


    Atenção ao uso de slices vazios

    Evite inicializar slices com []string{} quando não há elementos, pois essa sintaxe é amplamente usada para inicialização com valores:

    s := []string{"foo", "bar", "baz"}
    

    Alguns linters podem até apontar []string{} vazio como erro, justamente para evitar confusão semântica.


    Nulo vs. Vazio em bibliotecas

    É importante destacar que algumas bibliotecas diferenciam slices nulos de slices vazios. Por exemplo, encoding/json e reflect tratam os dois casos de forma distinta:

    type Foo struct {
        Bar []string
    }
    
    func main() {
        var s1 []string
        f1 := Foo{Bar: s1}
        b, _ := json.Marshal(f1)
        fmt.Println(string(b)) // {"Bar":null}
    
        var s2 = make([]string, 0)
        f2 := Foo{Bar: s2}
        b, _ = json.Marshal(f2)
        fmt.Println(string(b)) // {"Bar":[]}
    }
    

    Essa diferença pode impactar clientes que esperam um array vazio em vez de null. Em APIs, por exemplo, retornar null pode indicar ausência de dados, enquanto [] indica que o campo existe, mas não possui elementos.


    Como validar se o slice tem elementos?

    Nunca compare diretamente com nil. A forma correta é verificar o comprimento:

    if len(s) != 0 { // slice contém elementos }
    

    Verificando o comprimento do slice, cobrimos todos os cenários:

    • Se o slice é nulo, len(slice) != 0 é falso
    • Se o slice não é nulo, mas é vazio, len(slice) != 0 também é falso

    Esse princípio também é válido para maps.

    Slices nulos e slices vazios compartilham o mesmo comportamento em relação ao comprimento (len == 0), mas diferem na alocação e em como algumas bibliotecas os interpretam. A regra prática é simples: para checar se há elementos, use sempre len. Além disso, escolha entre nulo ou vazio considerando performance, semântica e compatibilidade com bibliotecas externas.

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

  • Como não inicializar slices

    Problema da inicialização sem capacidade

    Inicializar um slice utilizando make sem determinar o tamanho e a capacidade não é nada performático.

    Vamos considerar um exemplo de função que converte um slice de Foo para um slice de Bar:

    func convert(foos []Foo) []Bar {
        bars := make([]Bar, 0) // Omitindo a capacidade
    
        for _, foo := range foos {
            bars = append(bars, fooToBar(foo))
        }
    
        return bars
    }
    

    Quando é adicionado o primeiro elemento no slice a partir do append, é alocado um array adjacente a ele com capacidade 1.

    Adicionando o segundo elemento, a capacidade do array adjacente é estourada e cria-se um novo array adjacente com o dobro da capacidade do primeiro, como explicado em: https://arturbaccarin.dev.br/capacidade-dos-slices-explicada/.

    Agora imagine se forem adicionados 1000 elementos ao slice. Isso fará com que sejam criados vários arrays adjacentes durante o processo, gerando uma carga extra ao Garbage Collector para limpar da memória os arrays não utilizados.


    Primeira solução: definir a capacidade esperada

    Uma das opções para reduzir a criação de arrays adjacentes e reutilizar o mesmo array é definir o slice já com a capacidade esperada:

    func convert(foos []Foo) []Bar {
        bars := make([]Bar, 0, len(foos))
    
        for _, foo := range foos {
            bars = append(bars, fooToBar(foo))
        }
    
        return bars
    }
    


    Segunda solução: definir o comprimento esperado

    A segunda opção é definir o slice com o comprimento esperado, inicializando seus elementos e substituindo os valores diretamente por referência ao índice:

    func convert(foos []Foo) []Bar {
        bars := make([]Bar, len(foos))
    
        for i, foo := range foos {
            bars[i] = fooToBar(foo)
        }
    
        return bars
    }
    

    Essa abordagem é levemente mais performática que a primeira, pois a adição de novos valores ao slice não utiliza a função append.

    No entanto, isso não justifica a utilização dessa opção em todos os casos. Aqui, a prioridade deve ser a legibilidade do código.


    Slices e condições

    Quando trabalhamos com slices em Go cujo tamanho futuro não é conhecido com precisão, como em casos em que elementos só são adicionados sob determinadas condições, surge a dúvida sobre como inicializar a estrutura de forma mais eficiente.

    • Se a condição for atendida na maioria das vezes, pode ser vantajoso reservar capacidade antecipadamente para reduzir realocações e otimizar o desempenho.
    • Por outro lado, quando a ocorrência é rara ou imprevisível, iniciar com um slice vazio evita desperdício de memória.

    Não existe uma regra absoluta, mas sim a necessidade de avaliar o comportamento esperado do programa e escolher a estratégia que melhor se adapta ao caso de uso.


    Em resumo, a escolha da forma de inicializar slices em Go deve equilibrar performance e clareza. Avalie sempre o padrão de uso esperado para decidir entre otimização de memória ou simplicidade de código.


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

  • Segredos do slicing de slices

    Slicing é a operação de criar uma nova slice a partir de outra slice já existente, especificando um intervalo de índices. A sintaxe básica é slice[low:high], onde low é o índice inicial (inclusivo) e high é o índice final (exclusivo); se omitidos, assumem valores padrão (início ou fim da coleção).

    Entretanto, quando um slice é criado a partir de outro slice, ambos compartilham do mesmo array subjacente, mas com comprimento e capacidade diferentes entre si.

    Lembrando que o comprimento é o número de elementos referenciados pelo slice, enquanto a capacidade é o número de elementos do array subjacente.

    s1 := make([]int, 3, 6)
    s2 := s1[1:3]
    


    O perigo do compartilhamento

    Ambos os slices estão ligados ao mesmo array subjacente e, por conta disso, se há alguma alteração dos elementos compartilhados entre ambos, essa alteração tem reflexo nos dois slices.

    func main() {
    	s1 := make([]int, 3, 6)
    	s2 := s1[1:3]
    
    	fmt.Println(s1) // output: [0 0 0]
    	fmt.Println(s2) // output: [0 0]
    
    	s2[0] = 1
    
    	fmt.Println(s1) // output: [0 1 0]
    	fmt.Println(s2) // output: [1 0]
    
    	s1[2] = 2
    
    	fmt.Println(s1) // output: [0 1 2]
    	fmt.Println(s2) // output: [1 2]
    }
    


    Append em slices compartilhados

    Agora, o que acontece se adicionarmos mais um elemento em s2?

    Nesse caso, o array subjacente é modificado com a adição do novo elemento, porém somente o tamanho de s2 se altera. Isso significa que, mesmo s1 e s2 compartilharem o mesmo array, o novo elemento pode ser acessado somente por s2.

    func main() {
    	s1 := make([]int, 3, 6)
    	s2 := s1[1:3]
    
    	fmt.Println(len(s1)) // output: 3
    	fmt.Println(cap(s1)) // output: 6
    
    	s2 = append(s2, 4)
    
    	fmt.Println(len(s1)) // output: 3
    	fmt.Println(cap(s1)) // output: 6
    
    	fmt.Println(s1) // output: [0 0 0]
    	fmt.Println(s2) // output: [0 0 4]
    }
    


    Quando a capacidade é ultrapassada

    Por fim, o que acontece se continuarmos adicionando elementos em s2 até passar da capacidade do array subjacente?

    Quando a quantidade de elementos ultrapassa a capacidade, é criado um novo array subjacente, com o dobro da capacidade. Os elementos de s2 são copiados para ele e s2 passa a referenciá-lo. Com isso, s1 e s2 deixam de compartilhar o mesmo array. Alterações em s2 não terão mais efeito em s1 e vice-versa.

    func main() {
    	s1 := make([]int, 3, 6)
    	s2 := s1[1:3]
    
    	fmt.Println(len(s1)) // output: 3
    	fmt.Println(cap(s1)) // output: 6
    
    	fmt.Println(len(s2)) // output: 2
    	fmt.Println(cap(s2)) // output: 5
    
    	s2 = append(s2, 4, 5, 6, 7)
    
    	fmt.Println(len(s1)) // output: 3
    	fmt.Println(cap(s1)) // output: 6
    
    	fmt.Println(len(s2)) // output: 6
    	fmt.Println(cap(s2)) // output: 10
    
    	s2[0] = 1
    
    	fmt.Println(s1) // output: [0 0 0]
    	fmt.Println(s2) // output: [1 0 4 5 6 7]
    }
    


    O slicing em Go permite criar novas fatias a partir de arrays ou slices já existentes. É importante lembrar que slices podem compartilhar o mesmo array subjacente, o que faz com que alterações em um afetem o outro. Quando a capacidade é ultrapassada, um novo array é criado e o slice passa a referenciá-lo sozinho. Entender esse comportamento ajuda a usar slices de forma correta e sem surpresas.


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

  • Capacidade dos slices explicada

    Arrays

    Os arrays em Go possuem um tamanho fixo e não precisam ser inicializados explicitamente. Isso significa que, quando criados, já estão prontos para uso com todos os seus elementos definidos com os zero values do tipo correspondente, caso nenhum valor seja especificado.

    var a [4]int
    a[0] = 1
    i := a[0]
    // i == 1
    
    // a[2] == 0
    

    Em relação ao espaço na memória, os arrays distribuem seus valores lado a lado sequencialmente. A variável de array consiste no array inteiro com seus elementos. Ele não é apenas um ponteiro para o primeiro elemento, como em C. Isso significa que, ao atribuir ou passar um array, será criada uma cópia completa do seu conteúdo.

    Slices

    Os slices em Go são estruturas de dados semelhantes aos arrays, mas com comportamento diferente: não possuem tamanho fixo e contam com uma estrutura mais flexível.

    Um slice é composto por três elementos:

    • Um ponteiro para um array subjacente
    • Um inteiro que determina seu comprimento (len)
    • Um inteiro que determina sua capacidade (cap)

    O comprimento é o número de elementos referenciados pelo slice, enquanto a capacidade é o número de elementos do array subjacente.

    Podemos criar slices utilizando a função padrão make:

    func make([]T, len, cap) []T
    
    s := make([]byte, 5, 5)
    

    O parâmetro cap é opcional. Quando não informado, a capacidade será igual ao comprimento:

    s := make([]int, 5)
    
    len(s) == 5
    cap(s) == 5
    

    Ao executar essa função, é criado na memória um array com o tamanho definido pela capacidade e com a quantidade de elementos inicializados igual ao comprimento.

    s := make([]int, 3, 6) // → [0] [0] [0] [] [] []
    

    Se tentarmos acessar s[3], teremos o erro: panic: runtime error: index out of range [3] with length 3

    Para utilizar as demais posições da capacidade, usamos o append:

    s = append(s, 2)
    

    Nesse caso, o comprimento passa para 4 e o slice continua apontando para o mesmo array subjacente.

    Se adicionarmos mais elementos até ultrapassar a capacidade:

    s = append(s, 3, 4, 5)
    

    Como o array tem tamanho fixo, ao tentar inserir o sexto elemento o Go cria internamente um novo array com o dobro da capacidade, copia os elementos do array antigo e adiciona o novo valor. O ponteiro do slice passa a apontar para esse novo array.

    A memória do array antigo será liberada pelo garbage collector, caso esteja na heap.

    Esse comportamento pode ser observado no exemplo abaixo:

    func main() {
    	s := make([]int, 3, 6)
    	fmt.Printf("%p\n", &s[0]) // output: 0xc000100000
    
    	s = append(s, 2)
    	fmt.Printf("%p\n", &s[0]) // output: 0xc000100000
    
    	s = append(s, 3, 4, 5)
    	fmt.Println(cap(s))       // output: 12
    	fmt.Printf("%p\n", &s[0]) // output: 0xc000180000
    }
    
    • O slice é criado com um array subjacente de seis posições, com endereço inicial 0xc000100000.
    • Após adicionar um elemento, o endereço permanece o mesmo.
    • Ao adicionar mais três elementos, a capacidade passa de 6 para 12 e o endereço muda para 0xc000180000, indicando a criação de um novo array adjacente.

    A capacidade define até onde um slice pode crescer antes que o runtime precise alocar um novo array e copiar os dados existentes. Esse processo de realocação pode impactar diretamente a performance de aplicações que manipulam grandes volumes de dados ou realizam muitas operações de inserção. Planejar corretamente o tamanho inicial e a capacidade dos slices ajuda a reduzir cópias desnecessárias, otimizar o uso da memória e garantir maior desempenho em cenários críticos.


    Referência:

    GERRAND, Andrew. Go Slices: usage and internals. 2011. Disponível em: https://go.dev/blog/slices-intro. Acesso em: 09 dez. 2025.

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

  • Os desafios dos pontos flutuantes

    Os números com ponto flutuante (float) surgiram da necessidade de representar valores fracionários, algo que os inteiros não são capazes de fazer.

    É importante destacar que um float é sempre uma aproximação de um resultado aritmético real, devido à forma como é representado digitalmente em bits.

    Por exemplo, o número 1,0001² = 1,00020001. Se realizarmos a mesma operação utilizando um float32, o resultado será 1,0002, o que evidencia a perda de casas decimais importantes para a precisão.


    Representação em Go

    Em Go, os números de ponto flutuante seguem o padrão IEEE-754, que define sua representação interna por meio da divisão dos bits em duas partes principais:

    • Mantissa: contém os dígitos significativos do número, determinando sua precisão.
    • Expoente: indica a escala, ou seja, quanto o ponto binário deve ser deslocado para representar valores muito grandes ou muito pequenos.

    No caso de um float32, temos:

    • 1 bit para o sinal (positivo/negativo)
    • 8 bits para o expoente
    • 23 bits para a mantissa


    Comparações e precisão

    A comparação direta entre dois valores de ponto flutuante usando o operador == pode gerar resultados imprecisos. Isso ocorre porque muitos números decimais não podem ser representados exatamente em binário, levando a pequenas diferenças de arredondamento na memória.

    Além disso, o resultado de cálculos em ponto flutuante depende do processador utilizado. Diferentes FPUs (Floating Point Units) podem produzir resultados numericamente distintos para os mesmos cálculos. Por isso, utilizar uma delta (tolerância) é a abordagem mais segura para comparações, garantindo consistência entre máquinas e arquiteturas diferentes.


    A ordem das operações importa

    Outro ponto de atenção é a ordem das operações. Em cálculos de adição, subtração, multiplicação ou divisão, a sequência escolhida influencia diretamente a precisão do resultado. Por exemplo:

    func main() {
    	a := 1e16 // big number
    	b := 1.0  // small numver
    	c := 3.14159265
    
    	// Ordem 1: (a + b) * c
    	result1 := (a + b) * c
    
    	// Ordem 2: a*c + b*c
    	result2 := a*c + b*c
    
    	fmt.Println(result1)                     // output: 3.1415926500000004e+16
    	fmt.Println(result2)                     // output: 3.141592650000001e+16
    	fmt.Println(math.Abs(result1 - result2)) // output: 4.0000000000
    }
    
    • (a + b) * c → ao somar a + b, o valor de b é perdido devido à precisão limitada do float64.
    • a*c + b*c → o cálculo de b*c é feito separadamente, preservando sua contribuição.

    Em geral, realizar primeiro as operações de multiplicação e divisão tende a produzir resultados mais precisos, pois reduz a propagação de erros de arredondamento.

    Os números de ponto flutuante são indispensáveis para representar valores fracionários e realizar cálculos numéricos em Go e em diversas linguagens de programação. No entanto, compreender suas limitações é essencial: eles não oferecem exatidão absoluta, mas sim aproximações que variam conforme a representação binária e a arquitetura do processador.

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

  • Lidando com overflow em inteiros

    No Go existem 10 formas de declarar variáveis inteiras, cada uma com capacidade de armazenamento diferente:

    • int8 (8 bits) → -128 a 127
    • int16 (16 bits) → -32.768 a 32.767
    • int32 (32 bits) → -2.147.483.648 a 2.147.483.647
    • int64 (64 bits) → -9.223.372.036.854.775.808 a 9.223.372.036.854.775.807
    • uint8 (8 bits) → 0 a 255
    • uint16 (16 bits) → 0 a 65.535
    • uint32 (32 bits) → 0 a 4.294.967.295
    • uint64 (64 bits) → 0 a 18.446.744.073.709.551.615


    Além desses, existem os tipos int e uint, sem especificação de tamanho. Seu tamanho depende da arquitetura do sistema:

    • Em sistemas 32 bits → 32 bits
    • Em sistemas 64 bits → 64 bits

    A recomendação é usar int por padrão, a menos que haja necessidade específica de um tipo com tamanho fixo ou sem sinal.

    Essa escolha é feita em tempo de compilação.


    Overflow (Estouro de Memória)

    O overflow ocorre quando se tenta armazenar um valor maior (ou menor) do que a capacidade da variável. Exemplo: salvar o valor 128 em um int8.

    var x int8 = 128
    // Erro de compilação: cannot use 128 (untyped int constant) as int8 value in variable declaration (overflows)
    


    No entanto, em tempo de execução o comportamento é silencioso e pode gerar erros:

    var x int8 = 127
    x++
    println(x) // saída: -128
    


    Como prevenir overflow?

    O pacote padrão math fornece constantes com os valores máximos e mínimos de cada tipo, como:

    • math.MaxInt32
    • math.MaxUint64
    • math.MinInt16
    • math.MaxInt / math.MinInt (dependentes do sistema)


    I. Incremento seguro

    func Inc32(counter int32) int32 {
        if counter == math.MaxInt32 {
            panic("int32 overflow")
        }
        return counter + 1
    }
    


    II. Soma segura

    func AddInt(a, b int) int {
        if a > math.MaxInt-b {
            panic("int overflow")
        }
        return a + b
    }
    


    III. Multiplicação segura

    func MultiplyInt(a, b int) int {
        if a == 0 || b == 0 {
            return 0
        }
        result := a * b
        if a == 1 || b == 1 {
            return result
        }
        if a == math.MinInt || b == math.MinInt {
            panic("integer overflow")
        }
        if result/b != a {
            panic("integer overflow")
        }
        return result
    }
    


    Por fim, quando for necessário manipular valores além dos limites permitidos pelos tipos nativos, como em aplicações de criptografia ou cálculos científicos, o Go oferece o pacote nativo math/big. Esse pacote fornece tipos numéricos capazes de representar e manipular inteiros, racionais e números de ponto flutuante com precisão arbitrária, ou seja, sem as restrições de tamanho impostas por tipos como int64 ou float64.

    Ele disponibiliza estruturas como big.Int, big.Rat e big.Float, que suportam operações matemáticas avançadas (soma, subtração, multiplicação, divisão, exponenciação, comparações etc.), todas seguindo regras de precisão controlada.


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

  • Nomeando pacotes utilitários


    Um pacote utilitário é aquele que reúne funções, tipos e estruturas “genéricas” ou “de uso comum”. São recursos que, em teoria, podem ser aproveitados por diferentes partes de um projeto.

    Exemplo de funções típicas:

    func Contains(slice []string, s string) bool
    func Min(a, b int) int
    func Max(a, b int) int
    

    Mas surge a pergunta: como dar nome a esses pacotes de forma significativa?

    Nomes como utils, common, shared ou base não são boas práticas. Eles carregam pouco significado e não oferecem nenhum insight sobre o que o pacote realmente provê.

    Em vez de criar um grande pacote utilitário, a recomendação é dividir em pacotes menores, com nomes expressivos e coesos.

    💡 Dica: nomeie o pacote pelo que ele fornece, e não apenas pelo que ele contém. Isso aumenta a clareza e a expressividade do código.

    No exemplo abaixo, o pacote utils concentra funções sem relação direta entre si. O problema é que, ao crescer, esse pacote se torna difícil de manter e confuso de usar.


    Estrutura inicial

    myapp/
    ├── main.go
    └── utils/
        └── utils.go
    package utils
    
    func Contains(slice []int, value int) bool {
        for _, v := range slice {
            if v == value {
                return true
            }
        }
        return false
    }
    
    type StringSet map[string]struct{}
    
    func NewStringSet() StringSet {
        return make(StringSet)
    }
    
    func (s StringSet) Add(item string) {
        s[item] = struct{}{}
    }
    
    func (s StringSet) Has(item string) bool {
        _, exists := s[item]
        return exists
    }
    

    Uso no main.go.

    package main
    
    import (
        "fmt"
        "myapp/utils"
    )
    
    func main() {
        nums := []int{1, 2, 3}
        fmt.Println(utils.Contains(nums, 2)) // true
    
        s := utils.NewStringSet()
        s.Add("go")
        fmt.Println(s.Has("go")) // true
    }
    


    Estrutura reorganizada

    Ao dividir em pacotes menores e mais específicos, temos:

    myapp/
    ├── main.go
    ├── sliceutil/
    │ └── contains.go
    └── set/
    └── stringset.go
    package sliceutil
    
    func ContainsInt(slice []int, value int) bool {
        for _, v := range slice {
            if v == value {
                return true
            }
        }
        return false
    }
    
    
    package stringset
    
    func New(...string) map[string]struct{} { ... }
    func Sort(map[string]struct{}) []string { ... }
    

    Uso no main.go.

    package main
    
    import (
        "fmt"
        "myapp/sliceutil"
        "myapp/set"
    )
    
    func main() {
        nums := []int{1, 2, 3}
        fmt.Println(sliceutil.ContainsInt(nums, 2))
    
        s := stringset.New()
        stringset.Sort(s)
    }
    


    Benefícios da reorganização

    • Maior clareza: a chamada das funções fica mais explícita.
    • Melhor leitura: o código revela sua intenção de forma imediata.
    • Alta coesão: cada pacote agrupa funções relacionadas, evitando o “pacote monstro” difícil de manter.

    É verdade que os novos pacotes são pequenos, com poucos arquivos e funções. À primeira vista, isso pode parecer excesso de fragmentação. No entanto, se cada pacote tem alta coesão e seus objetos não pertencem a outro lugar específico, essa organização é não apenas aceitável, mas recomendada.


    👉 Em resumo: evite pacotes utilitários genéricos. Prefira nomes expressivos e pacotes coesos. Essa prática melhora a manutenção, a legibilidade e a escalabilidade do seu projeto.


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

  • 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.