Generics no Go: Casos de Uso e Padrões

Código reutilizável e seguro em termos de tipos com genéricos em Go

Conteúdo da página

Genéricos em Go representam um dos recursos linguísticos mais significativos adicionados desde a versão 1.0. Introduzidos no Go 1.18, os genéricos permitem que você escreva código seguro e 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 uma revisão sobre os fundamentos, confira nossa Lista de Atalhos do Go para construções linguísticas essenciais e sintaxe.

a-lone-programmer

Compreendendo Genéricos no Go

Os genéricos em 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 que a mesma lógica funcione com diferentes tipos, mantendo a segurança de tipos 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 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 é a criação de 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

Genéricos facilitam a escrita de funções reutilizáveis de manipulação de slices:

// 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 de Mapa Genéricos

Trabalhar com mapas torna-se mais seguro em termos de tipos com genéricos:

// Obter chaves do mapa como um 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 do mapa como um 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 do mapa com segurança e 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 Option Genérico

O padrão Option 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 desempacotar 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 tipos 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)

Melhores Práticas

1. Comece Simples

Não use genéricos em excesso. Se uma interface simples ou tipo concreto funcionar, prefira isso para melhor legibilidade:

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

// Excessivamente 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, tenha cuidado com o 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 do Mundo Real

Construtores de Consultas de Banco de Dados

Genéricos são particularmente úteis ao construir construtores de consultas de banco de dados ou trabalhar com ORMs. Ao trabalhar com bancos de dados em Go, você pode achar nossa comparação de ORMs Go para PostgreSQL útil para entender como os genéricos podem melhorar a segurança de tipos 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 criar aplicações de linha de comando ou sistemas de configuração, os genéricos podem ajudar a criar carregadores de configuração seguros em termos de tipos. Se você está 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 manuseio de configurações.

type ConfigLoader[T any] struct {
    path string
}

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

Bibliotecas de Utilitários

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

Código Crítico para Desempenho

Em aplicações sensíveis ao desempenho, como funções serverless, os genéricos podem ajudar a manter a segurança de tipos 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 de desempenho AWS Lambda em JavaScript, Python e Golang demonstra como o desempenho do Go, combinado com genéricos, pode ser vantajoso.

Armadilhas Comuns

1. Restrição Excessiva de Tipos

Evite tornar as restrições muito restritivas quando não precisam ser:

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

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

2. Ignorar 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: retornar valor zero
        return zero, false
    }
    return slice[index], true
}

Conclusão

Os genéricos em Go fornecem uma maneira poderosa de escrever código seguro e reutilizável sem sacrificar desempenho ou 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 tipos em suas aplicações Go.

Lembre-se de usar genéricos com juízo — nem toda situação os exige. Em caso de dúvida, prefira soluções mais simples como interfaces ou tipos concretos. No entanto, quando você precisa trabalhar com múltiplos tipos mantendo a segurança de tipos, 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 um recurso essencial para construir aplicações modernas e seguras em termos de tipos. Seja construindo estruturas de dados, bibliotecas de utilitários ou abstrações complexas, os genéricos podem ajudá-lo a escrever código mais limpo e mantível.

Assinar

Receba novos artigos sobre sistemas, infraestrutura e engenharia de IA.