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

Обобщённые структуры и методы Go: Дженерики

Когда речь заходит о дженериках, важно понимать, что они применимы не только к функциям, но и к структурам. В реальных задачах часто требуется контейнер, который может хранить данные разных типов, но при этом оставаться строго типизированным. Для этого в 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())
}

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

  1. Спецификация — Объявления методов
  2. Спецификация — Объявления типов

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

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

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

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

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

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

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

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