Constraints e interfaces genéricas

1. Fundamentos das Constraints em Go 1.18+

Constraints em Go são mecanismos que definem quais tipos podem ser utilizados como argumentos de tipo em funções e tipos genéricos. Elas surgiram no Go 1.18 para resolver a necessidade de escrever código genérico sem sacrificar a segurança de tipos.

A diferença fundamental entre any (que é um alias para interface{}) e constraints restritivas é que any aceita qualquer tipo, enquanto constraints específicas limitam os tipos permitidos, possibilitando operações seguras sobre eles.

// any aceita qualquer tipo
func PrintAny[T any](value T) {
    fmt.Println(value)
}

// Constraint restritiva: apenas tipos comparáveis
func IsEqual[T comparable](a, b T) bool {
    return a == b
}

A sintaxe básica para declarar constraints é [T constraint], onde T é o parâmetro de tipo e constraint define o conjunto de tipos permitidos.

2. Interfaces como Constraints

Interfaces tradicionais em Go funcionam naturalmente como constraints genéricas. Qualquer interface pode ser usada como constraint, e o compilador verificará se o tipo concreto implementa todos os métodos da interface.

// Interface fmt.Stringer como constraint
func PrintString[T fmt.Stringer](value T) {
    fmt.Println(value.String())
}

type User struct {
    Name string
}

func (u User) String() string {
    return u.Name
}

// Uso
user := User{Name: "Alice"}
PrintString(user) // Output: Alice

No entanto, constraints baseadas em interfaces têm limitações importantes: não podem restringir operadores como +, -, * ou <. Por exemplo, não é possível criar uma constraint que exija que o tipo suporte o operador de adição.

3. Criação de Constraints Customizadas

Para superar as limitações das interfaces tradicionais, Go permite criar constraints customizadas usando type sets com o operador ~ (tilde) e o pipe | para união de tipos.

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

func Sum[T Numeric](values []T) T {
    var result T
    for _, v := range values {
        result += v
    }
    return result
}

// Uso com tipos nomeados
type Celsius float64
temps := []Celsius{20.5, 25.0, 18.3}
total := Sum(temps) // Funciona porque Celsius tem ~float64 como subjacente

4. Type Sets e Operadores Aproximados (~)

O operador ~ é crucial para constraints flexíveis. Enquanto int restringe apenas ao tipo int da biblioteca padrão, ~int aceita qualquer tipo cujo tipo subjacente seja int, incluindo tipos nomeados como type MeuInt int.

type MyInt int
type MyString string

// Restritivo: apenas int literal
func OnlyInt[T int](value T) T {
    return value
}

// Flexível: aceita qualquer tipo com subjacente int
func AcceptApproxInt[T ~int](value T) T {
    return value * 2
}

var x MyInt = 10
// OnlyInt(x) // Erro de compilação!
result := AcceptApproxInt(x) // OK: result = 20

Importante: type sets não podem conter interfaces com métodos. Esta é uma restrição do design de Go para evitar ambiguidades na resolução de tipos.

5. Constraints com Métodos e Type Sets Combinados

É possível combinar métodos e type sets na mesma interface constraint, desde que o type set seja composto apenas por tipos concretos ou aproximados.

type StringableInt interface {
    ~int
    String() string
}

func FormatValue[T StringableInt](value T) string {
    return fmt.Sprintf("Valor: %s (%d)", value.String(), value)
}

type Score int

func (s Score) String() string {
    return fmt.Sprintf("Pontuação: %d", int(s))
}

// Uso
var score Score = 95
formatted := FormatValue(score) // "Valor: Pontuação: 95 (95)"

A ordem de avaliação segue a lógica: primeiro verifica-se se o tipo satisfaz o type set, depois se implementa os métodos declarados.

6. Inferência de Tipos e Resolução de Constraints

O compilador Go infere tipos genéricos a partir dos argumentos fornecidos. Falhas comuns incluem ambiguidade quando múltiplos tipos satisfazem a constraint, ou quando o tipo inferido não satisfaz a constraint.

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

// Inferência bem-sucedida
result := Max(10, 20) // T é inferido como int

// Falha: tipos diferentes
// result = Max(10, 20.5) // Erro: tipo inferido conflitante

// Solução: especificar explicitamente
result = Max[float64](10, 20.5) // OK

O pacote golang.org/x/exp/constraints oferece constraints pré-definidas como Ordered, Signed, Unsigned, Float e Complex, mas seu uso é experimental.

7. Boas Práticas e Padrões de Design

Ao decidir entre constraints e interfaces tradicionais, considere:

  • Use interfaces tradicionais quando apenas métodos são necessários
  • Use constraints genéricas quando precisa de type sets ou operações sobre tipos
  • Evite constraints excessivamente restritivas que limitam a reutilização
// Exemplo prático: função Min com Ordered
func Min[T constraints.Ordered](a, b T) T {
    if a < b {
        return a
    }
    return b
}

// Limitação: Ordered não inclui string em todas as implementações
// Alternativa usando comparable não funciona para comparação <

Para tipos que não implementam Ordered mas são comparáveis, crie constraints específicas:

type Comparable interface {
    ~int | ~float64 | ~string
}

func MinCustom[T Comparable](a, b T) T {
    if a < b {
        return a
    }
    return b
}

8. Casos Avançados e Limitações Conhecidas

Constraints com tipos parametrizados permitem criar abstrações poderosas:

func Reverse[T ~[]E, E any](slice T) T {
    result := make(T, len(slice))
    for i, v := range slice {
        result[len(slice)-1-i] = v
    }
    return result
}

// Uso
nums := []int{1, 2, 3, 4, 5}
reversed := Reverse(nums) // [5, 4, 3, 2, 1]

Limitações importantes:
- Sem suporte para métodos de operadores aritméticos (não é possível criar Addable constraint)
- Type sets não podem conter interfaces com métodos
- Inferência de tipos pode falhar em cenários complexos

Workarounds para operações aritméticas:

// Uso de função auxiliar
func Add[T Numeric](a, b T) T {
    return a + b // Funciona porque Numeric garante suporte a +
}

// Reflexão como último recurso
func AddReflect(a, b interface{}) interface{} {
    va := reflect.ValueOf(a)
    vb := reflect.ValueOf(b)
    // Implementação complexa e menos segura
    return va.Interface()
}

Referências