Golang 1.18 представил поддержку дженериков, что позволяет разработчикам писать код, не зависящий от конкретных типов. Это означает, что теперь функции и типы могут быть написаны для работы с любым набором типов. В этой статье мы рассмотрим, как использовать дженерики Golang в структуре и с интерфейсами.

Использование обобщений Golang в структуре

Начиная с Go 1.18, вы можете определять универсальные типы:

type Model[T any] struct {
 Data []T
}

Универсальный тип должен быть создан при использовании, а для создания экземпляра требуется список параметров типа. Вот пример:

func main() {
 // passing int as type parameter
 modelInt := Model[int]{Data: []int{1, 2, 3}}
 fmt.Println(modelInt.Data) // [1 2 3]

 // passing string as type parameter
 modelStr := Model[string]{Data: []string{"a", "b", "c"}}
 fmt.Println(modelStr.Data) // [a b c]
}

Определить методы для универсальных типов

Если вы объявляете методы для универсального типа, вы должны повторить объявление параметра типа в приемнике, даже если параметры типа не используются в области действия метода. Например:

type Model[T any] struct {
 Data []T
}

func (m *Model[T]) Push(item T) {
 m.Data = append(m.Data, item)
}

Использование интерфейсов с дженериками Golang

Типы на самом деле не реализуют универсальные интерфейсы, они реализуют экземпляры универсальных интерфейсов. Вы не можете использовать универсальный тип (включая интерфейсы) без создания экземпляра.

Вот пример:

type Getter[T any] interface {
 Get() T
}

type Model[T any] struct {
 Data []T
}

// implements Getter
func (m *Model[T]) Get(i int) T {
 return m.Data[i]
}

Полный пример

Объявите ограничение типа как интерфейс

В следующем разделе мы извлечем ранее определенное ограничение в его собственный интерфейс для повторного использования в нескольких местах. Использование интерфейсов в качестве ограничений типа позволяет любому реализующему типу удовлетворить ограничение. Например, если интерфейс имеет три метода и используется в качестве параметра типа в универсальной функции, все аргументы типа должны иметь эти методы. В этом разделе также рассматриваются интерфейсы ограничений, которые относятся к определенным типам. Пример:

type Number interface {
 int | float64
}

type Getter[T Number] interface {
 Get() T
}

type Model[T Number] struct {
 Data []T
}

func (m *Model[T]) Push(item T) {
 m.Data = append(m.Data, item)
}

func (m *Model[T]) Get(i int) T {
 return m.Data[i]
}

func main() {
 // passing int as type parameter
 modelInt := Model[int]{Data: []int{1, 2, 3}}
 fmt.Println(modelInt.Data) // [1 2 3]

 // passing float64 as type parameter
 modelFloat := Model[float64]{Data: []float64{1.1, 2.2, 0.02}}
 fmt.Println(modelFloat.Data) // [1.1 2.2 0.02]

 modelInt.Push(4)
 fmt.Println(modelInt.Data) // [1 2 3 4]

 itemAtOne := modelFloat.Get(1)
 fmt.Println(itemAtOne) // 2.2
}

В этом примере, если мы попытаемся использовать тип, который не удовлетворяет Number interface, вы получите ошибку компиляции.

 _ = Model[string]{Data: []string{"a", "b"}} // string does not satisfy Number (string missing in int | float64)

Полный пример

В заключение, дженерики Golang предоставляют разработчикам возможность писать код, не зависящий от конкретных типов. Это позволяет использовать более повторно используемый и поддерживаемый код.