Coleções genéricas
Esta aula explora coleções genéricas em Go, mostrando como implementar estruturas de dados reutilizáveis e seguras de tipo usando generics. Você aprenderá a criar slices e maps genéricos, entenderá o reuso de código e verá exemplos práticos de aplicação.
Nesta aula, vamos mergulhar no mundo das coleções genéricas em Go. Desde a versão 1.18, Go suporta generics, permitindo que você escreva funções e estruturas de dados que trabalham com qualquer tipo, sem abrir mão da segurança de tipos. Vamos explorar como implementar estruturas genéricas, como slices e maps, e como isso promove o reuso de código.
As coleções genéricas são fundamentais para criar bibliotecas e algoritmos que funcionam com diversos tipos, evitando duplicação de código e mantendo a clareza. Veremos na prática como definir tipos genéricos, usar restrições (constraints) e aproveitar os pacotes padrão que já oferecem suporte a genéricos.
Implementando estruturas genéricas
Para criar uma estrutura genérica, usamos a sintaxe de parâmetros de tipo entre colchetes. Por exemplo, uma pilha (stack) genérica pode ser definida assim:
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) {
if len(s.items) == 0 {
var zero T
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}O parâmetro T é um tipo genérico que pode ser qualquer tipo. A restrição any é um alias para interface{}, permitindo qualquer tipo. Isso nos permite usar a mesma pilha para inteiros, strings, structs, etc.
Podemos instanciar a pilha com diferentes tipos:
func main() {
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
fmt.Println(intStack.Pop()) // 2 true
stringStack := Stack[string]{}
stringStack.Push("hello")
fmt.Println(stringStack.Pop()) // hello true
}Além de any, podemos definir restrições mais específicas. Por exemplo, para uma função que soma elementos de um slice, precisamos de tipos que suportem + e -. A interface Number pode ser definida com tipos numéricos:
type Number interface {
int | int8 | int16 | int32 | int64 | uint | uint8 | uint16 | uint32 | uint64 | float32 | float64
}
func Sum[T Number](numbers []T) T {
var total T
for _, n := range numbers {
total += n
}
return total
}Com isso, podemos chamar Sum com um slice de inteiros ou floats, e o compilador garante que apenas tipos numéricos sejam usados.
slices e maps (pacotes)
O pacote slices foi adicionado na versão 1.21 e oferece funções genéricas para operações comuns em slices, como Contains, Index, Sort e Filter. Exemplo:
import "slices"
func main() {
numbers := []int{3, 1, 4, 1, 5}
slices.Sort(numbers)
fmt.Println(numbers) // [1 1 3 4 5]
fmt.Println(slices.Contains(numbers, 4)) // true
}O pacote maps oferece funções genéricas para mapas, como Keys, Values e Clone:
import "maps"
func main() {
m := map[string]int{"a": 1, "b": 2}
keys := maps.Keys(m)
fmt.Println(keys) // [a b]
clone := maps.Clone(m)
clone["c"] = 3
fmt.Println(m) // map[a:1 b:2]
fmt.Println(clone) // map[a:1 b:2 c:3]
}Esses pacotes eliminam a necessidade de escrever funções repetitivas para cada tipo de slice ou map, promovendo coesão e eficiência.
Reuso
Com generics, escrevemos código uma vez e reutilizamos para muitos tipos, reduzindo duplicação e erros. Por exemplo, uma função de filtro genérica pode ser usada para filtrar qualquer slice:
func Filter[T any](items []T, predicate func(T) bool) []T {
result := make([]T, 0)
for _, item := range items {
if predicate(item) {
result = append(result, item)
}
}
return result
}
func main() {
numbers := []int{1, 2, 3, 4, 5}
even := Filter(numbers, func(n int) bool { return n%2 == 0 })
fmt.Println(even) // [2 4]
words := []string{"apple", "banana", "cherry"}
long := Filter(words, func(s string) bool { return len(s) > 5 })
fmt.Println(long) // [banana cherry]
}Isso é muito mais eficiente do que criar versões específicas para cada tipo. Além disso, a segurança de tipos é mantida: se você tentar usar uma função de comparação que não aceita o tipo, o compilador acusa erro.
Exemplos
Vamos consolidar com um exemplo completo: uma lista simples genérica com métodos básicos.
package main
import (
"fmt"
"slices"
)
type List[T comparable] struct {
items []T
}
func (l *List[T]) Add(item T) {
l.items = append(l.items, item)
}
func (l *List[T]) Remove(item T) bool {
idx := slices.Index(l.items, item)
if idx == -1 {
return false
}
l.items = append(l.items[:idx], l.items[idx+1:]...)
return true
}
func (l *List[T]) Contains(item T) bool {
return slices.Contains(l.items, item)
}
func (l *List[T]) Len() int {
return len(l.items)
}
func main() {
list := List[string]{}
list.Add("Go")
list.Add("Rust")
list.Add("Python")
fmt.Println(list.Contains("Go")) // true
fmt.Println(list.Remove("Rust")) // true
fmt.Println(list.Len()) // 2
}Note que usamos comparable para que slices.Index funcione, pois ele exige comparação de igualdade. Isso demonstra como combinar restrições e pacotes genéricos.
Boas práticas
1. Use any apenas quando realmente não houver restrições; caso contrário, defina interfaces específicas.
2. Prefira funções genéricas a duplicar código para cada tipo.
3. Aproveite os pacotes slices e maps para operações comuns.
4. Lembre-se de que generics têm custo de performance mínimo, mas podem aumentar o tempo de compilação.
5. Documente suas restrições para que outros desenvolvedores entendam quais tipos são aceitos.
Referências
- Introdução a Generics no blog oficial da Go
- Documentação do pacote slices
- Documentação do pacote maps
- Especificação da linguagem: declarações de parâmetros de tipo
- Tutorial oficial: Generics em Go
- Go Playground para testar código
Exercícios
Implemente uma função genérica
Mapque recebe um slice deTe uma função que converteTemU, retornando um slice deU.✓ Resposta:func Map[T, U any](s []T, f func(T) U) []U { result := make([]U, len(s)) for i, v := range s { result[i] = f(v) } return result }Crie uma função genérica
Reduceque acumula os valores de um slice usando uma função de acumulação.✓ Resposta:func Reduce[T, U any](s []T, initial U, f func(U, T) U) U { acc := initial for _, v := range s { acc = f(acc, v) } return acc }Implemente um tipo
Set[T comparable]que armazena elementos únicos, usando um mapa interno.✓ Resposta:type Set[T comparable] struct { m map[T]struct{} } func NewSet[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]) Remove(v T) { delete(s.m, v) } func (s *Set[T]) Contains(v T) bool { _, ok := s.m[v] return ok }Use o pacote
slicespara escrever uma função que retorna os elementos únicos de um slice, mantendo a ordem original.✓ Resposta:func Unique[T comparable](s []T) []T { seen := make(map[T]bool) var result []T for _, v := range s { if !seen[v] { seen[v] = true result = append(result, v) } } return result }Escreva um exemplo de uso de uma função genérica que ordena um slice de qualquer tipo ordenável (use
constraints.Ordered).✓ Resposta:import "sort" type Ordered interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 | ~string } func SortSlice[T Ordered](s []T) { sort.Slice(s, func(i, j int) bool { return s[i] < s[j] }) }