Generics em Go: type parameter através de exemplos
1. Visão geral
A partir do Go 1.18, você pode escrever funções e tipos com type parameter. Assim, você não precisa duplicar a mesma lógica para int, float64, string, nem fazer conversão a partir de interface{}.
Este artigo vai desde o problema inicial até funções genéricas, constraints e a notação ~. Depois vêm tipos genéricos como Stack[T] e Cache[K, V], como escrever testes e erros comuns. Ao final, você saberá quando usar generics e quando uma interface comum é suficiente.
Todos os exemplos usam Go 1.22 ou superior. O package cmp está disponível desde Go 1.21. Arquivo go.mod mínimo:
module example.com/shop
go 1.22
Nenhuma biblioteca externa necessária. Tudo neste artigo está na standard library.
2. O problema antes dos generics
Suponha que você precise calcular a soma de quantidades (int) e a soma de valores (float64). Antes do Go 1.18, você geralmente escrevia duas funções idênticas:
func SumInts(xs []int) int {
var total int
for _, x := range xs {
total += x
}
return total
}
func SumFloats(xs []float64) float64 {
var total float64
for _, x := range xs {
total += x
}
return total
}
Outra forma era aceitar []interface{} e usar type switch. Essa abordagem perdia type safety. Erros só apareciam em tempo de execução, e você precisava fazer conversão de tipo em toda parte do código que chamava a função.
Generics resolvem exatamente isso: escreva a lógica uma vez, e o compiler ainda verifica tipos para você.
3. Sua primeira função genérica
3.1. Sintaxe de type parameter
Type parameters são declarados entre colchetes, logo após o nome da função:
package main
import "fmt"
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](xs []T) T {
var total T
for _, x := range xs {
total += x
}
return total
}
func main() {
quantities := []int{2, 1, 5}
prices := []float64{120.5, 99.9, 15}
fmt.Println(Sum(quantities))
fmt.Println(Sum(prices))
}
Explicando cada parte:
[T Number]declara o type parameterT.Tdeve satisfazer a constraintNumber.Numberé uma interface que contém um type set:int,int64,float64e tipos derivados deles (o significado de~está na seção 4.3).var total Tcria o zero value deT, ou seja,0ou0.0.- O operador
+=é válido porque todos os tipos no type set suportam adição.
Resultado:
8
235.4
3.2. Type inference
Você percebeu que chamamos Sum(quantities) e não Sum[int](quantities)? O compiler infere automaticamente T a partir dos argumentos. Isso é type inference.
Você só precisa especificar explicitamente quando o compiler não tem informações suficientes para inferir, por exemplo quando o type parameter aparece apenas no tipo de retorno:
func Zero[T any]() T {
var z T
return z
}
func main() {
fmt.Println(Zero[string]() == "") // true
}
3.3. Múltiplos type parameters: Map e Filter
Estas são duas das funções utilitárias mais populares. Vamos aplicá-las à lista de Order:
package main
import "fmt"
type Order struct {
ID string
Customer string
Total float64
}
func Map[T, U any](xs []T, f func(T) U) []U {
out := make([]U, 0, len(xs))
for _, x := range xs {
out = append(out, f(x))
}
return out
}
func Filter[T any](xs []T, keep func(T) bool) []T {
var out []T
for _, x := range xs {
if keep(x) {
out = append(out, x)
}
}
return out
}
func main() {
orders := []Order{
{"A01", "Lan", 250_000},
{"A02", "Minh", 1_200_000},
{"A03", "Lan", 890_000},
}
big := Filter(orders, func(o Order) bool { return o.Total >= 500_000 })
ids := Map(big, func(o Order) string { return o.ID })
fmt.Println(ids)
}
Map[T, U any]tem dois type parameters, então o tipo de entrada e saída podem ser diferentes.anyé um alias parainterface{}. Permite qualquer tipo, mas você não pode chamar operadores nesse valor além de atribuição e passagem adiante.- O compiler infere
T = OrdereU = stringa partir da funçãofunc(o Order) string.
Resultado:
[A02 A03]
4. Constraints: limitando o que T pode fazer
A constraint determina o que você pode fazer com valores do tipo T dentro da função.
4.1. comparable
comparable é uma constraint pré-definida. Ela permite usar == e !=, e é também a condição para T ser chave de um mapa:
func Unique[T comparable](xs []T) []T {
seen := make(map[T]struct{}, len(xs))
var out []T
for _, x := range xs {
if _, ok := seen[x]; !ok {
seen[x] = struct{}{}
out = append(out, x)
}
}
return out
}
// Unique([]string{"Lan", "Minh", "Lan"}) => [Lan Minh]
O struct Order acima também é comparable, porque todos os seus campos podem ser comparados. Structs contendo slices, maps ou funções não satisfazem comparable, e o compiler reportará um erro.
4.2. cmp.Ordered
Quando você precisa comparar maior ou menor, use cmp.Ordered da standard library:
import "cmp"
func MaxBy[T any, K cmp.Ordered](xs []T, key func(T) K) (T, bool) {
var best T
if len(xs) == 0 {
return best, false
}
best = xs[0]
for _, x := range xs[1:] {
if key(x) > key(best) {
best = x
}
}
return best, true
}
// top, _ := MaxBy(orders, func(o Order) float64 { return o.Total })
// top.ID => "A02"
K cmp.Orderedpermite usar>em valores de chave.- A função retorna
(T, bool)para lidar com slices vazios sem panicking.
Nota: Para slices numéricos simples, você não precisa escrever. Go 1.21 já tem funções built-in
max/min, e o packageslicesforneceslices.Max,slices.Contains,slices.Sort. Verifiqueslicesemapsantes de escrever seus próprios utilitários genéricos.
4.3. A notação ~ (tilde)
No mundo real, você frequentemente define tipos próprios para valores monetários:
type VND int64
func main() {
payments := []VND{150_000, 350_000}
fmt.Println(Sum(payments)) // 500000
}
Esta linha compila graças a ~int64 em Number. A notação ~int64 significa “qualquer tipo cujo underlying type seja int64”. Se você escrever int64 sem ~, então VND não satisfaz a constraint, e o compiler reporta:
VND does not satisfy Number (possibly missing ~ for int64 in Number)
Regra simples: ao escrever constraints para tipos básicos, quase sempre você deve usar ~.
4.4. Constraints combinadas com métodos
Constraints também podem exigir métodos, como interfaces comuns:
type Identifiable interface {
comparable
Key() string
}
Um tipo que queira satisfazer Identifiable deve ser comparável e ter um método Key() string.
5. Tipos genéricos
5.1. Stack[T]
Type parameters não são apenas para funções, mas também para structs:
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) }
Forma de usar:
var history Stack[Order]
history.Push(Order{ID: "A01"})
history.Push(Order{ID: "A02"})
o, _ := history.Pop()
fmt.Println(o.ID, history.Len()) // A02 1
- O receiver é escrito como
*Stack[T], deve incluir[T]. - Ao declarar uma variável, você deve especificar o tipo:
Stack[Order]. Não há inferência aqui. - O zero value de
Stack[Order]é utilizável imediatamente, porqueappendfunciona bem em slices nil.
5.2. Cache[K, V] seguro para múltiplas goroutines
Um exemplo mais realista: um cache in-memory compartilhado para Customer e Order.
package cache
import "sync"
type Cache[K comparable, V any] struct {
mu sync.RWMutex
data map[K]V
}
func New[K comparable, V any]() *Cache[K, V] {
return &Cache[K, V]{data: make(map[K]V)}
}
func (c *Cache[K, V]) Put(key K, value V) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
func (c *Cache[K, V]) Get(key K) (V, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
v, ok := c.data[key]
return v, ok
}
type Customer struct {
ID int
Name string
}
customers := cache.New[int, Customer]()
customers.Put(1, Customer{ID: 1, Name: "Lan"})
orders := cache.New[string, Order]()
orders.Put("A01", Order{ID: "A01", Total: 250_000})
c, ok := customers.Get(1)
fmt.Println(c.Name, ok) // Lan true
Uma única implementação serve dois tipos de dados. customers.Get retorna exatamente Customer, sem necessidade de type assertion. Se você acidentalmente Put um Order em customers, o código não compilará.
6. Escrevendo testes para código genérico
Go usa o package testing padrão. Table-driven tests funcionam normalmente com funções genéricas. Cada tabela de teste corresponde a um tipo específico:
package main
import "testing"
func TestSum(t *testing.T) {
ints := []struct {
name string
in []int
want int
}{
{"empty", nil, 0},
{"positive", []int{2, 1, 5}, 8},
{"mixed", []int{10, -3}, 7},
}
for _, tc := range ints {
t.Run(tc.name, func(t *testing.T) {
if got := Sum(tc.in); got != tc.want {
t.Errorf("Sum(%v) = %d, want %d", tc.in, got, tc.want)
}
})
}
if got := Sum([]VND{150_000, 350_000}); got != 500_000 {
t.Errorf("Sum VND = %d, want 500000", got)
}
}
func TestStackPopEmpty(t *testing.T) {
var s Stack[string]
v, ok := s.Pop()
if ok || v != "" {
t.Fatalf("Pop() on empty = (%q, %v), want (\"\", false)", v, ok)
}
}
Executar testes:
go test ./... -v
=== RUN TestSum
=== RUN TestSum/empty
=== RUN TestSum/positive
=== RUN TestSum/mixed
--- PASS: TestSum (0.00s)
=== RUN TestStackPopEmpty
--- PASS: TestStackPopEmpty (0.00s)
PASS
Teste pelo menos um tipo derivado como VND. Esta é a forma mais rápida de detectar uma constraint faltando ~.
7. Erros comuns e quando não usar
Métodos não podem ter type parameters próprios. O seguinte não compila:
// erro: methods cannot have type parameters
func (s *Stack[T]) Map[U any](f func(T) U) *Stack[U] { ... }
Escreva como uma função de nível superior: func MapStack[T, U any](s *Stack[T], f func(T) U) *Stack[U].
Não use type switch diretamente em T. Para fazer ramificação por tipo, você deve converter para any primeiro: switch v := any(x).(type) { ... }. Se você se vê fazendo isso frequentemente, generics pode não ser a ferramenta certa.
Constraints com type set não podem ser usadas como tipos de variáveis. var n Number reportará um erro. Interfaces contendo | ou ~ podem ser usadas apenas em posições de constraint.
comparable com interface pode fazer panic. Se K é um tipo interface e o valor dinâmico dentro é um slice, comparação ou uso como chave de mapa fará panic em tempo de execução.
Nota: Generics não são automaticamente mais rápidos que interfaces. O compilador Go usa a técnica “GC shape stenciling” com dicionários. Diferentes tipos de ponteiro podem compartilhar uma versão do código, e chamadas de método através de constraints ainda têm custos indiretos. Se performance importa, meça com benchmark em vez de adivinhar.
Quando NÃO usar generics:
- Quando a lógica depende de comportamento (métodos), não de tipos. Por exemplo,
PaymentServicerecebe umNotifiercom métodoSend. Interfaces geralmente são mais concisas e legíveis. - Quando há apenas um tipo específico usando a função. Não generalize “para o caso em que você possa precisar depois”.
- Quando
slices,maps,cmpjá têm a função que você precisa.
Uma regra fácil de lembrar: se você está escrevendo o mesmo código para vários tipos e a única diferença é o nome do tipo, é hora de usar generics.
8. Conclusão
Este artigo cobriu as principais partes de generics em Go:
- Declarar type parameters
[T C]e deixar o compiler inferir tipos. - Usar constraints
any,comparable,cmp.Orderede definir constraints próprias com~para suportar tipos derivados comoVND. - Construir tipos genéricos
Stack[T]eCache[K, V]seguros para múltiplas goroutines. - Escrever table-driven tests para funções genéricas.
- Reconhecer limitações: métodos não têm type parameters próprios, constraints não são tipos de variáveis, e generics não substituem interfaces.
Comece de onde há duplicação real de código no seu projeto. Escreva generics no mínimo necessário, priorize a standard library e sempre teste com pelo menos dois tipos diferentes.