Зарегистрируйтесь, чтобы продолжить обучение

Ограничения (constraints) Go: Дженерики

Когда мы пишем обобщённые функции в Go, важно управлять тем, какие типы можно использовать. Если не задать ограничение, параметр типа подходит для любого значения, но далеко не всегда это безопасно. Например, функция поиска минимального значения должна работать только с теми типами, для которых определены операции < и >. Для этого в Go существуют ограничения, которые указываются рядом с параметром типа.

Самое простое ограничение — any. Оно означает, что функция не накладывает никаких условий. Такой вариант подойдёт, например, для реверса среза:

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

Функция работает одинаково для []int, []string и любых других срезов.

Если нужно проверять равенство, используют встроенное ограничение comparable. Оно разрешает операции == и !=:

func Contains[T comparable](slice []T, target T) bool {
    for _, v := range slice {
        if v == target {
            return true
        }
    }
    return false
}

Здесь T обязан быть сравнимым. Попробовать вызвать функцию с несравнимым типом, например срезом, не получится — компилятор выдаст ошибку.

Для работы с упорядоченными данными удобно использовать constraints.Ordered из пакета golang.org/x/exp/constraints. Он включает числа и строки, для которых доступны операции &lt; и >:

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

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

Такое ограничение позволяет одной функцией сравнивать и числа, и строки.

Если встроенных ограничений недостаточно, можно описать свои. Например, интерфейс для типов, у которых есть метод String() string:

type Stringer interface {
    String() string
}

func PrintAll[T Stringer](values []T) {
    for _, v := range values {
        fmt.Println(v.String())
    }
}

Теперь вызвать функцию можно только с теми типами, которые реализуют метод String().

Ограничения можно комбинировать через встраивание. Допустим, нужно, чтобы тип одновременно был сравнимым и имел метод Name() string:

type Named interface {
    Name() string
}

type ComparableNamed interface {
    comparable
    Named
}

Функция с таким параметром типа сможет работать только с подходящими структурами:

func FindByName[T ComparableNamed](items []T, name string) *T {
    for _, v := range items {
        if v.Name() == name {
            return &v
        }
    }
    return nil
}

Наконец, можно ограничивать параметр конкретными базовыми типами. Для этого в интерфейсе перечисляют набор через |:

type Number interface {
    int | int64 | float64
}

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

Здесь Sum работает только с числами. Если попытаться вызвать её со строками или булевыми значениями, компилятор не даст собрать программу.

Ограничения позволяют точно задать правила для параметров типа. Они бывают встроенными (any, comparable, constraints.Ordered) и пользовательскими, могут требовать методы или конкретные наборы типов. Это делает обобщённые функции гибкими, но при этом безопасными: компилятор заранее проверяет, что выбранный тип соответствует условиям.


Самостоятельная работа

Закрепим работу с пользовательскими ограничениями.

  1. Опишите ограничение для типов с методом Len() int и напишите функцию, которая суммирует длины всех элементов.
  2. Реализуйте TotalLen[T HasLen](xs []T) int — суммирует Len() для каждого элемента.
  3. Создайте 1–2 простых типа с методом Len() и проверьте работу функции.
fmt.Println(TotalLen([]Words{{"a", "b"}, {"c"}})) // 3
fmt.Println(TotalLen([]Bytes{[]byte{1,2,3}, []byte{4}})) // 4
Показать решение
package main

import "fmt"

type HasLen interface{ Len() int }

func TotalLen[T HasLen](xs []T) int {
    total := 0
    for _, v := range xs {
        total += v.Len()
    }
    return total
}

type Words []string

func (w Words) Len() int { return len(w) }

type Bytes []byte

func (b Bytes) Len() int { return len(b) }

func main() {
    fmt.Println(TotalLen([]Words{{"a", "b"}, {"c"}}))          // 3
    fmt.Println(TotalLen([]Bytes{[]byte{1, 2, 3}, []byte{4}})) // 4
}

Дополнительные материалы

  1. Спецификация — Наборы типов (Type sets)
  2. Спецификация — Предобъявленные идентификаторы (`any`, `comparable`)
  3. Пакет constraints (exp)

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff