Зачем нужны дженерики?
До Go 1.18 для написания обобщённого кода приходилось использовать неудовлетворительные подходы:
До Go 1.18:
┌──────────────────────────────────────────────────────────────┐
│ 1. interface{} — потеря типобезопасности, type asserts │
│ 2. Кодогенерация — сложная настройка, медленный билд │
│ 3. Copy-paste — дублирование кода для каждого типа │
└──────────────────────────────────────────────────────────────┘
Go 1.18+:
┌──────────────────────────────────────────────────────────────┐
│ Дженерики — типобезопасность + обобщённый код │
│ без потери производительности │
└──────────────────────────────────────────────────────────────┘
Проблема без дженериков
package main
import "fmt"
// Without generics: need separate function for each type
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func minFloat64(a, b float64) float64 {
if a < b {
return a
}
return b
}
func minString(a, b string) string {
if a < b {
return a
}
return b
}
// Or use interface{} — loses type safety
func minAny(a, b any) any {
// How to compare? Type switch for every type?
// This is fragile and error-prone
switch a := a.(type) {
case int:
if a < b.(int) { // panic if b is not int
return a
}
return b
default:
panic("unsupported type")
}
}
func main() {
fmt.Println(minInt(3, 7))
fmt.Println(minFloat64(3.14, 2.72))
fmt.Println(minString("go", "rust"))
}
Решение с дженериками
package main
import (
"cmp"
"fmt"
)
// Generic min function — works for any ordered type
// cmp.Ordered constraint: all types supporting <, >, <=, >=
func min[T cmp.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
func main() {
fmt.Println(min(3, 7)) // int: 3
fmt.Println(min(3.14, 2.72)) // float64: 2.72
fmt.Println(min("go", "rust")) // string: go
// Note: since Go 1.21, use built-in min() and max()
fmt.Println(min(10, 20)) // built-in
}
Заметка: Начиная с Go 1.21, функции
min()иmax()встроены в язык и работают с любым количеством аргументов типаcmp.Ordered. Но понимание дженериков остаётся критически важным для написания собственных обобщённых функций и типов.
Синтаксис параметров типов
Обобщённые функции
// Syntax: func Name[T constraint](params) returnType
func Print[T any](val T) {
fmt.Println(val)
}
// Multiple type parameters
func Pair[K comparable, V any](key K, value V) map[K]V {
return map[K]V{key: value}
}
// Type parameter used in return type
func Zero[T any]() T {
var zero T
return zero
}
package main
import "fmt"
// Map transforms a slice by applying a function to each element
func Map[T any, U any](slice []T, fn func(T) U) []U {
result := make([]U, len(slice))
for i, v := range slice {
result[i] = fn(v)
}
return result
}
// Filter returns elements matching a predicate
func Filter[T any](slice []T, pred func(T) bool) []T {
var result []T
for _, v := range slice {
if pred(v) {
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{1, 2, 3, 4, 5}
// Map: int → string
strings := Map(nums, func(n int) string {
return fmt.Sprintf("num_%d", n)
})
fmt.Println(strings) // [num_1 num_2 num_3 num_4 num_5]
// Filter: keep even numbers
evens := Filter(nums, func(n int) bool {
return n%2 == 0
})
fmt.Println(evens) // [2 4]
// Map: string → int (length)
words := []string{"hello", "go", "generics"}
lengths := Map(words, func(s string) int {
return len(s)
})
fmt.Println(lengths) // [5 2 8]
}
Обобщённые типы
package main
import "fmt"
// Stack is a generic LIFO data structure
type Stack[T any] struct {
items []T
}
// Push adds an element to the top
func (s *Stack[T]) Push(val T) {
s.items = append(s.items, val)
}
// Pop removes and returns the top element
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
last := len(s.items) - 1
val := s.items[last]
s.items = s.items[:last]
return val, true
}
// Peek returns the top element without removing
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
return s.items[len(s.items)-1], true
}
// Len returns the number of elements
func (s *Stack[T]) Len() int {
return len(s.items)
}
func main() {
// Stack of integers
intStack := &Stack[int]{}
intStack.Push(1)
intStack.Push(2)
intStack.Push(3)
for intStack.Len() > 0 {
val, _ := intStack.Pop()
fmt.Println(val) // 3, 2, 1
}
// Stack of strings — same code, different type
strStack := &Stack[string]{}
strStack.Push("hello")
strStack.Push("world")
if top, ok := strStack.Peek(); ok {
fmt.Println("Top:", top) // world
}
}
Обобщённые интерфейсы
package main
import "fmt"
// Container is a generic interface
type Container[T any] interface {
Add(T)
Get(index int) (T, bool)
Len() int
}
// SliceContainer implements Container using a slice
type SliceContainer[T any] struct {
items []T
}
func (c *SliceContainer[T]) Add(val T) {
c.items = append(c.items, val)
}
func (c *SliceContainer[T]) Get(index int) (T, bool) {
if index < 0 || index >= len(c.items) {
var zero T
return zero, false
}
return c.items[index], true
}
func (c *SliceContainer[T]) Len() int {
return len(c.items)
}
// process works with any Container implementation
func process[T any](c Container[T]) {
fmt.Printf("Container has %d items\n", c.Len())
}
func main() {
c := &SliceContainer[int]{}
c.Add(10)
c.Add(20)
c.Add(30)
process[int](c) // Container has 3 items
if val, ok := c.Get(1); ok {
fmt.Println("Item at index 1:", val) // 20
}
}
Вывод типов (Type Inference)
Go может вывести параметры типов из аргументов функции, что позволяет не указывать их явно:
package main
import "fmt"
func Contains[T comparable](slice []T, target T) bool {
for _, v := range slice {
if v == target {
return true
}
}
return false
}
func main() {
nums := []int{1, 2, 3, 4, 5}
// Explicit type argument
fmt.Println(Contains[int](nums, 3)) // true
// Type inference — T is inferred from arguments
fmt.Println(Contains(nums, 3)) // true — T inferred as int
// Works with strings too
words := []string{"go", "rust", "python"}
fmt.Println(Contains(words, "go")) // true
fmt.Println(Contains(words, "java")) // false
}
Когда вывод типов не работает
package main
import "fmt"
// Return type depends on type parameter, cannot be inferred from args
func Zero[T any]() T {
var zero T
return zero
}
func main() {
// Must specify type explicitly — no arguments to infer from
n := Zero[int]()
s := Zero[string]()
b := Zero[bool]()
fmt.Println(n, s, b) // 0 false
}
Инстанциация (Instantiation)
Когда вы используете дженерик-функцию или тип с конкретным типом, Go создаёт инстанциацию -- конкретную версию кода:
package main
import "fmt"
func PrintSlice[T any](s []T) {
for _, v := range s {
fmt.Printf("%v ", v)
}
fmt.Println()
}
func main() {
// Each call instantiates the function for a specific type
PrintSlice([]int{1, 2, 3}) // instantiation: PrintSlice[int]
PrintSlice([]string{"a", "b"}) // instantiation: PrintSlice[string]
PrintSlice([]float64{1.1, 2.2}) // instantiation: PrintSlice[float64]
// You can also get a reference to a specific instantiation
printInts := PrintSlice[int]
printInts([]int{10, 20, 30})
}
Ограничение: методы не могут иметь свои параметры типов
В Go методы не могут объявлять собственные параметры типов. Они могут использовать только параметры, объявленные на уровне типа:
// VALID: method uses type parameter from the type
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(val T) {
s.items = append(s.items, val)
}
// INVALID: method cannot declare its own type parameter
// func (s *Stack[T]) Convert[U any](fn func(T) U) []U { ... }
// Compile error: method must have no type parameters
// WORKAROUND: Use a top-level function instead
func Convert[T any, U any](s *Stack[T], fn func(T) U) []U {
result := make([]U, len(s.items))
for i, v := range s.items {
result[i] = fn(v)
}
return result
}
Когда использовать дженерики
┌────────────────────────────────────────────────────────────────┐
│ Используйте дженерики когда: │
├────────────────────────────────────────────────────────────────┤
│ 1. Функция/тип работает с разными типами одинаковым образом │
│ 2. Есть дублирование кода, отличающегося только типами │
│ 3. Нужна типобезопасная коллекция/контейнер │
│ 4. Утилитарные функции (map, filter, contains, etc.) │
└────────────────────────────────────────────────────────────────┘
┌────────────────────────────────────────────────────────────────┐
│ НЕ используйте дженерики когда: │
├────────────────────────────────────────────────────────────────┤
│ 1. Поведение отличается для разных типов → интерфейсы │
│ 2. Достаточно обычного интерфейса (io.Reader, fmt.Stringer) │
│ 3. Один конкретный тип достаточен │
│ 4. Сложность перевешивает пользу │
└────────────────────────────────────────────────────────────────┘
Правило Роба Пайка: "Don't use generics until you know you need them." Начните с конкретных типов, переходите к интерфейсам, и только потом -- к дженерикам, если код действительно дублируется.
Производительность: GCShape Stenciling
В отличие от C++ (мономорфизация -- отдельная копия кода для каждого типа), Go использует GCShape Stenciling + словари. Это компромисс:
C++ Templates: Полная мономорфизация
+ Максимальная производительность
- Раздутый бинарник (code bloat)
- Долгая компиляция
Java Generics: Стирание типов (type erasure)
+ Один экземпляр кода
- Боксинг, нет типобезопасности в рантайме
Go Generics: GCShape Stenciling
+ Одна реализация для типов с одинаковой "формой GC"
+ Умеренный размер бинарника
- Небольшие накладные расходы (dictionary lookup)
GCShape -- это группировка типов по их поведению для сборщика мусора:
- Все указатели имеют одну GC-форму
- Каждый примитивный тип (int, float64, etc.) имеет свою GC-форму
// For pointer types — ONE instantiation shared:
type Cache[T any] struct {
items []*T // *int, *string, *MyStruct — same GC shape
}
// For value types — separate instantiation per type:
type Pair[T any] struct {
First T // int, float64, string — different GC shapes
Second T
}
На практике производительность дженериков в Go очень близка к конкретным типам. Разница в большинстве случаев менее 5% и заметна только в горячих циклах.