Golang: Generics (Parte 13)
Golang: Generics (Parte 13)
1. Introdução: O que são Generics e por que são úteis em Go?
Generics, ou programação genérica, permitem que você escreva funções e tipos que operam com tipos de dados arbitrários, em vez de tipos específicos. Antes da introdução dos generics no Go 1.18, a linguagem dependia de interfaces vazias (interface{}) e reflexão para alcançar um comportamento polimórfico, o que frequentemente resultava em código menos seguro em termos de tipo, mais verboso e com desempenho inferior devido à necessidade de asserções de tipo e conversões em tempo de execução.
Os generics resolvem esses problemas, permitindo que os desenvolvedores escrevam código mais reutilizável, seguro em termos de tipo e eficiente. Eles permitem que você defina algoritmos e estruturas de dados que funcionam com qualquer tipo, desde que esse tipo satisfaça certas restrições.
2. Parâmetros de Tipo: Como definir funções e tipos com parâmetros de tipo
Para usar generics, você define parâmetros de tipo em funções ou tipos. Os parâmetros de tipo são listados entre colchetes [] após o nome da função ou tipo.
Exemplo de Função Genérica:
package main
import "fmt"
// PrintSlice imprime todos os elementos de um slice de qualquer tipo.
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Print(v, " ")
}
fmt.Println()
}
func main() {
PrintSlice([]int{1, 2, 3})
PrintSlice([]string{"hello", "world"})
PrintSlice([]float64{3.14, 2.71})
}
Neste exemplo, [T any] declara um parâmetro de tipo T que pode ser qualquer tipo (devido à restrição any). A função PrintSlice pode agora operar em slices de inteiros, strings, floats, ou qualquer outro tipo.
3. Restrições de Tipo: Entendendo interfaces como restrições, incluindo comparable e restrições personalizadas
Parâmetros de tipo podem ter restrições, que são interfaces que definem o conjunto de operações permitidas para o tipo. Isso garante que o código genérico só possa ser usado com tipos que suportam as operações necessárias.
A Restrição any:
any é um alias para interface{}, significando que o parâmetro de tipo pode ser de qualquer tipo. É a restrição mais permissiva.
A Restrição comparable:
comparable é uma interface pré-declarada que permite o uso de operadores de comparação (== e !=) em tipos. Isso é útil para funções que precisam comparar valores, como encontrar um elemento em um slice.
package main
import "fmt"
// Index retorna o índice da primeira ocorrência de x em s, ou -1 se não encontrado.
func Index[T comparable](s []T, x T) int {
for i, v := range s {
if v == x {
return i
}
}
return -1
}
func main() {
fmt.Println(Index([]int{1, 2, 3}, 2)) // Saída: 1
fmt.Println(Index([]string{"a", "b", "c"}, "d")) // Saída: -1
}
Restrições Personalizadas:
Você pode definir suas próprias interfaces para usar como restrições de tipo. Isso é poderoso para definir comportamentos específicos que os tipos devem ter.
package main
import "fmt"
type Stringer interface {
String() string
}
// Concatena elementos que implementam a interface Stringer.
func Concat[T Stringer](s []T) string {
out := ""
for _, v := range s {
out += v.String()
}
return out
}
type MyInt int
func (m MyInt) String() string {
return fmt.Sprintf("MyInt(%d)", m)
}
func main() {
nums := []MyInt{1, 2, 3}
fmt.Println(Concat(nums)) // Saída: MyInt(1)MyInt(2)MyInt(3)
}
4. Funções Genéricas: Prática de escrita
Funções genéricas são a aplicação mais comum de generics. Elas permitem que você escreva algoritmos que funcionam independentemente do tipo de dados que estão processando.
Exemplo: Função Map
package main
import "fmt"
// Map aplica uma função a cada elemento de um slice e retorna um novo slice com os resultados.
func Map[T, U any](s []T, f func(T) U) []U {
res := make([]U, len(s))
for i, v := range s {
res[i] = f(v)
}
return res
}
func main() {
nums := []int{1, 2, 3, 4}
doubled := Map(nums, func(n int) int { return n * 2 })
fmt.Println(doubled) // Saída: [2 4 6 8]
words := []string{"hello", "world"}
upper := Map(words, func(s string) string { return s + "!" })
fmt.Println(upper) // Saída: [hello! world!]
}
5. Tipos Genéricos: Definindo structs e interfaces genéricas
Além de funções, você também pode definir tipos genéricos, como structs e interfaces.
Struct Genérica:
package main
import "fmt"
// Stack é uma pilha genérica.
type Stack[T any] struct {
data []T
}
// Push adiciona um elemento à pilha.
func (s *Stack[T]) Push(item T) {
s.data = append(s.data, item)
}
// Pop remove e retorna o elemento do topo da pilha.
func (s *Stack[T]) Pop() (T, bool) {
if len(s.data) == 0 {
var zero T // Retorna o valor zero do tipo T
return zero, false
}
lastIndex := len(s.data) - 1
item := s.data[lastIndex]
s.data = s.data[:lastIndex]
return item, true
}
func main() {
intStack := Stack[int]{}
intStack.Push(10)
intStack.Push(20)
fmt.Println(intStack.Pop()) // Saída: 20 true
fmt.Println(intStack.Pop()) // Saída: 10 true
fmt.Println(intStack.Pop()) // Saída: 0 false
stringStack := Stack[string]{}
stringStack.Push("Go")
stringStack.Push("Generics")
fmt.Println(stringStack.Pop()) // Saída: Generics true
}
Interface Genérica:
Interfaces genéricas são úteis para definir contratos que operam com tipos genéricos.
package main
import "fmt"
type Container[T any] interface {
Add(T)
Get() T
}
// MyContainer implementa a interface Container para um tipo específico.
type MyContainer[T any] struct {
value T
}
func (mc *MyContainer[T]) Add(item T) {
mc.value = item
}
func (mc *MyContainer[T]) Get() T {
return mc.value
}
func main() {
var intContainer Container[int] = &MyContainer[int]{}
intContainer.Add(42)
fmt.Println(intContainer.Get()) // Saída: 42
var stringContainer Container[string] = &MyContainer[string]{}
stringContainer.Add("Hello Generics")
fmt.Println(stringContainer.Get()) // Saída: Hello Generics
}
6. Casos de Uso Comuns: Cenários onde generics se destacam
Generics são particularmente úteis em cenários onde você precisa de código que opere de forma consistente em diferentes tipos, sem sacrificar a segurança de tipo ou o desempenho:
- Estruturas de Dados: Implementações de listas, pilhas, filas, árvores, etc., que podem armazenar qualquer tipo de elemento.
- Algoritmos: Funções de busca, ordenação, filtragem, mapeamento que podem ser aplicadas a coleções de qualquer tipo.
- Coleções: Funções utilitárias para manipular slices, maps e outras coleções.
- Operações de I/O: Funções que leem ou escrevem dados de/para diferentes fontes, onde o tipo de dado lido/escrito pode variar.
7. Melhores Práticas e Considerações: Dicas para uso eficaz e potenciais armadilhas
- Use com Moderação: Embora poderosos, generics não devem ser usados em excesso. Se uma função ou tipo pode ser escrito de forma clara e eficiente sem generics, prefira a simplicidade.
- Restrições Claras: Sempre que possível, use restrições de tipo específicas em vez de
any. Isso torna o código mais seguro em termos de tipo e mais fácil de entender, pois as operações permitidas são explícitas. - Legibilidade: Certifique-se de que o uso de generics não torne seu código mais difícil de ler ou depurar. Nomes de parâmetros de tipo devem ser concisos e descritivos (por exemplo,
Tpara Tipo,Kpara Chave,Vpara Valor). - Desempenho: Em geral, o compilador Go otimiza o código genérico para ter um desempenho comparável ao código não genérico. No entanto, em casos muito específicos, o uso excessivo de reflexão dentro de funções genéricas pode ter um impacto.
- Compatibilidade: Lembre-se que generics foram introduzidos no Go 1.18. Certifique-se de que seu ambiente de desenvolvimento e implantação suporta esta versão ou superior.
Generics são uma adição valiosa ao Go, permitindo a criação de bibliotecas mais robustas e reutilizáveis, e simplificando muitos padrões de código que antes eram complexos ou inseguros em termos de tipo. Ao entender seus fundamentos e aplicá-los criteriosamente, você pode escrever código Go mais eficaz e elegante.