O que são Generics em Go?

Generics (ou tipos genéricos) são um recurso introduzido no Go 1.18 que permite escrever funções e tipos que funcionam com qualquer type que satisfaça determinadas restrições. Antes do Go 1.18, a única forma de escrever código “genérico” era usando interfaces vazias (interface{}/any) e fazer type assertions em tempo de execução — perdendo toda a segurança de tipos em tempo de compilação.

Com generics, você define type parameters (parâmetros de tipo) entre colchetes [] na assinatura de funções ou definições de tipos, junto com constraints (restrições) que especificam quais tipos são aceitos. O compilador verifica a conformidade em tempo de compilação, mantendo a segurança de tipos que é uma marca registrada de Go.

A adição de generics foi a maior mudança na linguagem desde sua criação. Foi resultado de mais de uma década de discussão e design, liderada por Ian Lance Taylor e Robert Griesemer, com o objetivo de manter a simplicidade e legibilidade do Go enquanto se oferecia essa funcionalidade poderosa.

Funções genéricas

Uma func genérica declara type parameters entre colchetes antes dos parâmetros normais:

package main

import "fmt"

// Min retorna o menor entre dois valores de qualquer tipo ordenável
func Min[T cmp.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

func main() {
    fmt.Println(Min(3, 7))         // int: 3
    fmt.Println(Min(3.14, 2.71))   // float64: 2.71
    fmt.Println(Min("go", "java")) // string: "go"
}

Nesse exemplo, T é o type parameter e cmp.Ordered é a constraint. O compilador infere o tipo concreto de T automaticamente a partir dos argumentos — não é necessário especificar explicitamente na maioria dos casos.

Inferência de tipo

Go realiza type inference quando possível, permitindo chamar funções genéricas sem especificar os type parameters explicitamente:

// Ambas as chamadas são equivalentes
resultado := Min[int](3, 7)  // tipo explícito
resultado := Min(3, 7)       // tipo inferido

Constraints (restrições de tipo)

Constraints definem quais tipos são aceitos por um type parameter. Go oferece constraints predefinidas e permite criar constraints customizadas usando interfaces:

Constraints predefinidas

import "cmp"

// any — qualquer tipo (equivalente a interface{})
func Print[T any](v T) { fmt.Println(v) }

// comparable — tipos que suportam == e !=
func Contains[T comparable](slice []T, item T) bool {
    for _, v := range slice {
        if v == item {
            return true
        }
    }
    return false
}

// cmp.Ordered — tipos que suportam <, >, <=, >=
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

Constraints customizadas

Você cria constraints personalizadas definindo interfaces com union types:

// Constraint que aceita apenas tipos numéricos
type Number interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
    ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 |
    ~float32 | ~float64
}

func Soma[T Number](valores []T) T {
    var total T
    for _, v := range valores {
        total += v
    }
    return total
}

O operador ~ (til) indica que tipos derivados (type aliases) também são aceitos. Sem o ~, apenas o tipo exato seria permitido:

type Celsius float64

// Sem ~: Celsius NÃO seria aceito como float64
// Com ~float64: Celsius É aceito porque o tipo subjacente é float64

Tipos genéricos

Além de funções, você pode criar structs, interfaces e outros tipos com type parameters:

Struct genérico

// Pilha genérica usando slice
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
    }
    item := s.items[len(s.items)-1]
    s.items = s.items[:len(s.items)-1]
    return item, true
}

func (s *Stack[T]) Len() int {
    return len(s.items)
}

Mapa genérico com valores ordenados

type OrderedMap[K cmp.Ordered, V any] struct {
    keys   []K
    values map[K]V
}

func NewOrderedMap[K cmp.Ordered, V any]() *OrderedMap[K, V] {
    return &OrderedMap[K, V]{
        values: make(map[K]V),
    }
}

func (m *OrderedMap[K, V]) Set(key K, value V) {
    if _, exists := m.values[key]; !exists {
        m.keys = append(m.keys, key)
    }
    m.values[key] = value
}

Packages slices e maps

O Go 1.21+ trouxe os packages slices e maps na biblioteca padrão, que usam generics extensivamente. São a implementação canônica de operações comuns:

import (
    "fmt"
    "slices"
    "maps"
)

