- проблема: одинаковый код для разных типов → дублирование (maxInt, maxUint — код идентичен, отличаются типы)
- обобщённое программирование = парадигма: пишешь код один раз, компилятор допишет для конкретных типов
- инстанцирование выполняется на этапе компиляции → сохраняется статическая типизация
- до Go 1.18: интерфейсы (теряем типобезопасность), рефлексия, unsafe, кодогенерация
- с Go 1.0 были встроенные обобщённые типы (map, chan, slice) и функции (new, make, len, close)
Проблема дублирования
// ❌ два раза один и тот же код
func maxInt(a, b int) int {
if a > b { return a }
return b
}
func maxUint(a, b uint) uint {
if a > b { return a }
return b
}
С дженериками (Go 1.18+)
// ✅ один раз
func max[T int | uint](a, b T) T {
if a > b { return a }
return b
}
max[int](1, 2) // явное инстанцирование
max(uint(1), 2) // компилятор выведет тип сам
Компилятор возьмёт обобщённый код и сгенерирует реализации для int и uint. Вы этот код не пишете — его пишет компилятор.
До 1.18 — как жили без дженериков
// Пустой интерфейс — теряем типобезопасность
func maxAny(a, b any) any { ... } // что вернётся? кто проверит типы?
// Кодогенерация — громоздко
//go:generate ...
88% респондентов опроса назвали отсутствие дженериков критической проблемой.
18% не использовали Go именно из-за отсутствия дженериков.
- constraint = интерфейс, ограничивающий допустимые типы для generic-параметра
- типы через
|на одной строке = OR (один из); на новых строках = AND (все условия) ~int(тильда) = int и все type definitions с базовым типом int- зарезервированные:
comparable(==, !=),any(любой тип) - экспериментальные:
constraints.Ordered,constraints.Integer,constraints.Float(пакетgolang.org/x/exp/constraints)
OR — типы на одной строке
type Number interface {
int | float64 | uint // ИЛИ: один из этих типов
}
Типы через | на одной строке означают, что тип должен быть одним из перечисленных.
AND — условия на разных строках
type MyConstraint interface {
~int | ~float64 // базовый тип int или float64
String() string // И у типа должен быть метод String()
fmt.Stringer // И должен реализовывать Stringer
}
Каждая строка — отдельное условие. Тип должен удовлетворять всем.
Тильда — базовый тип
type Integer1 interface { int | int8 | int16 } // только эти типы
type Integer2 interface { ~int | ~int8 | ~int16 } // + type definitions
type MyInt int // базовый тип = int
func process1[T Integer1](v T) {}
func process2[T Integer2](v T) {}
process1(MyInt(1)) // ❌ MyInt ≠ int
process2(MyInt(1)) // ✅ базовый тип MyInt = int
Без тильды принимаются только точные типы. С тильдой — также любой named type поверх базового.
Анонимные constraints (inline)
// Именованный constraint
func sum[T Number](a, b T) T { return a + b }
// Анонимный — прямо в сигнатуре
func sum[T interface{ int | float64 }](a, b T) T { return a + b }
// Сокращённая форма (без interface{})
func sum[T int | float64](a, b T) T { return a + b }
Inline constraint удобен для одноразовых ограничений; именованный — если переиспользуется.
comparable и any
func getKeys[K comparable, V any](m map[K]V) []K { ... }
// K — можно ==, != (ключи map)
// V — абсолютно любой тип
comparable — встроенный constraint: типы, поддерживающие == и !=. Нужен для ключей map и операций сравнения.
any — алиас для interface{}, принимает абсолютно любой тип.
- type inference — компилятор выводит типы из аргументов функции, не нужно указывать явно
- для структур не работает — всегда явно инстанцировать
- если нечего вывести (нет аргументов с generic-типом) — явно указывать
- можно пропускать типы-параметры только с начала (префикс), не из середины
- типы-параметры (объявление) vs типы-аргументы (вызов) — аналогия с параметрами/аргументами функций
Type inference работает
func print[T any](v T) { fmt.Println(v) }
print[int](100) // явно
print(100) // ✅ компилятор выведет int из аргумента
print("hello") // ✅ выведет string
Не работает: нет аргументов
func create[T any]() *T {
var v T
return &v
}
create() // ❌ компилятору нечего вывести
create[int]() // ✅ явно
// Даже если слева очевидный тип:
var p *int = create() // ❌ всё равно не выводит
Go не выводит тип из контекста присваивания — только из аргументов вызова.
Не работает: структуры
type Box[T any] struct { Value T }
Box{Value: 42} // ❌ не компилируется
Box[int]{Value: 42} // ✅ всегда явно для структур
Даже когда по полю очевидно, компилятор не догадывается.
Пропуск типов — только с начала
func process1[T comparable, K any, E int](a T, b K) E { ... }
// T и K выводятся из аргументов, E — нет
process1[int]("hello", 3.14) // ❌ int подставится в T, не в E
func process2[E int, T comparable, K any](a T, b K) E { ... }
// E первый — указываем явно, T и K выведутся
process2[int]("hello", 3.14) // ✅ E=int, T=string, K=float64
Правило: пропускать можно только префикс (первые N типов), которые выводятся. Из середины — нельзя.
- нет обобщённых методов — хак: функция с явным ресивером
- нельзя создать константу из generic-типа
- нельзя передавать числа как параметры типов (в отличие от C++)
- нельзя встроить (embed) generic-тип в структуру
- проблемы с указателями, map+slice нельзя объединить, string+byte — только чтение
- метод, объявленный на типе, но не в constraint — недоступен
Нет обобщённых методов
type MyStruct struct{}
// ❌ нельзя
func (s *MyStruct) Process[T any](v T) {}
// ✅ хак: обобщённая функция с явным ресивером
func Process[T any](s *MyStruct, v T) {
fmt.Println(v)
}
Go не позволяет объявить метод с собственными type-параметрами. Единственный обходной путь — вынести логику в обычную функцию с явным параметром-ресивером.
Нельзя создать константу
func process[T int](v T) {
var x T = 0 // ✅ переменная
const c T = 0 // ❌ даже когда тип один — нельзя
}
Константы из generic-типа запрещены даже если constraint ограничивает тип одним конкретным типом.
Нельзя передавать числа (не типы)
// C++ — можно: template<int N> struct Array { int data[N]; };
// Go — нельзя: параметры только типы
func makeArray[N int]() {} // ❌ нет такого синтаксиса
В Go параметры типов — только типы. Передать числовое значение как type-параметр (как в C++ NTTP) невозможно.
Нельзя embed generic-тип
type Wrapper[T any] struct {
T // ❌ встраивание generic-типа
Value T // ✅ обычное поле — работает
}
Встраивание (embedding) generic-типа как анонимного поля запрещено. Обходной путь — именованное поле.
Проблемы с указателями
type PtrConstraint interface { *int32 | *int64 }
func process[T PtrConstraint](v T) {} // ✅
// Но вот так не работает:
type NumConstraint interface { int32 | int64 }
func process[T *NumConstraint](v T) {} // ❌
// Обходной путь:
func process[T NumConstraint](v *T) {} // ✅
Нельзя использовать *InterfaceName как constraint. Правильный паттерн: принимать *T где T ограничен числовым constraint.
Map + slice — нельзя объединить
// ❌ не компилируется
func get[T map[string]int | []int](v T) int {
return v[0] // у map и slice разная семантика []
}
Причина: map возвращает (value, ok), slice — только value. Индексация [] для map и slice несовместима.
String + byte — только чтение
func first[T ~string | ~[]byte](v T) byte {
return v[0] // ✅ читать можно
}
func setFirst[T ~string | ~[]byte](v T) {
v[0] = 'x' // ❌ строка неизменяемая
}
string и []byte можно объединить в constraint для операций чтения. Запись через индекс не компилируется — строки иммутабельны.
Метод типа не в constraint — недоступен
type S struct{}
func (S) Foo() {}
func (S) Bar() {}
type C interface {
~struct{}
Foo() // только Foo в constraint
}
func process[T C](v T) {
v.Foo() // ✅
v.Bar() // ❌ Bar не в constraint, хотя у S есть
}
Компилятор знает только то, что гарантирует constraint. Даже если реальный тип имеет метод Bar, вызов v.Bar() — ошибка компиляции.
- структуры могут быть обобщёнными:
type Set[T comparable] struct { m map[T]struct{} } - методы обобщённой структуры — в ресивере указывается тип:
func (s *Set[T]) Add(v T) - type definitions могут быть обобщёнными и частично инстанцированными
- type aliases не могут быть обобщёнными (до Go 1.24)
- variadic + generics — функция с разным количеством аргументов разных типов при разных вызовах
Обобщённая структура
type Set[T comparable] struct {
m map[T]struct{}
}
func NewSet[T comparable]() *Set[T] {
return &Set[T]{m: make(map[T]struct{})}
}
// Метод — тип в ресивере
func (s *Set[T]) Add(v T) {
s.m[v] = struct{}{}
}
// Если тип не нужен — пропустить
func (s *Set[_]) Len() int {
return len(s.m)
}
// Использование — инстанцируем один раз
s := NewSet[string]() // дальше компилятор знает тип
s.Add("hello")
Структура параметризована типом T. В методах ресивер пишется как Set[T], а не Set. Если параметр типа в методе не нужен — можно использовать _.
Обобщённые type definitions
type Pair[T, U any] struct { First T; Second U }
// Полное инстанцирование
type StringIntPair = Pair[string, int]
// Частичное — через type definition (не alias!)
type StringPair[U any] Pair[string, U]
// StringPair[int] → Pair[string, int]
Частичное инстанцирование возможно только через type definition, не через type alias. StringPair[U any] фиксирует первый параметр как string, оставляя U свободным.
Type aliases — не обобщённые (до Go 1.24)
type MySlice[T any] []T // ✅ type definition
type MyAlias[T any] = []T // ❌ до Go 1.24
С Go 1.24 обобщённые aliases поддерживаются.
Прятать сложность за alias/definition
type IntSet = Set[int] // простой alias для пользователя
type OrderedMap[K comparable, V any] struct { ... } // внутри сложно
// Для constraints тоже:
type Numeric interface { ~int | ~float64 }
Техника: сложный обобщённый тип прячется за простым именем через alias или частичное инстанцирование.
Variadic + generics
func printAll[T any](vals ...T) {
for _, v := range vals { fmt.Println(v) }
}
printAll(1, 2, 3) // T=int, 3 аргумента
printAll("a", "b") // T=string, 2 аргумента
printAll(1.0, 2.0, 3.0, 4.0) // T=float64, 4 аргумента
Все аргументы одного типа (variadic), но тип и количество меняются от вызова к вызову. Тип выводится из первого аргумента.
Обобщённые constraints (template template parameters)
type Unsigned interface { ~uint | ~uint8 | ~uint16 }
type Slice[T any] []T
type SliceConstraint[E Unsigned] interface {
~[]E // срез из unsigned-элементов
}
func do[E Unsigned, S SliceConstraint[E]](s S) { ... }
Редкий случай — когда нужно достучаться до типа внутри обобщённого контейнера. S здесь — constraint, параметризованный другим type-параметром E. Аналог template template parameters из C++.
- сигнал: пишете один и тот же код несколько раз, отличаются только типы → дженерики
- идеальные кейсы: структуры данных (Set, Tree, Graph), функции контейнеров (sort, merge, filter)
- цена: рост бинарника (каждая специализация = новый код в ассемблере)
- цена: увеличение времени компиляции (генерация кода для каждого типа)
- редкий случай: компилятор может подставить интерфейс вместо инстанцирования (когда много типов)
- рантайм оверхед = ноль (код как если бы написали руками для конкретного типа)
Когда использовать
// ❌ дублирование — сигнал
func containsInt(s []int, v int) bool { ... }
func containsString(s []string, v string) bool { ... }
// ✅ дженерик
func contains[T comparable](s []T, v T) bool {
for _, item := range s {
if item == v { return true }
}
return false
}
Идеальные кейсы
Структуры данных: Set, Heap, BinaryTree, Graph — параметризованы типом элемента.
Функции контейнеров: Map, Filter, Reduce, Sort для срезов/каналов любых типов.
Обобщённые обёртки: unsafe.Pointer → типизированный, atomic.PointerT.
Рост бинарника
func sum[T int | float64](a, b T) T { return a + b }
sum[int](1, 2)
sum[float64](1.0, 2.0)
В ассемблере будут две функции: sum[int] и sum[float64] с разными инструкциями. Бинарник растёт линейно от количества инстанцирований.
Интерфейсный fallback
В редких случаях (много типов) компилятор Go может не инстанцировать, а подставить пустой интерфейс — чтобы не раздувать бинарник.
Это прозрачно для пользователя, но добавляет рантайм-оверхед (boxing).
Не усложняй
Дженерики могут сделать код сложнее. Если нет дублирования — не нужны. Не ищите место, куда «воткнуть дженерик».
В C++ — известная проблема: изучил шаблоны → пишешь всё на шаблонах → через час не разберёшься.
- свойство 1: от интерфейса → к reflect-объекту (
TypeOf,ValueOf) - свойство 2: от reflect-объекта → к интерфейсу (метод
Interface()) - свойство 3: для модификации значение должно быть settable (передать указатель +
Elem()) CanSet()/CanAddr()— проверка: можно ли менять значение- копия значения — не settable, нужен указатель → Elem() → тогда settable
Свойство 1: интерфейс → reflect
var x float64 = 3.14
// Неявное преобразование x → any → анализ itab + data
t := reflect.TypeOf(x) // reflect.Type
v := reflect.ValueOf(x) // reflect.Value
Всё начинается с неявного преобразования в пустой интерфейс. TypeOf возвращает reflect.Type, ValueOf — reflect.Value.
Свойство 2: reflect → интерфейс
var x float64 = 3.14
v := reflect.ValueOf(x)
// Обратно: reflect.Value → interface{} → float64
y := v.Interface().(float64)
fmt.Println(y) // 3.14
Метод Interface() упаковывает тип и значение обратно в интерфейс. Это обратная операция к ValueOf.
Свойство 3: settable — для модификации нужен указатель
var x float64 = 3.14
// ❌ Копия — нельзя менять
v := reflect.ValueOf(x)
fmt.Println(v.CanSet()) // false
v.SetFloat(2.0) // ПАНИКА: using unaddressable value
// ❌ Указатель — тоже копия (копия указателя)
v2 := reflect.ValueOf(&x)
fmt.Println(v2.CanSet()) // false — сам указатель копия
// ✅ Указатель + Elem() — разыменование → оригинал
v3 := reflect.ValueOf(&x).Elem()
fmt.Println(v3.CanSet()) // true!
v3.SetFloat(2.0)
fmt.Println(x) // 2.0 — оригинал изменился
ValueOf(x) — копия, CanSet: false. ValueOf(&x) — копия указателя, CanSet: false. Только ValueOf(&x).Elem() даёт доступ к оригиналу, CanSet: true.
Вызов SetFloat (и любого SetXxx) на non-settable значении вызывает панику с текстом using unaddressable value.
Почему так
Аналогия с функциями: если передал значение — функция работает с копией. Если хочешь менять — передай указатель. Рефлексия работает по тому же принципу.
ValueOf(x) → копия x → CanSet: false
ValueOf(&x) → копия указателя → CanSet: false
ValueOf(&x).Elem() → оригинал x → CanSet: true
Интерфейсы — дополнительный Elem()
var z int = 42
var iface any = &z
v := reflect.ValueOf(iface) // any → копия указателя
v = v.Elem() // разыменование интерфейса → *int
v = v.Elem() // разыменование указателя → int
v.SetInt(100)
fmt.Println(z) // 100
Когда значение лежит в интерфейсе — нужен дополнительный Elem() для разыменования интерфейсного слоя, и ещё один для разыменования указателя. Итого два Elem() вместо одного.
- анализ структур: поля (NumField, Field), методы (NumMethod, Method), параметры (NumIn, NumOut)
- создание типов: структуры, массивы, указатели, каналы — всё через reflect
- вызов функций и методов:
MethodByName("Add").Call(args) - проверки: тип реализует интерфейс (Implements), тип comparable
- каналы и select через рефлексию: Send, Recv, Select с кейсами
- nil →
reflect.Valueс Kind ==Invalid
Анализ структур
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func (u User) Greet() string { return "Hi, " + u.Name }
t := reflect.TypeOf(User{})
t.Kind() // struct
t.NumField() // 2
t.Field(0).Name // "Name"
t.Field(0).Type.String() // "string"
t.NumMethod() // 1
t.Method(0).Name // "Greet"
t.Method(0).Type.NumIn() // 1 (ресивер)
t.Method(0).Type.NumOut() // 1
Для метода NumIn() возвращает количество входных параметров включая ресивер.
Создание типов
// Структура
fields := []reflect.StructField{
{Name: "X", Type: reflect.TypeOf(0)},
{Name: "Y", Type: reflect.TypeOf("")},
}
structType := reflect.StructOf(fields)
// Массив
arrayType := reflect.ArrayOf(5, reflect.TypeOf(0))
// Указатель
ptrType := reflect.PointerTo(reflect.TypeOf(0))
Каналы создаются через reflect.MakeChan с указанием направления и буфера.
Вызов методов и функций
type Vec struct{ X, Y int }
func (v Vec) Add(other Vec) Vec { return Vec{v.X + other.X, v.Y + other.Y} }
v := reflect.ValueOf(Vec{1, 2})
method := v.MethodByName("Add")
args := []reflect.Value{reflect.ValueOf(Vec{3, 4})}
result := method.Call(args)
fmt.Println(result[0]) // {4 6}
// Вызов обычной функции
fn := reflect.ValueOf(fmt.Sprintf)
res := fn.Call([]reflect.Value{
reflect.ValueOf("hello %s"),
reflect.ValueOf("world"),
})
MethodByName принимает имя метода как строку, возвращает reflect.Value типа функции. Ресивер уже связан — в args передаём только остальные аргументы.
Проверка implements
type Stringer interface{ String() string }
dataType := reflect.TypeOf(Data{})
stringerType := reflect.TypeOf((*Stringer)(nil)).Elem()
dataType.Implements(stringerType) // true/false
Nil нужен только для получения reflect.Type интерфейса — не создаём реальный объект.
Каналы и select
ch := reflect.MakeChan(reflect.ChanOf(reflect.BothDir, reflect.TypeOf(0)), 1)
ch.Send(reflect.ValueOf(42))
val, ok := ch.TryRecv()
// Select
cases := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: ch},
{Dir: reflect.SelectDefault},
}
chosen, value, _ := reflect.Select(cases)
reflect.Select принимает срез SelectCase — это позволяет делать select с динамическим количеством каналов, что невозможно через обычный select.
Nil и Invalid
v := reflect.ValueOf(nil)
fmt.Println(v.IsValid()) // false
fmt.Println(v.Kind()) // invalid
// Элемент nil-среза
s := reflect.ValueOf([]int(nil))
elem := s.Index(0) // паника!
IsValid() возвращает false если Value — нулевое значение (создано из nil или дефолтным конструктором). Вызов любого метода на невалидном Value кроме IsValid, Kind, String — паника.
- сериализация/десериализация: encoding/json, encoding/xml — теги + рефлексия
- валидаторы: проверка полей по тегам (go-playground/validator, 17k+ stars)
- ORM: маппинг структур на таблицы, автоматический scan
- DI-контейнеры: внедрение зависимостей по типу
- дженерики + рефлексия вместе: обобщённый код на двух уровнях
- изменение приватных полей: unsafe.Pointer + reflect (крайний случай)
Сериализация — главный кейс
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
}
// encoding/json внутри:
// 1. reflect.TypeOf(user) → перебрать поля
// 2. field.Tag.Get("json") → получить имя в JSON
// 3. reflect.ValueOf(user).Field(i) → получить значение
// 4. собрать JSON
data, _ := json.Marshal(User{Name: "Alice", Age: 30})
// {"name":"Alice","age":30}
encoding/json использует рефлексию для перебора полей структуры, чтения тегов и получения значений. Без рефлексии пришлось бы для каждой структуры писать явную сериализацию.
Валидаторы
type CreateUser struct {
Email string `validate:"required,email"`
Age int `validate:"gte=18,lte=120"`
}
// Библиотека validator:
// 1. reflect → перебрать поля
// 2. прочитать тег validate
// 3. распарсить правила
// 4. проверить значение каждого поля
Примеры популярных библиотек: go-playground/validator (17k+ stars), swagger (10k+ stars). Оба используют рефлексию для чтения тегов и значений полей.
ORM
ORM-библиотеки (gorm, sqlx и др.) используют рефлексию для маппинга полей структур на колонки таблиц и автоматического scan результатов запроса в Go-структуры.
DI-контейнеры
DI-контейнеры используют рефлексию для анализа типов зависимостей функций-конструкторов и автоматического внедрения зарегистрированных зависимостей по типу.
Обобщённый код в рантайме
// Функция bind — привязывает обобщённую реализацию к конкретному типу
// Используя рефлексию: анализ типов аргументов → MakeFunc → подмена
func invertSlice(s any) any {
// рефлексия: определить тип элемента, развернуть срез
}
var invertInts func([]int) []int
bind(&invertInts, invertSlice) // привязка через рефлексию
invertInts([]int{1, 2, 3}) // [3, 2, 1]
Метапрограммирование в рантайме: один алгоритм, разные типы — через рефлексию. Ключевой инструмент — reflect.MakeFunc.
Дженерики + рефлексия вместе
// Generic (compile-time) + reflect (runtime)
func AssignPrivateField[T any](obj *T, field string, value any) {
v := reflect.ValueOf(obj).Elem()
f := v.FieldByName(field)
ptr := unsafe.Pointer(f.UnsafeAddr())
*(*T)(ptr) = value.(T) // ← generic + unsafe
}
Два уровня метапрограммирования в одной функции: дженерики работают на этапе компиляции (type parameters), рефлексия — в рантайме. Для изменения приватных полей требуется unsafe.Pointer + UnsafeAddr().
- производительность: рефлексия существенно медленнее прямого кода
- сложность: огромное API (десятки методов), код трудно читать и писать
- паники в рантайме: SetFloat на non-settable, Elem на non-pointer, Index на nil-slice
- GC-ловушка:
Value.Pointer()возвращает uintptr — GC может собрать объект - ограничения: нельзя создать интерфейсный тип (до 1.22), нельзя создать type definition
Производительность
// Прямой вызов
user.Name = "Alice"
// Через рефлексию — в разы медленнее
v := reflect.ValueOf(&user).Elem()
v.FieldByName("Name").SetString("Alice")
Рефлексия = аллокации, type assertions, поиск по имени. На горячих путях — критично.
Сложность кода
// Прямой код — понятно
result := vec.Add(Vec{3, 4})
// Рефлексия — «гуглить надо»
v := reflect.ValueOf(vec)
method := v.MethodByName("Add")
args := []reflect.Value{reflect.ValueOf(Vec{3, 4})}
result := method.Call(args)
У reflect.Value — десятки методов. Запомнить нереально, всегда гуглить.
Паники в рантайме
// Попытка изменить копию
v := reflect.ValueOf(3.14)
v.SetFloat(2.0) // ПАНИКА: using unaddressable value
// Лишний Elem()
v := reflect.ValueOf(42)
v.Elem() // ПАНИКА: call of reflect.Value.Elem on int Value
// Index на nil-slice
s := reflect.ValueOf([]int(nil))
s.Index(0) // ПАНИКА
Рефлексия переносит ошибки из compile-time в runtime.
GC-ловушка: Pointer() возвращает uintptr
v := reflect.ValueOf(&obj)
ptr := v.Pointer() // uintptr — просто число!
// Между этими строками GC может собрать obj,
// потому что uintptr НЕ удерживает объект
runtime.GC()
// ptr теперь указывает на освобождённую память!
use(unsafe.Pointer(ptr)) // 💥 use-after-free
Value.Pointer() возвращает uintptr — это просто целое число, GC не считает его ссылкой на объект.
Решение: UnsafePointer()
v := reflect.ValueOf(&obj)
ptr := v.UnsafePointer() // unsafe.Pointer — удерживает объект
// GC НЕ соберёт obj, пока ptr жив
runtime.GC()
use(ptr) // ✅ безопасно
UnsafePointer() возвращает unsafe.Pointer — GC видит эту ссылку и не собирает объект.
Ограничения рефлексии
Планировалось: всё что можно написать без рефлексии — можно и с ней. Почти получилось, но есть исключения:
Нельзя создать интерфейсный тип через reflect (до Go 1.22).
Нельзя создать новый type definition через reflect.
Проблемы с анонимными полями в структурах.
- теги = ключ-значение пары в бэктиках после поля структуры
- доступ через рефлексию:
Field(i).Tag.Get("json")илиLookup("json") Lookupвозвращает(value, ok)— можно отличить пустой тег от отсутствующего- парсинг значения (omitempty, name) — вручную, рефлексия отдаёт только строку
- основа для JSON/XML сериализации, валидаторов, ORM
Синтаксис тегов
type User struct {
Name string `json:"name" xml:"user_name"`
Email string `json:"email,omitempty"`
Age int `json:"-"` // игнорировать при сериализации
}
Формат: ключ:"значение" через пробел. Значение — произвольная строка, парсинг на вашей стороне. Тег json:"-" означает «игнорировать поле при сериализации».
Одному полю можно назначить несколько тегов для разных пакетов: json:"name" xml:"user_name" — encoding/json прочитает свой тег, encoding/xml — свой.
Чтение тегов через рефлексию
t := reflect.TypeOf(User{})
// Поле Name
field0 := t.Field(0)
fmt.Println(field0.Tag) // json:"name" xml:"user_name"
fmt.Println(field0.Tag.Get("json")) // "name"
fmt.Println(field0.Tag.Get("xml")) // "user_name"
// Поле Email
field1 := t.Field(1)
fmt.Println(field1.Tag.Get("json")) // "email,omitempty"
Доступ к тегам — через reflect.TypeOf, а не ValueOf: теги — это метаданные типа, а не значения. field.Tag возвращает полную строку тегов, Tag.Get(key) — значение для конкретного ключа.
Lookup vs Get
// Get — вернёт "" и для пустого тега, и для отсутствующего
val := field.Tag.Get("missing") // "" — непонятно, есть тег или нет
// Lookup — различает
val, ok := field.Tag.Lookup("json")
if ok {
// тег есть, val может быть ""
} else {
// тега нет
}
Get не позволяет отличить отсутствующий тег от тега с пустым значением — оба вернут "". Lookup возвращает (value, ok) — ok=false означает, что тега нет совсем.
Парсинг значения — вручную
tag := field.Tag.Get("json") // "email,omitempty"
// Рефлексия отдаёт строку целиком, дальше — сами
parts := strings.Split(tag, ",")
name := parts[0] // "email"
omitempty := len(parts) > 1 && parts[1] == "omitempty" // true
Рефлексия возвращает значение тега как единую строку — разбор опций (omitempty, name и т.д.) целиком лежит на вызывающем коде. encoding/json делает это внутри себя.
Как это использует encoding/json
// Упрощённо: что делает json.Marshal внутри
t := reflect.TypeOf(obj)
v := reflect.ValueOf(obj)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("json")
value := v.Field(i)
// парсим tag, сериализуем value...
}
json.Marshal перебирает поля через TypeOf (для тегов) и ValueOf (для значений) в одном цикле по индексу.
- интроспекция — исследовать тип/свойства объекта в рантайме (только чтение)
- рефлексия — исследовать и модифицировать структуру и поведение в рантайме
- рефлексия = метапрограммирование в рантайме (дженерики = в compile-time)
- два базовых типа:
reflect.Type(TypeOf) иreflect.Value(ValueOf) - Kind vs Type: kind = базовый тип (int), type = конкретный (MyInt). Для alias: kind == type
Интроспекция vs рефлексия
Интроспекция: «что это за объект? какие у него поля? методы?»
Рефлексия: «что это за объект? + давай поменяем ему поля и вызовем методы»
Go поддерживает полную рефлексию через пакет reflect.
reflect.Type и reflect.Value
var x float64 = 3.14
t := reflect.TypeOf(x) // reflect.Type — информация о типе
v := reflect.ValueOf(x) // reflect.Value — информация о значении
fmt.Println(t) // float64
fmt.Println(v) // 3.14
fmt.Println(v.Type()) // float64
Под капотом: TypeOf и ValueOf принимают any → неявное преобразование в пустой интерфейс → анализ двух указателей интерфейса (itab + data).
Kind vs Type
type MyInt int
var a int = 42
var b MyInt = 42
// alias (type MyAlias = int)
ta := reflect.TypeOf(a)
fmt.Println(ta.Name(), ta.Kind()) // int, int — совпадают
// type definition (type MyInt int)
tb := reflect.TypeOf(b)
fmt.Println(tb.Name(), tb.Kind()) // MyInt, int — различаются!
Kind возвращает базовый тип (enum: int, string, struct, ptr, slice...). Type возвращает конкретный тип. Для type definition: Kind = базовый, Type = определённый.
Kind для проверок в коде
v := reflect.ValueOf(x)
switch v.Kind() {
case reflect.Int:
fmt.Println("integer:", v.Int())\ncase reflect.Float64:
fmt.Println("float:", v.Float())\ncase reflect.String:
fmt.Println("string:", v.String())
}
API: самый большой тип
var x uint8 = 42
v := reflect.ValueOf(x)
// Возвращает uint64 (самый большой), не uint8
val := v.Uint() // uint64
Для простоты API: getter-методы Value возвращают максимальный тип семейства (Int() → int64, Uint() → uint64, Float() → float64). Информация о реальном типе сохраняется в Kind/Type.