Go

Dominando Generics em Go: Type Parameters, Constraints e Casos de Uso Reais em Projetos Reais

11 min de leitura

Dominando Generics em Go: Type Parameters, Constraints e Casos de Uso Reais em Projetos Reais

Introdução aos Generics em Go Generics é um recurso que chegou ao Go na versão 1.18, permitindo que você escreva código mais flexível e reutilizável sem perder a segurança de tipos. Antes disso, Go era conhecida por ser uma linguagem simples, sem suporte a tipos genéricos, o que frequentemente levava ao uso de e type assertions — uma abordagem que funcionava, mas era propensa a erros em tempo de execução. O impacto de adicionar generics ao Go foi significativo: agora você pode escrever funções, tipos e métodos que trabalham com múltiplos tipos de dados mantendo a segurança de tipos em tempo de compilação. Isso reduz duplicação de código, elimina type assertions desnecessários e melhora a legibilidade. Neste artigo, você entenderá como usar type parameters e constraints de forma prática, e verá casos reais onde generics fazem toda diferença. Type Parameters: O Conceito Fundamental Type parameters são placeholders para tipos que serão preenchidos quando você usa a função ou tipo genérico.

<h2>Introdução aos Generics em Go</h2>

<p>Generics é um recurso que chegou ao Go na versão 1.18, permitindo que você escreva código mais flexível e reutilizável sem perder a segurança de tipos. Antes disso, Go era conhecida por ser uma linguagem simples, sem suporte a tipos genéricos, o que frequentemente levava ao uso de <code>interface{}</code> e type assertions — uma abordagem que funcionava, mas era propensa a erros em tempo de execução.</p>

<p>O impacto de adicionar generics ao Go foi significativo: agora você pode escrever funções, tipos e métodos que trabalham com múltiplos tipos de dados mantendo a segurança de tipos em tempo de compilação. Isso reduz duplicação de código, elimina type assertions desnecessários e melhora a legibilidade. Neste artigo, você entenderá como usar type parameters e constraints de forma prática, e verá casos reais onde generics fazem toda diferença.</p>

<h2>Type Parameters: O Conceito Fundamental</h2>

<p>Type parameters são placeholders para tipos que serão preenchidos quando você usa a função ou tipo genérico. Eles são declarados entre colchetes angulares <code>[]</code> e funcionam de forma semelhante a linguagens como Java e TypeScript, mas com a simplicidade que Go sempre promoveu.</p>

<h3>Sintaxe Básica</h3>

<p>A sintaxe para declarar um type parameter é simples. Em uma função genérica, você coloca o nome do tipo entre colchetes após o nome da função:</p>

<pre><code class="language-go">func Primeiro[T any](slice []T) T {

if len(slice) == 0 {

var zero T

return zero

}

return slice[0]

}</code></pre>

<p>Aqui, <code>T</code> é o type parameter, e <code>any</code> é sua constraint (falaremos sobre isso na próxima seção). A função <code>Primeiro</code> aceita um slice de qualquer tipo e retorna o primeiro elemento. Você chama essa função passando o tipo explicitamente ou deixando o compilador inferir:</p>

<pre><code class="language-go">// Explícito

numeros := []int{1, 2, 3}

primeiro := Primeiro[int](numeros) // primeiro = 1

// Inferido

palavras := []string{&quot;hello&quot;, &quot;world&quot;}

palavra := Primeiro(palavras) // Go infere string automaticamente</code></pre>

<h3>Type Parameters em Tipos Personalizados</h3>

<p>Você também pode usar type parameters ao definir tipos (structs, interfaces, tipos base):</p>

<pre><code class="language-go">type Pilha[T any] struct {

elementos []T

}

func (p *Pilha[T]) Empilhar(elemento T) {

p.elementos = append(p.elementos, elemento)

}

func (p *Pilha[T]) Desempilhar() (T, bool) {

var zero T

if len(p.elementos) == 0 {

return zero, false

}

ultimo := p.elementos[len(p.elementos)-1]

p.elementos = p.elementos[:len(p.elementos)-1]

return ultimo, true

}</code></pre>

<p>Você instancia essa pilha para um tipo específico:</p>

<pre><code class="language-go">pilha := &amp;Pilha[string]{}

pilha.Empilhar(&quot;Alice&quot;)

pilha.Empilhar(&quot;Bob&quot;)

nome, ok := pilha.Desempilhar() // nome = &quot;Bob&quot;, ok = true</code></pre>

<h2>Constraints: Definindo Limites ao Polimorfismo</h2>

<p>Constraints definem quais tipos podem ser usados para um type parameter. Sem constraints, você está limitado a operações muito básicas (atribuição, passagem para funções como argumento). Constraints permitem assumir propriedades específicas dos tipos que você está trabalhando.</p>

