Generics no Go 1.18: escrevendo código reutilizável
Go 1.18 trouxe a feature mais pedida da história da linguagem: Generics (tipos paramétricos). Agora é possível escrever funções e tipos que trabalham com múltiplos tipos sem sacrificar a segurança de tipos.
O problema antes dos Generics
Antes do 1.18, se você quisesse uma função Contains para slices, precisava de uma para cada tipo:
func ContainsInt(s []int, v int) bool {
for _, item := range s {
if item == v {
return true
}
}
return false
}
func ContainsString(s []string, v string) bool {
for _, item := range s {
if item == v {
return true
}
}
return false
}
Ou usar interface{} (agora any) e perder segurança de tipos:
func Contains(s []any, v any) bool {
// Sem type safety, precisa de type assertion
}
Generics: a solução
func Contains[T comparable](s []T, v T) bool {
for _, item := range s {
if item == v {
return true
}
}
return false
}
// Uso
Contains([]int{1, 2, 3}, 2) // true
Contains([]string{"a", "b"}, "c") // false
O [T comparable] é um type parameter — T é o nome do tipo genérico, comparable é a constraint (restrição).
Constraints (restrições)
Constraints definem quais tipos são aceitos:
any (alias para interface{})
func Imprime[T any](valor T) {
fmt.Println(valor)
}
comparable
Tipos que suportam == e !=:
func Igual[T comparable](a, b T) bool {
return a == b
}
Constraints customizadas com interfaces
type Numero interface {
~int | ~int32 | ~int64 | ~float32 | ~float64
}
func Soma[T Numero](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
Soma([]int{1, 2, 3}) // 6
Soma([]float64{1.5, 2.5}) // 4.0
O ~int significa “qualquer tipo cujo tipo subjacente é int” (permite type aliases).
constraints package
import "golang.org/x/exp/constraints"
func Max[T constraints.Ordered](a, b T) T {
if a > b {
return a
}
return b
}
Max(3, 5) // 5
Max("abc", "xyz") // "xyz"
Tipos genéricos
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
var zero T
if len(s.items) == 0 {
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
// Uso
stack := &Stack[string]{}
stack.Push("a")
stack.Push("b")
val, ok := stack.Pop() // "b", true
Map, Filter, Reduce
func Map[T any, R any](s []T, f func(T) R) []R {
result := make([]R, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
func Filter[T any](s []T, f func(T) bool) []T {
var result []T
for _, v := range s {
if f(v) {
result = append(result, v)
}
}
return result
}
func Reduce[T any, R any](s []T, init R, f func(R, T) R) R {
acc := init
for _, v := range s {
acc = f(acc, v)
}
return acc
}
// Uso
nums := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
pares := Filter(nums, func(n int) bool { return n%2 == 0 })
dobros := Map(pares, func(n int) int { return n * 2 })
soma := Reduce(dobros, 0, func(acc, n int) int { return acc + n })
// soma = 60
Sets com Generics
type Set[T comparable] map[T]struct{}
func NewSet[T comparable](items ...T) Set[T] {
s := make(Set[T])
for _, item := range items {
s[item] = struct{}{}
}
return s
}
func (s Set[T]) Add(item T) {
s[item] = struct{}{}
}
func (s Set[T]) Contains(item T) bool {
_, ok := s[item]
return ok
}
func (s Set[T]) Union(other Set[T]) Set[T] {
result := NewSet[T]()
for item := range s {
result.Add(item)
}
for item := range other {
result.Add(item)
}
return result
}
// Uso
a := NewSet(1, 2, 3)
b := NewSet(3, 4, 5)
c := a.Union(b) // {1, 2, 3, 4, 5}
Quando usar Generics
Use quando:
- Funções que trabalham com slices/maps de qualquer tipo
- Estruturas de dados genéricas (Stack, Queue, Set, Tree)
- Funções utilitárias (Map, Filter, Reduce)
Não use quando:
- Uma interface simples resolve o problema
- O código é específico para um tipo
- A legibilidade sofre
Conclusão
Generics é a feature que Go precisava para eliminar o código duplicado sem sacrificar a simplicidade. A sintaxe é limpa, as constraints são expressivas, e a performance é a mesma de código não-genérico (Go monomorphiza os tipos em tempo de compilação).
Comece usando generics em funções utilitárias (Contains, Map, Filter) e estruturas de dados. Evite generics excessivos — a filosofia do Go continua sendo “aclaridade é melhor que cleverness”.
Caso eu tenha falado alguma besteira, por favor, agradecerei correções e sugestões.