Generics do Go: Casos de Uso e Padrões

Código reutilizável com segurança de tipos com generics do Go

Conteúdo da página

Genéricos em Go representam uma das características de linguagem mais significativas adicionadas desde o Go 1.0. Introduzidos no Go 1.18, os genéricos permitem que você escreva código seguro de tipo, reutilizável que funciona com múltiplos tipos sem sacrificar o desempenho ou a clareza do código.

Este artigo explora casos de uso práticos, padrões comuns e melhores práticas para aproveitar os genéricos em seus programas Go. Se você é novo no Go ou precisa de um reforço sobre os fundamentos, consulte nossa Folha de Dicas do Go para construções essenciais da linguagem e sintaxe.

a-lone-programmer

Entendendo Genéricos no Go

Os genéricos no Go permitem que você escreva funções e tipos parametrizados por parâmetros de tipo. Isso elimina a necessidade de duplicação de código quando você precisa da mesma lógica para funcionar com diferentes tipos, mantendo a segurança de tipo em tempo de compilação.

Sintaxe Básica

A sintaxe para genéricos usa colchetes para declarar parâmetros de tipo:

// Função genérica
func Max[T comparable](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Uso
maxInt := Max(10, 20)
maxString := Max("apple", "banana")

Restrições de Tipo

As restrições de tipo especificam quais tipos podem ser usados com seu código genérico:

  • any: Qualquer tipo (equivalente a interface{})
  • comparable: Tipos que suportam os operadores == e !=
  • Restrições de interface personalizadas: Defina seus próprios requisitos
// Usando uma restrição personalizada
type Numeric interface {
    int | int8 | int16 | int32 | int64 | 
    uint | uint8 | uint16 | uint32 | uint64 | 
    float32 | float64
}

func Sum[T Numeric](numbers []T) T {
    var sum T
    for _, n := range numbers {
        sum += n
    }
    return sum
}

Casos de Uso Comuns

1. Estruturas de Dados Genéricas

Um dos casos de uso mais convincentes para genéricos é criar estruturas de dados reutilizáveis:

// Pilha genérica
type Stack[T any] struct {
    items []T
}

func NewStack[T any]() *Stack[T] {
    return &Stack[T]{items: make([]T, 0)}
}

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
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

// Uso
intStack := NewStack[int]()
intStack.Push(42)
intStack.Push(100)

stringStack := NewStack[string]()
stringStack.Push("hello")

2. Utilitários de Slice

Os genéricos tornam fácil escrever funções de manipulação de slice reutilizáveis:

// Função Map
func Map[T, U any](slice []T, fn func(T) U) []U {
    result := make([]U, len(slice))
    for i, v := range slice {
        result[i] = fn(v)
    }
    return result
}

// Função Filter
func Filter[T any](slice []T, fn func(T) bool) []T {
    var result []T
    for _, v := range slice {
        if fn(v) {
            result = append(result, v)
        }
    }
    return result
}

// Função Reduce
func Reduce[T, U any](slice []T, initial U, fn func(U, T) U) U {
    result := initial
    for _, v := range slice {
        result = fn(result, v)
    }
    return result
}

// Uso
numbers := []int{1, 2, 3, 4, 5}
doubled := Map(numbers, func(n int) int { return n * 2 })
evens := Filter(numbers, func(n int) bool { return n%2 == 0 })
sum := Reduce(numbers, 0, func(acc, n int) int { return acc + n })

3. Utilitários Genéricos de Mapa

Trabalhar com mapas torna-se mais seguro de tipo com genéricos:

// Obter chaves de mapa como slice
func Keys[K comparable, V any](m map[K]V) []K {
    keys := make([]K, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    return keys
}

// Obter valores de mapa como slice
func Values[K comparable, V any](m map[K]V) []V {
    values := make([]V, 0, len(m))
    for _, v := range m {
        values = append(values, v)
    }
    return values
}

// Obter valor de mapa com valor padrão
func GetOrDefault[K comparable, V any](m map[K]V, key K, defaultValue V) V {
    if v, ok := m[key]; ok {
        return v
    }
    return defaultValue
}

4. Padrão Genérico de Opção

O padrão de opção torna-se mais elegante com genéricos:

type Option[T any] struct {
    value *T
}

func Some[T any](value T) Option[T] {
    return Option[T]{value: &value}
}

func None[T any]() Option[T] {
    return Option[T]{value: nil}
}

func (o Option[T]) IsSome() bool {
    return o.value != nil
}

func (o Option[T]) IsNone() bool {
    return o.value == nil
}

func (o Option[T]) Unwrap() T {
    if o.value == nil {
        panic("tentativa de desembrulhar valor None")
    }
    return *o.value
}

func (o Option[T]) UnwrapOr(defaultValue T) T {
    if o.value == nil {
        return defaultValue
    }
    return *o.value
}

Padrões Avançados

Composição de Restrições

Você pode compor restrições para criar requisitos mais específicos:

type Addable interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64 | string
}

type Multiplicable interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

type Numeric interface {
    Addable
    Multiplicable
}

func Multiply[T Multiplicable](a, b T) T {
    return a * b
}

Interfaces Genéricas

Interfaces também podem ser genéricas, permitindo abstrações poderosas:

type Repository[T any, ID comparable] interface {
    FindByID(id ID) (T, error)
    Save(entity T) error
    Delete(id ID) error
    FindAll() ([]T, error)
}

// Implementação
type InMemoryRepository[T any, ID comparable] struct {
    data map[ID]T
}

func NewInMemoryRepository[T any, ID comparable]() *InMemoryRepository[T, ID] {
    return &InMemoryRepository[T, ID]{
        data: make(map[ID]T),
    }
}

func (r *InMemoryRepository[T, ID]) FindByID(id ID) (T, error) {
    if entity, ok := r.data[id]; ok {
        return entity, nil
    }
    var zero T
    return zero, fmt.Errorf("entidade não encontrada")
}

Inferência de Tipo

A inferência de tipo do Go frequentemente permite que você omita parâmetros de tipo explícitos:

// Inferência de tipo em ação
numbers := []int{1, 2, 3, 4, 5}

// Não é necessário especificar [int] - o Go infere
doubled := Map(numbers, func(n int) int { return n * 2 })

// Parâmetros de tipo explícitos quando necessário
result := Map[int, string](numbers, strconv.Itoa)

Boas Práticas

1. Comece Simples

Não sobreuse genéricos. Se uma interface simples ou um tipo concreto funcionariam, prefira-os para melhor legibilidade:

// Prefira isso para casos simples
func Process(items []Processor) {
    for _, item := range items {
        item.Process()
    }
}

// Muito genérico - evite
func Process[T Processor](items []T) {
    for _, item := range items {
        item.Process()
    }
}

2. Use Nomes de Restrições Significativos

Nomeie suas restrições claramente para comunicar a intenção:

// Bom
type Sortable interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64 | string
}