func main() {
    // slices
    nums := []int{3, 1, 4, 1, 5, 9}
    slices.Sort(nums)
    fmt.Println(nums) // [1 1 3 4 5 9]

    idx := slices.Index(nums, 4)
    fmt.Println("Índice de 4:", idx) // 3

    has := slices.Contains(nums, 7)
    fmt.Println("Contém 7:", has) // false

    // maps
    m := map[string]int{"go": 1, "python": 2, "rust": 3}
    keys := maps.Keys(m)
    fmt.Println("Chaves:", keys)

    clone := maps.Clone(m)
    fmt.Println("Clone:", clone)
}

Esses packages eliminam a necessidade de reescrever funções utilitárias de slice e map em cada projeto.

Generics vs Interfaces: quando usar cada um

A escolha entre generics e interfaces é uma decisão importante de design:

CenárioUse GenericsUse Interfaces
Coleções tipadas (Stack, Queue, List)SimNão
Funções utilitárias (Sort, Filter, Map)SimNão
Polimorfismo em tempo de execuçãoNãoSim
Injeção de dependênciaNãoSim
Evitar reflection e type assertionsSimPode precisar
API pública de bibliotecaDependeSim

A regra geral é: use generics quando o tipo importa para a implementação (operações dependem do tipo), e use interfaces quando o comportamento importa (você quer definir um contrato).

Padrões avançados com generics

Função genérica com múltiplas constraints

func Transform[T any, R any](input []T, fn func(T) R) []R {
    result := make([]R, len(input))
    for i, v := range input {
        result[i] = fn(v)
    }
    return result
}

// Uso
nomes := []string{"go", "rust", "python"}
tamanhos := Transform(nomes, func(s string) int {
    return len(s)
})
fmt.Println(tamanhos) // [2 4 6]

Result type genérico

type Result[T any] struct {
    Value T
    Err   error
}

func NewResult[T any](val T, err error) Result[T] {
    return Result[T]{Value: val, Err: err}
}

func (r Result[T]) Unwrap() (T, error) {
    return r.Value, r.Err
}

Esse padrão é útil para encapsular o resultado de operações que podem falhar, semelhante ao Result do Rust, mantendo a abordagem idiomática de tratamento de erros do Go.

Boas práticas com generics

Para usar generics de forma eficaz em projetos Go:

  • Comece sem generics — adicione apenas quando houver duplicação clara de código para tipos diferentes
  • Prefira constraints específicas sobre any — constraints mais restritivas geram código mais seguro
  • Use benchmarks para verificar se generics impactam a performance comparado a implementações concretas
  • Não abuse — código genérico excessivo dificulta a leitura, especialmente para quem está aprendendo Go
  • Siga as convenções — use T para um tipo, K, V para chave/valor, E para elementos
  • Escreva testes com múltiplos tipos para validar que sua implementação genérica funciona corretamente com todos os tipos esperados

Para um guia aprofundado sobre generics com exemplos práticos, confira nosso artigo Generics em Go: Guia Prático.

Perguntas frequentes sobre Generics em Go

Generics afetam a performance em Go?

O impacto na performance é mínimo na maioria dos casos. O compilador Go usa uma abordagem híbrida: GC shape stenciling com dictionaries. Tipos com o mesmo “GC shape” (como todos os pointer types) compartilham a mesma instanciação de código, reduzindo o inchaço do binário. Para operações intensivas, faça benchmarks comparando implementações genéricas vs concretas.

Posso usar generics com métodos de struct?

Sim, mas com uma limitação: métodos não podem declarar novos type parameters — eles só podem usar os type parameters já definidos no tipo. Isso significa que você não pode ter um método genérico em um struct não-genérico.

Qual a diferença entre any e comparable?

any é um alias para interface{} e aceita literalmente qualquer tipo. comparable é mais restritivo: aceita apenas tipos que suportam operadores == e != (números, strings, booleans, arrays, structs com campos comparable). Slices, maps e funções não são comparable.

Quando devo criar constraints customizadas?

Crie constraints customizadas quando as predefinidas (any, comparable, cmp.Ordered) não expressam com precisão os tipos que você quer aceitar. Por exemplo, se sua função precisa de operações aritméticas, crie uma constraint Number que lista os tipos numéricos. Organize constraints reutilizáveis em um package separado do projeto.