Genéricos avançado
Esta aula aprofunda o uso de genéricos em Go, abordando constraints customizadas, a interface comparable, inferência de tipos e as limitações da implementação atual. Você aprenderá a criar funções e tipos genéricos flexíveis e reutilizáveis, com exemplos práticos e exercícios para fixar o conteúdo.
Nesta aula, vamos explorar os recursos avançados de genéricos na linguagem Go. Desde a versão 1.18, Go suporta genéricos, permitindo escrever código que funciona com vários tipos, mantendo a segurança de tipos em tempo de compilação. Vamos aprofundar em constraints customizadas, na interface comparable, na inferência de tipos e nas limitações atuais da implementação. Com isso, você poderá criar funções e tipos mais flexíveis e reutilizáveis, sem perder a simplicidade e a clareza que Go preza.
Dominar genéricos é essencial para escrever bibliotecas reutilizáveis e para evitar duplicação de código. Vamos começar entendendo como definir constraints além das básicas, depois explorar o uso prático de comparable, e então ver como a inferência de tipos facilita a chamada de funções genéricas. Por fim, discutiremos as limitações que ainda existem, para que você possa tomar decisões informadas ao projetar seu código.
Constraints customizadas
Constraints são interfaces que definem o conjunto de tipos que podem ser usados como argumentos de tipo em uma função ou tipo genérico. Além das predefinidas como any e comparable, podemos criar nossas próprias constraints combinando interfaces e tipos subjacentes. Isso nos permite especificar exatamente quais operações são suportadas pelos tipos que serão utilizados.
Uma constraint pode ser definida como uma interface que inclui métodos e também tipos subjacentes. Por exemplo, podemos criar uma constraint Numeric que aceita tipos numéricos, usando a sintaxe de união de tipos. Isso é útil para funções que operam com números, como somas ou médias.
// Definindo uma constraint para tipos numéricos
package main
import "fmt"
// Numeric é uma interface que aceita tipos numéricos.
type Numeric interface {
~int | ~float64 | ~int64 // etc.
}
// Soma retorna a soma de dois valores numéricos.
func Soma[T Numeric](a, b T) T {
return a + b
}
func main() {
fmt.Println(Soma(3, 4)) // 7 (int)
fmt.Println(Soma(3.5, 2.5)) // 6.0 (float64)
}
No exemplo, a constraint Numeric usa o operador ~ para permitir tipos cujo tipo subjacente é int ou float64. Isso significa que qualquer tipo definido como type MeuInt int também será aceito. Podemos também incluir métodos na interface, mas para tipos numéricos não é necessário. Constraints customizadas são poderosas para expressar requisitos específicos do seu domínio.
Outra forma de criar constraints é usando composição de interfaces. Por exemplo, podemos ter uma interface Stringer e uma constraint que exige implementar String() string. Isso permite que funções genéricas chamem métodos definidos nos tipos.
// Exemplo de constraint com método
package main
import "fmt"
type Stringer interface {
String() string
}
// Descreve retorna a descrição de qualquer tipo que implemente Stringer.
func Descreve[T Stringer](v T) string {
return "Descrição: " + v.String()
}
type Pessoa struct {
Nome string
}
func (p Pessoa) String() string {
return p.Nome
}
func main() {
p := Pessoa{Nome: "Alice"}
fmt.Println(Descreve(p))
}
Essa abordagem é útil quando você precisa garantir que o tipo genérico tenha determinados métodos. Lembre-se de que a constraint é verificada em tempo de compilação, garantindo segurança.
comparable
A interface comparable é uma constraint pré-definida que aceita todos os tipos que podem ser comparados com == e !=. Isso inclui tipos básicos (int, string, etc.), ponteiros, canais, e tipos que são comparáveis, como structs cujos campos são todos comparáveis. Ela é essencial para funções que precisam verificar igualdade, como busca em slices ou mapas.
Na prática, comparable é útil em funções como Contains ou Index. Por exemplo, podemos criar uma função genérica que verifica se um elemento está presente em um slice. Antes de Go 1.18, precisaríamos usar interfaces e reflexão, agora podemos fazer de forma segura e eficiente.
// Função genérica que verifica se um elemento está no slice
package main
import "fmt"
// Contains verifica se v está presente em s.
func Contains[T comparable](s []T, v T) bool {
for _, item := range s {
if item == v {
return true
}
}
return false
}
func main() {
nums := []int{1, 2, 3, 4}
fmt.Println(Contains(nums, 3)) // true
nomes := []string{"Alice", "Bob"}
fmt.Println(Contains(nomes, "Carol")) // false
}
O uso de comparable também é comum em estruturas de dados como conjuntos (sets) ou em funções que removem duplicatas. Note que nem todos os tipos são comparáveis; por exemplo, slices e mapas não são, então eles não podem ser usados com comparable. Isso é uma limitação importante a se considerar.
Você também pode usar comparable em constraints customizadas, combinando com outros métodos. Por exemplo, uma constraint que exige comparação e um método String().
Inferência de tipo
Uma das grandes vantagens dos genéricos em Go é a inferência de tipos. Isso significa que, na maioria das chamadas, você não precisa especificar os argumentos de tipo explicitamente, pois o compilador pode deduzi-los a partir dos argumentos da função. Isso torna o código mais limpo e legível.
Por exemplo, na função Soma definida anteriormente, podemos chamar Soma(3, 4) sem especificar [int], pois o compilador infere que os argumentos são int. A inferência funciona também com tipos mais complexos, como slices e mapas, e até com funções que retornam valores genéricos.
// Exemplo de inferência em função com múltiplos parâmetros
package main
import "fmt"
// Map aplica a função f a cada elemento do slice e retorna um novo slice.
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
}
func main() {
nums := []int{1, 2, 3}
// Inferência de T e U a partir dos argumentos
dobrados := Map(nums, func(x int) int { return x * 2 })
fmt.Println(dobrados) // [2 4 6]
// Inferência com tipos diferentes
strings := Map(nums, func(x int) string { return fmt.Sprintf("N%d", x) })
fmt.Println(strings) // [N1 N2 N3]
}
No exemplo, a função Map tem dois parâmetros de tipo: T e U. O compilador infere T a partir do tipo do slice nums e U a partir do tipo de retorno da função passada. Isso permite chamadas concisas sem especificar os tipos manualmente.
A inferência de tipos também funciona em tipos genéricos, como em instâncias de structs e interfaces. Por exemplo, ao criar um Fila[int], podemos simplesmente escrever Fila[int]{} ou até Fila[int]{} se o tipo for inferido pelo contexto. No entanto, em alguns casos, como quando você quer uma variável do tipo genérico, pode ser necessário especificar o tipo explicitamente.
É importante notar que a inferência não funciona quando os argumentos de tipo são usados apenas no retorno, pois não há como inferir. Por exemplo, em uma função como func Novo[T any]() T, você precisa especificar o tipo ao chamar: Novo[int]().
Limitações
Apesar de poderosos, os genéricos em Go têm algumas limitações em comparação com outras linguagens. Uma delas é que não é possível usar operadores aritméticos (como +, -) diretamente em parâmetros de tipo, a menos que a constraint permita explicitamente. No exemplo da função Soma, usamos a constraint Numeric que inclui tipos subjacentes, mas se tentássemos somar um tipo any, o compilador reclamaria.
Outra limitação é que não é possível usar genéricos em métodos, apenas em funções e tipos. Ou seja, você não pode definir métodos genéricos em uma struct. Por exemplo, você não pode ter um método func (s Slice[T]) MapTo[U any](...) . Isso é uma decisão de design do Go, e a recomendação é usar funções genéricas em vez de métodos.
Além disso, a inferência de tipos tem limites em cenários mais complexos, como quando há múltiplos parâmetros de tipo e alguns não podem ser inferidos. Nesses casos, você deve especificar os tipos explicitamente. Também não é possível usar genéricos em funções variádicas (com número variável de argumentos) da mesma forma que funções normais, embora seja possível.
Outra limitação é que tipos genéricos não podem ser usados como tipos de campo em structs sem especificar os parâmetros. Por exemplo, você não pode ter um campo Dados T em uma struct genérica sem antes definir o parâmetro de tipo. Isso é uma restrição natural, mas pode ser contornada usando interfaces.
Por fim, o desempenho dos genéricos é uma preocupação. Em Go, o compilador gera código especializado para cada conjunto de tipos, o que pode aumentar o tamanho do binário. No entanto, isso é semelhante a templates em C++ e geralmente é aceitável. É importante estar ciente dessas limitações ao projetar APIs.
Em resumo, os genéricos em Go são uma ferramenta útil, mas têm regras específicas. Compreender essas limitações ajuda a escrever código mais eficaz e a evitar surpresas.
Boas práticas
Ao usar genéricos, é recomendável começar com necessidades concretas, evitando abstrações prematuras. Use constraints customizadas para expressar claramente os requisitos do tipo, mas não exagere na complexidade. Prefira funções genéricas a métodos genéricos, já que Go não suporta métodos genéricos. Além disso, documente as constraints para que outros desenvolvedores entendam quais tipos são aceitos. Teste suas funções genéricas com diferentes tipos, incluindo tipos definidos pelo usuário, para garantir que funcionem como esperado.
Referências
- Go Blog: An Introduction To Generics
- Go Specification: Type Parameter Declarations
- Go Specification: Constraints
- Go Specification: Comparison Operators
- Package constraints (experimental)
- Go Wiki: GoGenerics
Exercícios
-
Crie uma constraint chamada
Ordenávelque aceite tipos que suportam comparação com<e>. Dica: use a interfaceconstraints.Ordereddo pacote experimental, ou defina uma união de tipos básicos ordenáveis.✓ Resposta: Você pode usar o pacotegolang.org/x/exp/constraintse definir:
Ou manualmente:import "golang.org/x/exp/constraints" type Ordenável[T constraints.Ordered] interface { ~T }type Ordenável interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 | ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~float32 | ~float64 | ~string } -
Escreva uma função genérica
Maxque retorna o maior valor entre dois valores comparáveis (usandocomparablee a constraintOrdenável). Teste com tiposintestring.✓ Resposta:func Max[T constraints.Ordered](a, b T) T { if a > b { return a } return b } -
Explique por que a função abaixo não compila e como corrigi-la:
func Soma[T any](a, b T) T { return a + b }✓ Resposta: O operador+não é definido para o tipoany, pois não há garantia de que o tipo suporte soma. Para corrigir, use uma constraint que restrinja a tipos numéricos, comoNumericdefinida anteriormente. -
Crie um tipo genérico
Parque armazena dois valores de tipos possivelmente diferentes. Implemente um métodoPrimeiro()que retorna o primeiro valor.✓ Resposta:type Par[T, U any] struct { primeiro T segundo U } func (p Par[T, U]) Primeiro() T { return p.primeiro } -
Escreva uma função genérica
Unicoque remove elementos duplicados de um slice usandocomparable. Teste com um slice de strings.✓ Resposta:func Unico[T comparable](s []T) []T { resultado := make([]T, 0, len(s)) visto := make(map[T]bool) for _, v := range s { if !visto[v] { visto[v] = true resultado = append(resultado, v) } } return resultado }