Когда речь заходит о дженериках, важно понимать, что они применимы не только к функциям, но и к структурам. В реальных задачах часто требуется контейнер, который может хранить данные разных типов, но при этом оставаться строго типизированным. Для этого в Go можно объявить обобщённую структуру. Синтаксис похож на функции: после имени структуры указывается параметр типа.
type Box[T any] struct {
value T
}
Здесь Box
— это контейнер, который может хранить значение любого типа. Если создать Box[int]
, внутри окажется число, а если Box[string]
— строка. Такой подход избавляет от необходимости использовать пустой интерфейс и делать ручное приведение типов.
Структуры могут иметь методы, которые тоже используют параметры типа. Например, у коробки можно добавить методы для установки и получения значения:
func (b *Box[T]) Set(v T) {
b.value = v
}
func (b *Box[T]) Get() T {
return b.value
}
Эти методы сохраняют строгую типизацию: Box[int]
принимает и возвращает только числа, а Box[string]
— только строки. Попытка смешать типы приведёт к ошибке компиляции.
Иногда нужно работать сразу с несколькими типами. В таком случае объявляют несколько параметров. Например, пара ключ-значение:
type Pair[K, V any] struct {
Key K
Value V
}
Такая структура позволяет строить словари, кэш или ассоциированные данные, где один параметр отвечает за тип ключа, а другой — за тип значения.
Хороший пример обобщённой структуры — стек. Он хранит элементы в срезе и предоставляет методы для добавления и извлечения:
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(v T) {
s.items = append(s.items, v)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
last := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return last, true
}
Такой стек можно использовать с любым типом:
ints := Stack[int]{}
ints.Push(10)
ints.Push(20)
val, ok := ints.Pop() // вернёт 20
strings := Stack[string]{}
strings.Push("go")
strings.Push("generics")
s, _ := strings.Pop() // вернёт "generics"
Те же принципы можно применить для очереди, дерева или связанного списка. Например, очередь:
type Queue[T any] struct {
items []T
}
func (q *Queue[T]) Enqueue(v T) {
q.items = append(q.items, v)
}
func (q *Queue[T]) Dequeue() (T, bool) {
if len(q.items) == 0 {
var zero T
return zero, false
}
first := q.items[0]
q.items = q.items[1:]
return first, true
}
Обобщённые структуры и методы делают код гибким и переиспользуемым. Вместо десятков реализаций для разных типов достаточно описать один универсальный вариант. Go сохраняет строгую проверку на этапе компиляции, поэтому контейнеры и коллекции на дженериках получаются безопасными и удобными.
Самостоятельная работа
Создадим обобщённый контейнер и методы к нему.
Реализуйте множество Set[T]
с методами Add
, Has
, Remove
.
// создаём множество для строк
s := NewSet[string]()
s.Add("apple")
s.Add("banana")
s.Add("orange")
fmt.Println("Has apple?", s.Has("apple")) // true
fmt.Println("Has grape?", s.Has("grape")) // false
s.Remove("banana")
fmt.Println("Has banana?", s.Has("banana")) // false
// создаём множество для чисел
nums := NewSet[int]()
nums.Add(10)
nums.Add(20)
nums.Add(30)
fmt.Println("Has 10?", nums.Has(10)) // true
nums.Remove(10)
fmt.Println("Has 10?", nums.Has(10)) // false
Шаги:
- Опишите структуру
Set[T comparable]
на базеmap[T]struct{}
. - Реализуйте
Add(v T)
,Has(v T) bool
,Remove(v T)
. - Проверьте работу на
int
иstring
.
Показать решение
package main
import "fmt"
// Set — простое множество для любых сравнимых типов
type Set[T comparable] struct {
m map[T]struct{}
}
// NewSet создаёт новое множество, optionally с начальными элементами
func NewSet[T comparable](values ...T) *Set[T] {
s := &Set[T]{m: make(map[T]struct{}, len(values))}
for _, v := range values {
s.m[v] = struct{}{}
}
return s
}
// Add добавляет элемент в множество
func (s *Set[T]) Add(v T) {
s.m[v] = struct{}{}
}
// AddAll добавляет несколько элементов сразу
func (s *Set[T]) AddAll(values ...T) {
for _, v := range values {
s.Add(v)
}
}
// Has проверяет наличие элемента
func (s *Set[T]) Has(v T) bool {
_, ok := s.m[v]
return ok
}
// Remove удаляет элемент
func (s *Set[T]) Remove(v T) {
delete(s.m, v)
}
// Len возвращает количество элементов
func (s *Set[T]) Len() int {
return len(s.m)
}
// Items возвращает срез всех элементов (в произвольном порядке)
func (s *Set[T]) Items() []T {
items := make([]T, 0, len(s.m))
for v := range s.m {
items = append(items, v)
}
return items
}
// Clear очищает множество
func (s *Set[T]) Clear() {
clear(s.m)
}
// String — реализует fmt.Stringer
func (s *Set[T]) String() string {
return fmt.Sprint(s.Items())
}
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.