// Menos claro
type T interface {
    int | string
}

3. Documente Restrições Complexas

Quando as restrições se tornam complexas, adicione documentação:

// Numeric representa tipos que suportam operações aritméticas.
// Isso inclui todos os tipos inteiros e de ponto flutuante.
type Numeric interface {
    int | int8 | int16 | int32 | int64 |
    uint | uint8 | uint16 | uint32 | uint64 |
    float32 | float64
}

4. Considere o Desempenho

Genéricos são compilados para tipos concretos, então não há sobrecarga de tempo de execução. No entanto, esteja atento ao tamanho do código se você instanciar muitas combinações de tipos:

// Cada combinação cria código separado
var intStack = NewStack[int]()
var stringStack = NewStack[string]()
var floatStack = NewStack[float64]()

Aplicações em Ambiente Real

Construtores de Consultas de Banco de Dados

Genéricos são particularmente úteis ao construir construtores de consulta de banco de dados ou ao trabalhar com ORMs. Ao trabalhar com bancos de dados no Go, você pode encontrar útil nossa comparação de ORMs do Go para PostgreSQL para entender como os genéricos podem melhorar a segurança de tipo em operações de banco de dados.

type QueryBuilder[T any] struct {
    table string
    where []string
}

func (qb *QueryBuilder[T]) Where(condition string) *QueryBuilder[T] {
    qb.where = append(qb.where, condition)
    return qb
}

func (qb *QueryBuilder[T]) Find() ([]T, error) {
    // Implementação
    return nil, nil
}

Gerenciamento de Configuração

Ao construir aplicações CLI ou sistemas de configuração, os genéricos podem ajudar a criar carregadores de configuração seguros de tipo. Se você estiver construindo ferramentas de linha de comando, nosso guia sobre construção de aplicações CLI com Cobra & Viper demonstra como os genéricos podem melhorar o tratamento de configuração.

type ConfigLoader[T any] struct {
    path string
}

func (cl *ConfigLoader[T]) Load() (T, error) {
    var config T
    // Carregar e deserializar configuração
    return config, nil
}

Bibliotecas de Utilidade

Genéricos brilham ao criar bibliotecas de utilidade que funcionam com vários tipos. Por exemplo, ao gerar relatórios ou trabalhar com diferentes formatos de dados, os genéricos podem fornecer segurança de tipo. Nosso artigo sobre geração de relatórios PDF no Go mostra como os genéricos podem ser aplicados a utilitários de geração de relatórios.

Código Crítico de Desempenho

Em aplicações sensíveis a desempenho, como funções serverless, os genéricos podem ajudar a manter a segurança de tipo sem sobrecarga de tempo de execução. Ao considerar escolhas de linguagem para aplicações serverless, entender as características de desempenho é crucial. Nossa análise sobre desempenho do AWS Lambda em JavaScript, Python e Golang demonstra como o desempenho do Go, combinado com genéricos, pode ser vantajoso.

Armadilhas Comuns

1. Restrições de Tipo Excessivamente Rígidas

Evite fazer restrições muito restritivas quando não forem necessárias:

// Muito restritivo
func Process[T int | string](items []T) { }

// Melhor - mais flexível
func Process[T comparable](items []T) { }

2. Ignorar a Inferência de Tipo

Deixe o Go inferir tipos quando possível:

// Tipos explícitos desnecessários
result := Max[int](10, 20)

// Melhor - deixe o Go inferir
result := Max(10, 20)

3. Esquecer Valores Zero

Lembre-se de que tipos genéricos têm valores zero:

func Get[T any](slice []T, index int) (T, bool) {
    if index < 0 || index >= len(slice) {
        var zero T  // Importante: retorne o valor zero
        return zero, false
    }
    return slice[index], true
}

Conclusão

Genéricos no Go oferecem uma maneira poderosa de escrever código seguro de tipo, reutilizável sem sacrificar o desempenho ou a legibilidade. Ao entender a sintaxe, restrições e padrões comuns, você pode aproveitar os genéricos para reduzir a duplicação de código e melhorar a segurança de tipo em suas aplicações Go.

Lembre-se de usar genéricos com discernimento — não toda situação exige-os. Quando em dúvida, prefira soluções mais simples, como interfaces ou tipos concretos. No entanto, quando você precisa trabalhar com múltiplos tipos enquanto mantém a segurança de tipo, os genéricos são uma excelente ferramenta em seu kit de ferramentas Go.

À medida que o Go continua a evoluir, os genéricos estão se tornando uma característica essencial para construir aplicações modernas e seguras de tipo. Seja ao construir estruturas de dados, bibliotecas de utilidade ou abstrações complexas, os genéricos podem ajudá-lo a escrever código mais limpo e mais mantível.