Что такое 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
}