<h3>Constraint <code>any</code></h3>

<p>A constraint <code>any</code> é equivalente a <code>interface{}</code> — permite qualquer tipo. É útil quando você não precisa de operações específicas no type parameter:</p>

<pre><code class="language-go">func Imprimir[T any](valor T) {

fmt.Println(valor)

}

Imprimir(42)

Imprimir(&quot;hello&quot;)

Imprimir(3.14)</code></pre>

<h3>Constraints Baseadas em Tipos Específicos</h3>

<p>Você pode especificar que um type parameter deve ser um dos tipos concretos listados:</p>

<pre><code class="language-go">func Somar[T int | int64 | float64](a, b T) T {

return a + b

}

resultado1 := Somar(5, 3) // int: 8

resultado2 := Somar(int64(10), 5) // int64: 15

resultado3 := Somar(2.5, 3.7) // float64: 6.2</code></pre>

<p>O operador <code>|</code> na constraint significa &quot;ou&quot;, permitindo múltiplos tipos.</p>

<h3>Constraints com Interfaces</h3>

<p>A abordagem mais poderosa é usar uma interface como constraint. Você define uma interface com os métodos ou características que o type parameter deve ter:</p>

<pre><code class="language-go">type Comparable interface {

Comparar(outro Comparable) int

}

func Maximo[T Comparable](a, b T) T {

if a.Comparar(b) &gt; 0 {

return a

}

return b

}</code></pre>

<p>Qualquer tipo que implemente a interface <code>Comparable</code> pode ser usado:</p>

<pre><code class="language-go">type Numero int

func (n Numero) Comparar(outro Comparable) int {

o := outro.(Numero)

if n &gt; o {

return 1

} else if n &lt; o {

return -1

}

return 0

}

max := Maximo(Numero(10), Numero(5)) // Numero(10)</code></pre>

<h3>Constraints Pré-definidas (Pacote <code>constraints</code>)</h3>

<p>Go fornece algumas constraints prontas no pacote <code>constraints</code>:</p>

<pre><code class="language-go">import &quot;golang.org/x/exp/constraints&quot;

func MaiorNumero[T constraints.Ordered](a, b T) T {

if a &gt; b {

return a

}

return b

}

resultado := MaiorNumero(7, 3) // 7

resultado2 := MaiorNumero(&quot;zebra&quot;, &quot;apple&quot;) // &quot;zebra&quot;</code></pre>

<p>A constraint <code>constraints.Ordered</code> funciona com tipos que suportam operadores de comparação (<code>&lt;</code>, <code>&gt;</code>, <code>==</code>, etc.).</p>

<h2>Casos de Uso Reais</h2>

<p>Generics resolvem problemas práticos do dia a dia. Vamos explorar cenários onde eles realmente fazem diferença.</p>

<h3>Caso 1: Estruturas de Dados Genéricas</h3>

<p>Antes de generics, implementar uma fila ou lista reutilizável era complicado. Agora:</p>

<pre><code class="language-go">type Fila[T any] struct {

items []T

}

func (f *Fila[T]) Enfileirar(item T) {

f.items = append(f.items, item)

}

func (f *Fila[T]) Desenfileirar() (T, bool) {

var zero T

if len(f.items) == 0 {

return zero, false

}

item := f.items[0]

f.items = f.items[1:]

return item, true

}

// Uso

filaDePessoas := &amp;Fila[string]{}

filaDePessoas.Enfileirar(&quot;Alice&quot;)

filaDePessoas.Enfileirar(&quot;Bob&quot;)

pessoa, ok := filaDePessoas.Desenfileirar() // &quot;Alice&quot;</code></pre>

<h3>Caso 2: Funções Utilitárias em Slices</h3>

<p>Operações comuns com slices agora são genéricas:</p>

<pre><code class="language-go">func Filtrar[T any](items []T, predicado func(T) bool) []T {

resultado := []T{}

for _, item := range items {

if predicado(item) {

resultado = append(resultado, item)

}

}

return resultado

}

numeros := []int{1, 2, 3, 4, 5}

pares := Filtrar(numeros, func(n int) bool { return n%2 == 0 })

// pares = []int{2, 4}</code></pre>

<h3>Caso 3: API HTTP com Respostas Genéricas</h3>

<p>Um padrão muito comum é uma resposta JSON genérica:</p>

<pre><code class="language-go">type Resposta[T any] struct {

Sucesso bool json:&quot;sucesso&quot;

Dados T json:&quot;dados&quot;

Erro string json:&quot;erro,omitempty&quot;

}

type Usuario struct {

ID int json:&quot;id&quot;

Nome string json:&quot;nome&quot;

}

func ObterUsuario(w http.ResponseWriter, r *http.Request) {

usuario := Usuario{ID: 1, Nome: &quot;Alice&quot;}

resposta := Resposta[Usuario]{

Sucesso: true,

Dados: usuario,

}

w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)

json.NewEncoder(w).Encode(resposta)

}</code></pre>

<h3>Caso 4: Cache Genérico</h3>

<p>Um cache que funciona com qualquer tipo de dado:</p>

<pre><code class="language-go">type Cache[T any] struct {

dados map[string]T

mu sync.RWMutex

}

func (c *Cache[T]) Set(chave string, valor T) {

c.mu.Lock()

defer c.mu.Unlock()

if c.dados == nil {

c.dados = make(map[string]T)

}

c.dados[chave] = valor

}

func (c *Cache[T]) Get(chave string) (T, bool) {

c.mu.RLock()

defer c.mu.RUnlock()

valor, existe := c.dados[chave]

return valor, existe

}

// Uso

cacheUsuarios := &amp;Cache[Usuario]{}

cacheUsuarios.Set(&quot;user:1&quot;, Usuario{ID: 1, Nome: &quot;Alice&quot;})

usuario, ok := cacheUsuarios.Get(&quot;user:1&quot;)</code></pre>

<h3>Caso 5: Constraint com Múltiplos Métodos</h3>

<p>Às vezes você precisa que um type parameter implemente vários métodos:</p>

<pre><code class="language-go">type Serializavel interface {

Marshal() ([]byte, error)

Unmarshal([]byte) error

}

func ProcessarDados[T Serializavel](dados T) ([]byte, error) {

return dados.Marshal()

}</code></pre>

<h2>Boas Práticas e Limitações</h2>

<p>Generics são poderosos, mas exigem moderação. Use generics quando eles reduzirem duplicação de código de verdade. Não use quando tornar o código mais complexo sem benefício claro.</p>

<h3>Quando NÃO usar Generics</h3>

<ul>

<li>Quando uma solução com <code>interface{}</code> é suficiente e clara</li>

<li>Quando a lógica depende muito do tipo específico (melhor é duplicar código)</li>

<li>Em tipos com apenas alguns casos específicos</li>

</ul>

<h3>Quando USAR Generics</h3>

<ul>

<li>Estruturas de dados que armazenam múltiplos tipos (pilhas, filas, árvores)</li>

<li>Funções utilitárias que operam em slices ou maps de qualquer tipo</li>

<li>APIs que retornam respostas genéricas</li>

<li>Quando você vê duplicação de código idêntico com tipos diferentes</li>

</ul>

<h2>Conclusão</h2>

<p>Generics em Go trouxe três melhorias fundamentais: <strong>reutilização de código sem sacrificar segurança de tipos</strong>, <strong>eliminação de type assertions desnecessários</strong> e <strong>clareza na intenção do código</strong> (quando bem utilizados). Type parameters e constraints funcionam juntos para permitir polimorfismo seguro. Na prática, você usará generics principalmente em estruturas de dados, funções utilitárias e padrões de API genérica — mas sempre com moderação, preferindo simplicidade quando possível.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://go.dev/doc/tutorial/generics" target="_blank" rel="noopener noreferrer">Documentação Oficial - Type Parameters</a></li>

<li><a href="https://github.com/golang/proposal/blob/master/design/43651-type-parameters.md" target="_blank" rel="noopener noreferrer">Go Proposal: Generics - Design Document</a></li>

<li><a href="https://pkg.go.dev/golang.org/x/exp/constraints" target="_blank" rel="noopener noreferrer">Pacote constraints - golang.org/x/exp</a></li>

<li><a href="https://go.dev/doc/effective_go" target="_blank" rel="noopener noreferrer">The Go Programming Language - Effective Go</a></li>

<li><a href="https://go.dev/tour/generics/1" target="_blank" rel="noopener noreferrer">A Tour of Go - Generics</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Migrations em Go com golang-migrate: Versionando o Banco de Dados: Do Básico ao Avançado
Migrations em Go com golang-migrate: Versionando o Banco de Dados: Do Básico ao Avançado

O que são Migrations e Por que Você Precisa Delas Migrations são scripts vers...

Validação de Dados em APIs Go com go-playground/validator: Do Básico ao Avançado
Validação de Dados em APIs Go com go-playground/validator: Do Básico ao Avançado

Introdução: Por Que Validar Dados em APIs? Quando você constrói uma API em Go...

Dominando Make e New em Go: Diferenças Práticas na Alocação de Memória em Projetos Reais
Dominando Make e New em Go: Diferenças Práticas na Alocação de Memória em Projetos Reais

Make e New em Go: Diferenças Práticas na Alocação de Memória Quando você come...