search

Go 1.18+ em prática: Type Parameters, Constraints e Estruturas de Dados Genéricas

Go 1.18+ em prática: Type Parameters, Constraints e Estruturas de Dados Genéricas

Go 1.18 marcou uma mudança estrutural no ecossistema GoLang ao introduzir type parameters (parâmetros de tipo), conhecidos popularmente como generics. O objetivo foi resolver um problema recorrente em bases de código Go: a duplicação de implementações para diferentes tipos (por exemplo, int, float64, string) e o uso excessivo de interface{} com type assertions, que reduz segurança de tipos e aumenta o risco de erros em tempo de execução.

Neste artigo, você vai entender, de forma aplicada, três pilares do recurso no Go 1.18+:

  1. Type Parameters: como declarar funções e tipos parametrizados.
  2. Constraints: como limitar quais tipos podem ser usados.
  3. Estruturas de Dados Genéricas: como implementar coleções reutilizáveis e seguras.
Generics GoLang

1) O que mudou no GoLang com generics

Antes do Go 1.18, havia duas alternativas principais para reutilização de código com múltiplos tipos:

  • Duplicar funções (MinInt, MinFloat, etc.), elevando custo de manutenção.
  • Usar interface{} e converter o tipo depois, com risco de panic caso a conversão esteja errada.

Com generics, GoLang passou a permitir que funções e tipos sejam escritos uma única vez e usados com vários tipos, mantendo checagem de tipo em tempo de compilação.

2) Type Parameters: a base de tudo

2.1 Funções genéricas

Uma função genérica declara parâmetros de tipo entre colchetes [] logo após o nome da função.

Exemplo: função que retorna o primeiro elemento de um slice, funcionando para qualquer tipo:

package main

import "fmt"

func First[T any](s []T) (T, bool) {
	var zero T
	if len(s) == 0 {
		return zero, false
	}
	return s[0], true
}

func main() {
	v1, ok1 := First([]int{10, 20})
	v2, ok2 := First([]string{"go", "lang"})

	fmt.Println(v1, ok1)
	fmt.Println(v2, ok2)
}

Pontos importantes:

  • T é um parâmetro de tipo.
  • any é um alias de interface{} e significa “qualquer tipo”.
  • var zero T é o valor zero do tipo T (0 para números, "" para string, nil para ponteiros, etc.).
  • O compilador verifica o uso correto de T.

2.2 Inferência de tipo

Em muitos casos, o GoLang infere o tipo T a partir dos argumentos:

_ = First([]int{1, 2, 3})      // T = int
_ = First([]string{"a", "b"})  // T = string

Também é possível explicitar:

_ = First[int]([]int{1, 2, 3})

A inferência tende a reduzir verbosidade, mas explicitar pode ajudar em cenários ambíguos.

3) Constraints: limitando o que T pode ser

Constraints com Interfaces GoLang

3.1 Por que constraints existem

O parâmetro T any permite qualquer tipo, mas isso limita operações. Por exemplo, você não pode usar > em T sem garantir que T seja ordenável.

Exemplo incorreto (não compila):

func Max[T any](a, b T) T {
	if a > b { // erro: operador > não definido para "any"
		return a
	}
	return b
}

Para permitir certas operações, usamos constraints, que descrevem um conjunto permitido de tipos.

3.2 Constraints com interfaces

Em GoLang, constraints são escritas como interfaces. Uma interface de constraint pode:

  • exigir métodos (como interfaces tradicionais)
  • ou definir um type set (conjunto de tipos permitido), usando união com | e o operador ~

Exemplo: suportar tipos numéricos específicos:

type Number interface {
	int | int64 | float64
}

func Sum[T Number](a, b T) T {
	return a + b
}

Agora T está limitado a int, int64 ou float64, e + passa a ser válido.

3.3 O operador ~ (aproximação por tipo subjacente)

O operador ~ permite aceitar tipos definidos pelo usuário cujo tipo subjacente é um tipo base.

Exemplo:

type MyInt int

type Integer interface {
	~int | ~int64
}

func Inc[T Integer](v T) T {
	return v + 1
}

func main() {
	var x MyInt = 10
	_ = Inc(x) // compila porque MyInt tem tipo subjacente int
}

Sem ~int, MyInt não seria aceito quando a constraint fosse apenas int.

3.4 Reuso de constraints prontas (constraints)

Há o pacote golang.org/x/exp/constraints (e discussões históricas sobre padronização). Em projetos, ele é frequentemente usado para constraints como constraints.Ordered (tipos ordenáveis). Exemplo:

import "golang.org/x/exp/constraints"

func Min[T constraints.Ordered](a, b T) T {
	if a < b {
		return a
	}
	return b
}

Em ambientes corporativos, vale avaliar política de dependências externas: x/exp é experimental e pode ter mudanças. Quando estabilidade for requisito, uma alternativa é definir constraints internas (com type sets) para o conjunto de tipos de interesse do projeto.

4) Estruturas de dados genéricas: como criar e usar

