HardТеория8 min

Ограничения типов (Constraints)

any, comparable, cmp.Ordered, интерфейсные ограничения, union types, тильда, type sets

Что такое constraint?

Constraint (ограничение) -- это интерфейс, определяющий набор типов, допустимых для параметра типа. Constraint говорит компилятору, какие операции разрешены с параметром типа.

┌──────────────────────────────────────────────────────────┐
│                    Constraints                            │
├──────────────────────────────────────────────────────────┤
│  any           — любой тип (alias для interface{})       │
│  comparable    — типы, поддерживающие == и !=            │
│  cmp.Ordered   — типы, поддерживающие <, >, <=, >=      │
│  Interface     — типы с определёнными методами           │
│  Union         — конкретный набор типов (int | string)   │
│  ~T            — тип T и все типы с underlying type T    │
└──────────────────────────────────────────────────────────┘

Встроенные constraints: any и comparable

any

any -- это alias для interface{}, введённый в Go 1.18. Разрешает любой тип. С параметром типа any можно только:

  • Присваивать переменным того же типа
  • Передавать в функции
  • Получать нулевое значение
package main

import "fmt"

// Print works with absolutely any type
func Print[T any](val T) {
    fmt.Println(val)
}

// Swap works with any type
func Swap[T any](a, b *T) {
    *a, *b = *b, *a
}

func main() {
    Print(42)
    Print("hello")
    Print([]int{1, 2, 3})

    a, b := 10, 20
    Swap(&a, &b)
    fmt.Println(a, b) // 20 10
}

comparable

comparable разрешает типы, поддерживающие == и !=. Это необходимо для ключей карты и операций сравнения.

package main

import "fmt"

// Index finds first occurrence of target in slice
func Index[T comparable](s []T, target T) int {
    for i, v := range s {
        if v == target { // requires comparable
            return i
        }
    }
    return -1
}

// Set is a generic set using map
type Set[T comparable] struct {
    items map[T]struct{}
}

func NewSet[T comparable]() *Set[T] {
    return &Set[T]{items: make(map[T]struct{})}
}

func (s *Set[T]) Add(val T) {
    s.items[val] = struct{}{}
}

func (s *Set[T]) Contains(val T) bool {
    _, ok := s.items[val]
    return ok
}

func (s *Set[T]) Remove(val T) {
    delete(s.items, val)
}

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

func (s *Set[T]) Values() []T {
    vals := make([]T, 0, len(s.items))
    for k := range s.items {
        vals = append(vals, k)
    }
    return vals
}

func main() {
    // Index
    fmt.Println(Index([]string{"a", "b", "c"}, "b")) // 1

    // Set
    s := NewSet[int]()
    s.Add(1)
    s.Add(2)
    s.Add(3)
    s.Add(2) // duplicate, ignored

    fmt.Println("Contains 2:", s.Contains(2)) // true
    fmt.Println("Size:", s.Len())              // 3
    fmt.Println("Values:", s.Values())
}

Типы, не являющиеся comparable: слайсы, карты, функции, структуры с полями-слайсами/картами/функциями.

// These will NOT compile:
// Index([][]int{{1}, {2}}, []int{1})   // slices are not comparable
// Index([]map[string]int{{}}, map[string]int{}) // maps are not comparable

cmp.Ordered (Go 1.21+)

cmp.Ordered -- constraint для типов, поддерживающих операторы упорядочения (<, >, <=, >=):

package main

import (
    "cmp"
    "fmt"
)

// Max returns the larger of two ordered values
func Max[T cmp.Ordered](a, b T) T {
    if a > b {
        return a
    }
    return b
}

// Clamp restricts a value to a range
func Clamp[T cmp.Ordered](val, minVal, maxVal T) T {
    if val < minVal {
        return minVal
    }
    if val > maxVal {
        return maxVal
    }
    return val
}

// BinarySearch finds target in sorted slice
func BinarySearch[T cmp.Ordered](sorted []T, target T) (int, bool) {
    lo, hi := 0, len(sorted)-1
    for lo <= hi {
        mid := lo + (hi-lo)/2
        switch cmp.Compare(sorted[mid], target) {
        case -1:
            lo = mid + 1
        case 1:
            hi = mid - 1
        default:
            return mid, true
        }
    }
    return lo, false
}

func main() {
    fmt.Println(Max(10, 20))       // 20
    fmt.Println(Max("abc", "xyz")) // xyz

    fmt.Println(Clamp(150, 0, 100)) // 100
    fmt.Println(Clamp(-5, 0, 100))  // 0
    fmt.Println(Clamp(50, 0, 100))  // 50

    sorted := []int{1, 3, 5, 7, 9, 11, 13}
    idx, found := BinarySearch(sorted, 7)
    fmt.Printf("BinarySearch(7): index=%d, found=%v\n", idx, found)
    // BinarySearch(7): index=3, found=true
}

Определение cmp.Ordered:

// From standard library (cmp package)
type Ordered interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |
        ~float32 | ~float64 |
        ~string
}

Интерфейс как constraint

Любой интерфейс с методами может быть constraint. Это требует от параметра типа реализации указанных методов:

package main

import "fmt"

// Stringer constraint — requires String() method
type Stringer interface {
    String() string
}

// JoinStrings concatenates String() results
func JoinStrings[T Stringer](items []T, sep string) string {
    result := ""
    for i, item := range items {
        if i > 0 {
            result += sep
        }
        result += item.String()
    }
    return result
}

// Custom type implementing Stringer
type Color struct {
    R, G, B uint8
}

func (c Color) String() string {
    return fmt.Sprintf("rgb(%d,%d,%d)", c.R, c.G, c.B)
}

// Reader constraint — requires Read method
type Reader interface {
    Read(p []byte) (n int, err error)
}

// ReadAll reads all data from a Reader
func ReadAll[T Reader](reader T) ([]byte, error) {
    var result []byte
    buf := make([]byte, 1024)
    for {
        n, err := reader.Read(buf)
        result = append(result, buf[:n]...)
        if err != nil {
            return result, err
        }
    }
}

func main() {
    colors := []Color{
        {255, 0, 0},
        {0, 255, 0},
        {0, 0, 255},
    }

    fmt.Println(JoinStrings(colors, ", "))
    // rgb(255,0,0), rgb(0,255,0), rgb(0,0,255)
}

Union types (Объединения типов)

Constraint может указать конкретный набор допустимых типов через оператор |:

package main

import "fmt"

// Number constraint: only int, float64, and complex128
type Number interface {
    int | int8 | int16 | int32 | int64 |
        float32 | float64
}

// Sum adds all numbers in a slice
func Sum[T Number](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Signed constraint: only signed integers
type Signed interface {
    int | int8 | int16 | int32 | int64
}

// Abs returns absolute value
func Abs[T Signed](n T) T {
    if n < 0 {
        return -n
    }
    return n
}

func main() {
    ints := []int{1, 2, 3, 4, 5}
    fmt.Println("Sum ints:", Sum(ints)) // 15

    floats := []float64{1.1, 2.2, 3.3}
    fmt.Println("Sum floats:", Sum(floats)) // 6.6

    fmt.Println("Abs(-42):", Abs(-42)) // 42

    // Won't compile: string is not in Number constraint
    // Sum([]string{"a", "b"}) // compile error
}

Важно: Интерфейсы с union types могут использоваться только как constraints для дженериков. Их нельзя использовать как обычные типы переменных.

// VALID: as constraint
func DoSomething[T int | string](val T) {}

// INVALID: cannot use as variable type
// var x (int | string) // compile error
// func Process(val int | string) {} // compile error

Тильда (~) -- underlying type

Оператор ~T разрешает не только тип T, но и все именованные типы с underlying type T:

package main

import "fmt"

// Without tilde: ONLY exact types
type ExactInt interface {
    int // only built-in int, not custom types based on int
}

// With tilde: int AND all types with underlying type int
type AnyInt interface {
    ~int // int, type MyInt int, type Score int, etc.
}

// Practical example
type UserID int
type Score int
type Count int

// Sum works with int, UserID, Score, Count, etc.
func Sum[T ~int](nums []T) T {
    var total T
    for _, n := range nums {
        total += n
    }
    return total
}

// Without tilde, this would NOT compile for UserID
func main() {
    ids := []UserID{1, 2, 3, 4, 5}
    fmt.Println("Sum of IDs:", Sum(ids)) // 15

    scores := []Score{100, 95, 87}
    fmt.Println("Sum of scores:", Sum(scores)) // 282

    // Also works with plain int
    plain := []int{10, 20, 30}
    fmt.Println("Sum of ints:", Sum(plain)) // 60
}

Тильда с составными типами

package main

import "fmt"

// StringLike matches string and all types based on string
type StringLike interface {
    ~string
}

// ToUpper converts any string-like type to uppercase
func ToUpper[T ~string](s T) T {
    result := make([]byte, len(s))
    for i := range len(s) {
        c := s[i]
        if c >= 'a' && c <= 'z' {
            result[i] = c - 32
        } else {
            result[i] = c
        }
    }
    return T(result)
}

type Name string
type Email string

func main() {
    fmt.Println(ToUpper("hello"))       // HELLO
    fmt.Println(ToUpper(Name("alice"))) // ALICE
    fmt.Println(ToUpper(Email("test"))) // TEST
}

Комбинированные constraints

Constraints могут комбинировать методы и типы:

package main

import "fmt"

// Addable can be added and must have String method
type Addable interface {
    ~int | ~float64
    String() string
}

// Alternatively: method-only constraint with type constraint
type Sortable interface {
    comparable
    Less(other Sortable) bool
}

// Combining comparable with methods
type Identifiable interface {
    comparable
    ID() string
}

// Practical: constraint requiring both methods and type restriction
type Numeric interface {
    ~int | ~int64 | ~float64
}

type Printable interface {
    fmt.Stringer
}

// NumericStringer requires both numeric type AND String method
type NumericStringer interface {
    Numeric
    Printable
}

type Dollar int

func (d Dollar) String() string {
    return fmt.Sprintf("$%d", int(d))
}

func FormatAll[T NumericStringer](items []T) []string {
    result := make([]string, len(items))
    for i, item := range items {
        result[i] = item.String()
    }
    return result
}

func main() {
    prices := []Dollar{10, 25, 99}
    fmt.Println(FormatAll(prices)) // [$10 $25 $99]
}

Концепция Type Sets

В Go 1.18+ каждый интерфейс определяет множество типов (type set), а не только набор методов:

┌─────────────────────────────────────────────────────────────┐
│  Интерфейс           │  Type Set                           │
├──────────────────────┼─────────────────────────────────────┤
│  any                 │  Все типы                           │
│  comparable          │  Типы, поддерживающие ==            │
│  interface{ M() }    │  Типы с методом M()                 │
│  interface{ ~int }   │  int и все типы с underlying int    │
│  interface{ A | B }  │  Объединение множеств A и B         │
│  interface{           │  Пересечение: ~int с методом M()   │
│    ~int              │                                      │
│    M()               │                                      │
│  }                   │                                      │
└──────────────────────┴─────────────────────────────────────┘
// Empty interface (any): type set = all types
type Any interface{}

// Method interface: type set = all types with Error() method
type Error interface {
    Error() string
}

// Type union: type set = {int, string}
type IntOrString interface {
    int | string
}

// Tilde union: type set = {int, all types based on int} ∪ {string, all types based on string}
type IntOrStringLike interface {
    ~int | ~string
}

// Combined: type set = ({int} ∪ {float64}) ∩ {types with String() method}
type NumericStringer interface {
    int | float64
    String() string
}
// Only int or float64 types that ALSO have a String() method

Самоссылающиеся дженерики (Self-Referential)

Паттерн, где constraint ссылается на сам тип. Полезен для методов сравнения, клонирования и т.д.

package main

import "fmt"

// Comparable allows comparing values of the same type
type SelfComparable[T any] interface {
    CompareTo(other T) int
}

// Max returns the larger of two self-comparable values
func MaxOf[T SelfComparable[T]](a, b T) T {
    if a.CompareTo(b) > 0 {
        return a
    }
    return b
}

// Money implements SelfComparable
type Money struct {
    Amount   int
    Currency string
}

func (m Money) CompareTo(other Money) int {
    if m.Amount > other.Amount {
        return 1
    }
    if m.Amount < other.Amount {
        return -1
    }
    return 0
}

// Cloneable pattern
type Cloneable[T any] interface {
    Clone() T
}

func CloneAll[T Cloneable[T]](items []T) []T {
    result := make([]T, len(items))
    for i, item := range items {
        result[i] = item.Clone()
    }
    return result
}

type Point struct {
    X, Y int
}

func (p Point) Clone() Point {
    return Point{X: p.X, Y: p.Y}
}

func main() {
    a := Money{Amount: 100, Currency: "USD"}
    b := Money{Amount: 200, Currency: "USD"}
    fmt.Println("Max:", MaxOf(a, b)) // Max: {200 USD}

    points := []Point{{1, 2}, {3, 4}, {5, 6}}
    clones := CloneAll(points)
    clones[0].X = 999
    fmt.Println("Original:", points[0]) // {1 2} — not affected
    fmt.Println("Clone:", clones[0])     // {999 2}
}

Устаревший пакет constraints

Пакет golang.org/x/exp/constraints был создан вместе с дженериками в Go 1.18. Начиная с Go 1.21, его содержимое перенесено в стандартную библиотеку:

golang.org/x/exp/constraints    →    Стандартная библиотека
─────────────────────────────────────────────────────────
constraints.Ordered             →    cmp.Ordered (Go 1.21)
constraints.Signed              →    Определите сами или используйте cmp.Ordered
constraints.Unsigned            →    Определите сами
constraints.Integer             →    Определите сами
constraints.Float               →    Определите сами
constraints.Complex             →    Определите сами
// Modern Go (1.21+): use cmp.Ordered instead of constraints.Ordered
import "cmp"

func Sort[T cmp.Ordered](s []T) {
    // ...
}

// If you need more specific constraints, define your own:
type Integer interface {
    ~int | ~int8 | ~int16 | ~int32 | ~int64 |
        ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
}

type Float interface {
    ~float32 | ~float64
}

Проверь себя

Что делает оператор тильда (~) в ограничениях дженериков?

Можно ли использовать интерфейс с union types (int | string) как тип переменной?

Какие типы НЕ являются comparable в Go?

Чем cmp.Ordered отличается от comparable?