Обобщения в языке Go
Обобщения — важная возможность, представленная в Go версии 1.18, позволяющая разработчикам писать более гибкий и переиспользуемый код.
Обобщения в основном реализуются через следующие две ключевые концепции:
- Параметры типа:Позволяет использовать один или несколько типов в качестве параметров в определениях функций или типов.
- Ограничения типов:Задают условия, которым должны удовлетворять параметры типа, обеспечивая безопасную работу с этими типами внутри функций.
| Концепции | Функция | Пример |
|---|---|---|
| Параметр типа | Объявляется после имени функции или типа и представляет неопределённый тип. | [T any] |
| Ограничение типа | Определяет условия, которым должны удовлетворять параметры типа (например, поддерживаемые операторы или методы). | int,float64,comparable,constraints.Ordered,any |
any |
Ограничить параметры типаanyтипом. | [T any] |
comparable |
Ограничить параметры типаcomparableтипом. | [K comparable] |
Обобщения позволяют писать код, не зависящий от конкретных типов данных.
До появления обобщений, если мы хотели обрабатывать разные типы данных, нам обычно приходилось писать дублирующиеся функции для каждого типа.
Ограничения традиционного подхода:
Пример
func MaxInt(a, b int) int {
if a > b {
return a
}
return b
}
// Функция для обработки типа float64
func MaxFloat(a, b float64) float64 {
if a > b {
return a
}
return b
}
Решение с использованием обобщений:
Пример
func Max[T comparable](a, b T) T {
if a > b {
return a
}
return b
}
Подробное объяснение синтаксиса обобщений
Объявление параметра типа
Обобщённые функции и типы объявляются через списки параметров типа, синтаксис:[Type parameters Constraints]。
Пример
funcИмя функции[ограничение T](параметр T)Тип возвращаемого значения{
// Тело функции
}
typeимя типа[ограничение T] struct {
// Поле структуры
}
Соглашения об именовании параметров типа
- Обычно используются заглавные буквы:
T、K、V、Eожидание T: обозначает типK: обозначает ключV: обозначает значениеE: обозначает элемент
Ограничения
Ограничения определяют условия, которым должны удовлетворять параметры типа, и являются ключевой концепцией обобщений.
Встроенные ограничения
1. anyограничение
anyявляется пустым интерфейсомinterface{}псевдоним для, означающий, что допускается любой тип.
Пример
fmt.Printf("Значение: %v, Тип: %T\n", value, value)
}
// Пример использования
PrintAny(42) // Значение: 42, Тип: int
PrintAny("hello") // Значение: hello, Тип: string
PrintAny(3.14) // Значение: 3.14, Тип: float64
2. comparableограничение
comparableУказывает на поддержку типа==и!=operator.
Пример
for i, v := range slice {
if v == target {
return i
}
}
return -1
}
// Пример использования
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(FindIndex(numbers, 3)) // Вывод: 2
names := []string{"Alice", "Bob", "Charlie"}
fmt.Println(FindIndex(names, "Bob")) // Вывод: 1
3. Ограничения объединения
Использование|Оператор объединяет несколько типов.
Пример
type Number interface {
int | int8 | int16 | int32 | int64 |
uint | uint8 | uint16 | uint32 | uint64 |
float32 | float64
}
func Add[T Number](a, b T) T {
return a + b
}
// Пример использования
fmt.Println(Add(10, 20)) // Вывод: 30
fmt.Println(Add(3.14, 2.71)) // Вывод: 5.85
Пользовательское ограничение
1. Ограничения методов
Определите ограничения, требующие конкретных методов.
Пример
type Stringer interface {
String() string
}
func PrintString[T Stringer](value T) {
fmt.Println(value.String())
}
// Реализуйте пользовательский тип
type Person struct {
Name string
Age int
}
func (p Person) String() string {
return fmt.Sprintf("%s (%d years old)", p.Name, p.Age)
}
// Пример использования
person := Person{Name: "Alice", Age: 25}
PrintString(person) // Вывод: Alice (25 years old)
2. Сложные ограничения
Объедините требования к типу и методу.
Пример
type NumericStringer interface {
Number
String() string
}
Практика обобщённых функций
1. Общие вспомогательные функции
Пример
func Swap[T any](a, b T) (T, T) {
return b, a
}
// Проверить, содержит ли срез элемент
func Contains[T comparable](slice []T, target T) bool {
for _, item := range slice {
if item == target {
return true
}
}
return false
}
// Функция удаления дубликатов
func Unique[T comparable](slice []T) []T {
seen := make(map[T]bool)
result := []T{}
for _, item := range slice {
if !seen[item] {
seen[item] = true
result = append(result, item)
}
}
return result
}
// Пример использования
func main() {
// Пример обмена
a, b := 10, 20
a, b = Swap(a, b)
fmt.Printf("a=%d, b=%d\n", a, b) // Вывод: a=20, b=10
// Пример Contains
numbers := []int{1, 2, 3, 4, 5}
fmt.Println(Contains(numbers, 3)) // Вывод: true
// Пример Unique
duplicates := []int{1, 2, 2, 3, 4, 4, 5}
unique := Unique(duplicates)
fmt.Println(unique) // Вывод: [1 2 3 4 5]
}
2. Функции математических операций
Пример
func Max[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
max := slice[0]
for _, value := range slice[1:] {
if value > max {
max = value
}
}
return max
}
// Найти минимальное значение среза
func Min[T Number](slice []T) T {
if len(slice) == 0 {
var zero T
return zero
}
min := slice[0]
for _, value := range slice[1:] {
if value < min {
min = value
}
}
return min
}
// Найти среднее значение среза
func Average[T Number](slice []T) float64 {
if len(slice) == 0 {
return 0
}
var sum T
for _, value := range slice {
sum += value
}
return float64(sum) / float64(len(slice))
}
// Пример использования
func main() {
ints := []int{1, 5, 3, 9, 2}
floats := []float64{1.1, 5.5, 3.3, 9.9, 2.2}
fmt.Printf("Максимум int: %d\n", Max(ints)) // Вывод: 9
fmt.Printf("Минимум float: %.1f\n", Min(floats)) // Вывод: 1.1
fmt.Printf("Среднее: %.2f\n", Average(floats)) // Вывод: 4.40
}
Обобщённые типы
1. Обобщённые структуры
Пример
type Stack[T any] struct {
элементов []T
}
// Добавить
func (s *Stack[T]) Push(value T) {
s.elements = append(s.elements, value)
}
// Извлечь
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
lastIndex := len(s.elements) - 1
value := s.elements[lastIndex]
s.elements = s.elements[:lastIndex]
return value, true
}
// Посмотреть на верхний элемент
func (s *Stack[T]) Peek() (T, bool) {
if len(s.elements) == 0 {
var zero T
return zero, false
}
return s.elements[len(s.elements)-1], true
}
// Проверить, пуст ли стек
func (s *Stack[T]) IsEmpty() bool {
return len(s.elements) == 0
}
// Пример использования
func main() {
// Стек целых чисел
intStack := Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
fmt.Println(intStack.Pop()) // Вывод: 3 true
// Стек строк
stringStack := Stack[string]{}
stringStack.Push("hello")
stringStack.Push("world")
fmt.Println(stringStack.Pop()) // Вывод: world true
}
2. Обобщённая карта (Map)
Пример
type SafeMap[K comparable, V any] struct {
data map[K]V
mutex sync.RWMutex
}
// Создать новый SafeMap
func NewSafeMap[K comparable, V any]() *SafeMap[K, V] {
return &SafeMap[K, V]{
data: make(map[K]V),
}
}
// Установить пару ключ-значение
func (m *SafeMap[K, V]) Set(key K, value V) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.data[key] = value
}
// Получить значение
func (m *SafeMap[K, V]) Get(key K) (V, bool) {
m.mutex.RLock()
defer m.mutex.RUnlock()
value, exists := m.data[key]
return value, exists
}
// Удалить ключ
func (m *SafeMap[K, V]) Delete(key K) {
m.mutex.Lock()
defer m.mutex.Unlock()
delete(m.data, key)
}
// Получить все ключи
func (m *SafeMap[K, V]) Keys() []K {
m.mutex.RLock()
defer m.mutex.RUnlock()
keys := make([]K, 0, len(m.data))
for key := range m.data {
keys = append(keys, key)
}
return keys
}
// Пример использования
func main() {
// Создать map из string в int
scores := NewSafeMap[string, int]()
scores.Set("Alice", 95)
scores.Set("Bob", 87)
if score, exists := scores.Get("Alice"); существует {
fmt.Printf("Alice's score: %d\n", score) // Вывод: Alice's score: 95
}
fmt.Println("Keys:", scores.Keys()) // Вывод: Keys: [Alice Bob]
}
Вывод типов
Компилятор Go может автоматически выводить параметры типа, делая код более лаконичным.
Пример
func main() {
// Пример вывода типов
fmt.Println(Max([]int{1, 2, 3})) // Компилятор выводит T как int
fmt.Println(Max([]float64{1.1, 2.2})) // Компилятор выводит T как float64
// Явно укажите тип (иногда необходимо)
var result int = Max[int]([]int{1, 2, 3})
fmt.Println(result)
}
Практические упражнения
Упражнение 1: Реализуйте обобщённый фильтр
НапишитеFilterфункцию, которая фильтрует элементы среза по условию.
Пример
func Filter[T any](slice []T, predicate func(T) bool) []T {
// Реализуйте логику фильтрации
}
// Тестовый код
func main() {
numbers := []int{1, 2, 3, 4, 5, 6}
even := Filter(numbers, func(n int) bool {
return n%2 == 0
})
fmt.Println(even) // Должно вывести: [2 4 6]
}
Упражнение 2: Реализуйте обобщённую функцию map
НапишитеMapФункция, которая преобразует каждый элемент среза в другой тип.
Пример
func Map[T any, U any](slice []T, mapper func(T) U) []U {
// Реализуйте логику преобразования
}
// Тестовый код
func main() {
numbers := []int{1, 2, 3, 4, 5}
strings := Map(numbers, func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
fmt.Println(strings)
}
Распространённые проблемы и соображения
1. Соображения по производительности
Обобщения выполняют специализацию типов во время компиляции, а производительность во время выполнения сравнима с написанным вручную кодом для конкретного типа.
2. Выбор ограничений типов
- Использование
anyнаиболее гибок, когда, но ограничен в функциональности. - Использование
comparableПоддерживает сравнение на равенство - Используйте ограничения объединения, чтобы ограничить доступные конкретные типы.
3. Обработка ошибок
Пример
func SafeMax[T Number](slice []T) (T, error) {
if len(slice) == 0 {
var zero T
return zero, errors.New("срез пуст")
}
return Max(slice), nil
}