Generics em GoLang não se limitam a funções; também se aplicam a tipos (structs, slices customizados, mapas encapsulados, etc.). Isso facilita criar coleções reutilizáveis sem perder segurança.

4.1 Uma Stack (pilha) genérica

Implementação clássica com slice:

package stack

type Stack[T any] struct {
	items []T
}

func (s *Stack[T]) Push(v T) {
	s.items = append(s.items, v)
}

func (s *Stack[T]) Pop() (T, bool) {
	var zero T
	if len(s.items) == 0 {
		return zero, false
	}
	last := len(s.items) - 1
	v := s.items[last]
	s.items = s.items[:last]
	return v, true
}

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

Uso:

s := stack.Stack[string]{}
s.Push("a")
s.Push("b")
v, ok := s.Pop()

Benefícios práticos:

  • sem interface{} e sem type assertions
  • API clara: Stack[string] deixa explícito o tipo
  • erros aparecem na compilação, não em runtime

4.2 Um Set genérico com map[T]struct{}

Em GoLang, sets são normalmente implementados com mapa. Com generics:

package set

type Set[T comparable] struct {
	m map[T]struct{}
}

func New[T comparable]() Set[T] {
	return Set[T]{m: make(map[T]struct{})}
}

func (s Set[T]) Add(v T) {
	s.m[v] = struct{}{}
}

func (s Set[T]) Has(v T) bool {
	_, ok := s.m[v]
	return ok
}

func (s Set[T]) Remove(v T) {
	delete(s.m, v)
}

func (s Set[T]) Len() int {
	return len(s.m)
}

A constraint comparable é essencial, pois chaves de map em Go precisam ser comparáveis.

Uso:

users := set.New[int]()
users.Add(10)
_ = users.Has(10)

4.3 Uma lista encadeada simples (exemplo didático)

Listas encadeadas são úteis para ilustrar tipos recursivos com generics:

package list

type Node[T any] struct {
	Value T
	Next  *Node[T]
}

type List[T any] struct {
	Head *Node[T]
}

func (l *List[T]) Prepend(v T) {
	l.Head = &Node[T]{Value: v, Next: l.Head}
}

func (l *List[T]) ToSlice() []T {
	var out []T
	for n := l.Head; n != nil; n = n.Next {
		out = append(out, n.Value)
	}
	return out
}

Esse padrão reduz duplicação para List[int], List[string], List[User] etc.

5) Boas práticas: quando usar (e quando evitar) generics no GoLang

5.1 Use generics quando houver reutilização real

Generics fazem sentido quando:

  • a mesma lógica se repete para múltiplos tipos
  • você quer construir coleções reutilizáveis (stack, set, pool, cache)
  • o uso de interface{} está forçando conversões inseguras

Evite quando:

  • só existe um tipo concreto no projeto (generic vira custo sem benefício)
  • a API fica difícil de entender (legibilidade é prioridade no GoLang)
  • há risco de constraints muito amplas (“any em todo lugar”) que mascaram design fraco

5.2 Prefira constraints mínimas e explícitas

Quanto mais ampla a constraint, maior a chance de usos indevidos. Exemplos:

  • Para chaves de mapa, use comparable, não any.
  • Para ordenação, use uma constraint ordenável, não “tipos numéricos” se você também quer string.

5.3 Atenção à segurança e à previsibilidade (contexto de Cyber Segurança)

Embora generics reduzam uma classe de falhas (erros de tipo em runtime), ainda existem pontos de atenção em Cyber Segurança e robustez:

  • Validação de entrada continua obrigatória: generics não impedem dados inválidos.
  • Evite esconder conversões inseguras dentro de funções genéricas que aceitam any e usam reflexão ou type assertions internamente.
  • Em bibliotecas, documente invariantes: por exemplo, se um Set[T] assume T imutável (chave de map), isso deve ficar claro para evitar comportamento inesperado.

6) Checklist rápido para começar com generics no Go 1.18+

  1. Identifique duplicação: funções iguais para tipos diferentes.
  2. Transforme em função genérica com func Nome[T ...](...).
  3. Defina a constraint mais restrita que permita as operações necessárias.
  4. Escreva testes cobrindo múltiplos tipos (int, string, tipos definidos pelo usuário).
  5. Revise a API: se ficou mais complexa, considere manter versões concretas.

Conclusão

Com o Go 1.18+, GoLang passou a oferecer generics de forma pragmática: o suficiente para reduzir duplicação e aumentar segurança de tipos, sem abandonar o estilo direto da linguagem. Entender type parameters e constraints é o passo decisivo para construir funções reutilizáveis e estruturas de dados genéricas (como Stack[T] e Set[T]) de forma legível e segura.

Na prática, a recomendação é clara: use generics para o que é estruturalmente genérico (coleções, algoritmos de utilidade e camadas compartilhadas) e evite generalizar cedo demais. Isso mantém o código mais simples, auditável e previsível — características especialmente relevantes em ambientes onde confiabilidade e segurança importam.

Compartilhar este artigo: