Go 1.18 marcou uma mudança estrutural no ecossistema GoLang ao introduzir type parameters (parâmetros de tipo), conhecidos popularmente como generics. O objetivo foi resolver um problema recorrente em bases de código Go: a duplicação de implementações para diferentes tipos (por exemplo, int, float64, string) e o uso excessivo de interface{} com type assertions, que reduz segurança de tipos e aumenta o risco de erros em tempo de execução.
Neste artigo, você vai entender, de forma aplicada, três pilares do recurso no Go 1.18+:
- Type Parameters: como declarar funções e tipos parametrizados.
- Constraints: como limitar quais tipos podem ser usados.
- Estruturas de Dados Genéricas: como implementar coleções reutilizáveis e seguras.
1) O que mudou no GoLang com generics
Antes do Go 1.18, havia duas alternativas principais para reutilização de código com múltiplos tipos:
- Duplicar funções (
MinInt,MinFloat, etc.), elevando custo de manutenção. - Usar
interface{}e converter o tipo depois, com risco de panic caso a conversão esteja errada.
Com generics, GoLang passou a permitir que funções e tipos sejam escritos uma única vez e usados com vários tipos, mantendo checagem de tipo em tempo de compilação.
2) Type Parameters: a base de tudo
2.1 Funções genéricas
Uma função genérica declara parâmetros de tipo entre colchetes [] logo após o nome da função.
Exemplo: função que retorna o primeiro elemento de um slice, funcionando para qualquer tipo:
package main
import "fmt"
func First[T any](s []T) (T, bool) {
var zero T
if len(s) == 0 {
return zero, false
}
return s[0], true
}
func main() {
v1, ok1 := First([]int{10, 20})
v2, ok2 := First([]string{"go", "lang"})
fmt.Println(v1, ok1)
fmt.Println(v2, ok2)
}
Pontos importantes:
Té um parâmetro de tipo.anyé um alias deinterface{}e significa “qualquer tipo”.var zero Té o valor zero do tipoT(0 para números,""para string,nilpara ponteiros, etc.).- O compilador verifica o uso correto de
T.
2.2 Inferência de tipo
Em muitos casos, o GoLang infere o tipo T a partir dos argumentos:
_ = First([]int{1, 2, 3}) // T = int
_ = First([]string{"a", "b"}) // T = string
Também é possível explicitar:
_ = First[int]([]int{1, 2, 3})
A inferência tende a reduzir verbosidade, mas explicitar pode ajudar em cenários ambíguos.
3) Constraints: limitando o que T pode ser
3.1 Por que constraints existem
O parâmetro T any permite qualquer tipo, mas isso limita operações. Por exemplo, você não pode usar > em T sem garantir que T seja ordenável.
Exemplo incorreto (não compila):
func Max[T any](a, b T) T {
if a > b { // erro: operador > não definido para "any"
return a
}
return b
}
Para permitir certas operações, usamos constraints, que descrevem um conjunto permitido de tipos.
3.2 Constraints com interfaces
Em GoLang, constraints são escritas como interfaces. Uma interface de constraint pode:
- exigir métodos (como interfaces tradicionais)
- ou definir um type set (conjunto de tipos permitido), usando união com
|e o operador~
Exemplo: suportar tipos numéricos específicos:
type Number interface {
int | int64 | float64
}
func Sum[T Number](a, b T) T {
return a + b
}
Agora T está limitado a int, int64 ou float64, e + passa a ser válido.
3.3 O operador ~ (aproximação por tipo subjacente)
O operador ~ permite aceitar tipos definidos pelo usuário cujo tipo subjacente é um tipo base.
Exemplo:
type MyInt int
type Integer interface {
~int | ~int64
}
func Inc[T Integer](v T) T {
return v + 1
}
func main() {
var x MyInt = 10
_ = Inc(x) // compila porque MyInt tem tipo subjacente int
}
Sem ~int, MyInt não seria aceito quando a constraint fosse apenas int.
3.4 Reuso de constraints prontas (constraints)
Há o pacote golang.org/x/exp/constraints (e discussões históricas sobre padronização). Em projetos, ele é frequentemente usado para constraints como constraints.Ordered (tipos ordenáveis). Exemplo:
import "golang.org/x/exp/constraints"
func Min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
Em ambientes corporativos, vale avaliar política de dependências externas: x/exp é experimental e pode ter mudanças. Quando estabilidade for requisito, uma alternativa é definir constraints internas (com type sets) para o conjunto de tipos de interesse do projeto.
4) Estruturas de dados genéricas: como criar e usar
Generics em GoLang não se limitam a funções; também se aplicam a tipos (structs, slices customizados, mapas encapsulados, etc.). Isso facilita criar coleções reutilizáveis sem perder segurança.
4.1 Uma Stack (pilha) genérica
Implementação clássica com slice:
package stack
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
last := len(s.items) - 1
v := s.items[last]
s.items = s.items[:last]
return v, true
}
func (s *Stack[T]) Len() int {
return len(s.items)
}
Uso:
s := stack.Stack[string]{}
s.Push("a")
s.Push("b")
v, ok := s.Pop()
Benefícios práticos:
- sem
interface{}e sem type assertions - API clara:
Stack[string]deixa explícito o tipo - erros aparecem na compilação, não em runtime
4.2 Um Set genérico com map[T]struct{}
Em GoLang, sets são normalmente implementados com mapa. Com generics:
package set
type Set[T comparable] struct {
m map[T]struct{}
}
func New[T comparable]() Set[T] {
return Set[T]{m: make(map[T]struct{})}
}
func (s Set[T]) Add(v T) {
s.m[v] = struct{}{}
}
func (s Set[T]) Has(v T) bool {
_, ok := s.m[v]
return ok
}
func (s Set[T]) Remove(v T) {
delete(s.m, v)
}
func (s Set[T]) Len() int {
return len(s.m)
}
A constraint comparable é essencial, pois chaves de map em Go precisam ser comparáveis.
Uso:
users := set.New[int]()
users.Add(10)
_ = users.Has(10)
4.3 Uma lista encadeada simples (exemplo didático)
Listas encadeadas são úteis para ilustrar tipos recursivos com generics:
package list
type Node[T any] struct {
Value T
Next *Node[T]
}
type List[T any] struct {
Head *Node[T]
}
func (l *List[T]) Prepend(v T) {
l.Head = &Node[T]{Value: v, Next: l.Head}
}
func (l *List[T]) ToSlice() []T {
var out []T
for n := l.Head; n != nil; n = n.Next {
out = append(out, n.Value)
}
return out
}
Esse padrão reduz duplicação para List[int], List[string], List[User] etc.
5) Boas práticas: quando usar (e quando evitar) generics no GoLang
5.1 Use generics quando houver reutilização real
Generics fazem sentido quando:
- a mesma lógica se repete para múltiplos tipos
- você quer construir coleções reutilizáveis (stack, set, pool, cache)
- o uso de
interface{}está forçando conversões inseguras
Evite quando:
- só existe um tipo concreto no projeto (generic vira custo sem benefício)
- a API fica difícil de entender (legibilidade é prioridade no GoLang)
- há risco de constraints muito amplas (“
anyem todo lugar”) que mascaram design fraco
5.2 Prefira constraints mínimas e explícitas
Quanto mais ampla a constraint, maior a chance de usos indevidos. Exemplos:
- Para chaves de mapa, use
comparable, nãoany. - Para ordenação, use uma constraint ordenável, não “tipos numéricos” se você também quer
string.
5.3 Atenção à segurança e à previsibilidade (contexto de Cyber Segurança)
Embora generics reduzam uma classe de falhas (erros de tipo em runtime), ainda existem pontos de atenção em Cyber Segurança e robustez:
- Validação de entrada continua obrigatória: generics não impedem dados inválidos.
- Evite esconder conversões inseguras dentro de funções genéricas que aceitam
anye usam reflexão ou type assertions internamente. - Em bibliotecas, documente invariantes: por exemplo, se um
Set[T]assumeTimutável (chave de map), isso deve ficar claro para evitar comportamento inesperado.
6) Checklist rápido para começar com generics no Go 1.18+
- Identifique duplicação: funções iguais para tipos diferentes.
- Transforme em função genérica com
func Nome[T ...](...). - Defina a constraint mais restrita que permita as operações necessárias.
- Escreva testes cobrindo múltiplos tipos (
int,string, tipos definidos pelo usuário). - Revise a API: se ficou mais complexa, considere manter versões concretas.
Conclusão
Com o Go 1.18+, GoLang passou a oferecer generics de forma pragmática: o suficiente para reduzir duplicação e aumentar segurança de tipos, sem abandonar o estilo direto da linguagem. Entender type parameters e constraints é o passo decisivo para construir funções reutilizáveis e estruturas de dados genéricas (como Stack[T] e Set[T]) de forma legível e segura.
Na prática, a recomendação é clara: use generics para o que é estruturalmente genérico (coleções, algoritmos de utilidade e camadas compartilhadas) e evite generalizar cedo demais. Isso mantém o código mais simples, auditável e previsível — características especialmente relevantes em ambientes onde confiabilidade e segurança importam.