• проблема: одинаковый код для разных типов → дублирование (maxInt, maxUint — код идентичен, отличаются типы)
  • обобщённое программирование = парадигма: пишешь код один раз, компилятор допишет для конкретных типов
  • инстанцирование выполняется на этапе компиляции → сохраняется статическая типизация
  • до Go 1.18: интерфейсы (теряем типобезопасность), рефлексия, unsafe, кодогенерация
  • с Go 1.0 были встроенные обобщённые типы (map, chan, slice) и функции (new, make, len, close)

Проблема дублирования

go
// ❌ два раза один и тот же код 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+)

go
// ✅ один раз 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 — как жили без дженериков

go
// Пустой интерфейс — теряем типобезопасность 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 — типы на одной строке

go
type Number interface { int | float64 | uint // ИЛИ: один из этих типов }

Типы через | на одной строке означают, что тип должен быть одним из перечисленных.

AND — условия на разных строках

go
type MyConstraint interface { ~int | ~float64 // базовый тип int или float64 String() string // И у типа должен быть метод String() fmt.Stringer // И должен реализовывать Stringer }

Каждая строка — отдельное условие. Тип должен удовлетворять всем.

Тильда — базовый тип

go
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)

go
// Именованный 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

go
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 работает

go
func print[T any](v T) { fmt.Println(v) } print[int](100) // явно print(100) // ✅ компилятор выведет int из аргумента print("hello") // ✅ выведет string

Не работает: нет аргументов

go
func create[T any]() *T { var v T return &v } create() // ❌ компилятору нечего вывести create[int]() // ✅ явно // Даже если слева очевидный тип: var p *int = create() // ❌ всё равно не выводит

Go не выводит тип из контекста присваивания — только из аргументов вызова.

Не работает: структуры

go
type Box[T any] struct { Value T } Box{Value: 42} // ❌ не компилируется Box[int]{Value: 42} // ✅ всегда явно для структур

Даже когда по полю очевидно, компилятор не догадывается.

Пропуск типов — только с начала

go
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 — недоступен

Нет обобщённых методов

go
type MyStruct struct{} // ❌ нельзя func (s *MyStruct) Process[T any](v T) {} // ✅ хак: обобщённая функция с явным ресивером func Process[T any](s *MyStruct, v T) { fmt.Println(v) }

Go не позволяет объявить метод с собственными type-параметрами. Единственный обходной путь — вынести логику в обычную функцию с явным параметром-ресивером.

Нельзя создать константу

go
func process[T int](v T) { var x T = 0 // ✅ переменная const c T = 0 // ❌ даже когда тип один — нельзя }

Константы из generic-типа запрещены даже если constraint ограничивает тип одним конкретным типом.

Нельзя передавать числа (не типы)

go
// C++ — можно: template<int N> struct Array { int data[N]; }; // Go — нельзя: параметры только типы func makeArray[N int]() {} // ❌ нет такого синтаксиса

В Go параметры типов — только типы. Передать числовое значение как type-параметр (как в C++ NTTP) невозможно.

Нельзя embed generic-тип

go
type Wrapper[T any] struct { T // ❌ встраивание generic-типа Value T // ✅ обычное поле — работает }

Встраивание (embedding) generic-типа как анонимного поля запрещено. Обходной путь — именованное поле.

Проблемы с указателями

go
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 — нельзя объединить

go
// ❌ не компилируется func get[T map[string]int | []int](v T) int { return v[0] // у map и slice разная семантика [] }

Причина: map возвращает (value, ok), slice — только value. Индексация [] для map и slice несовместима.

String + byte — только чтение

go
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 — недоступен

go
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 — функция с разным количеством аргументов разных типов при разных вызовах

Обобщённая структура

go
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

go
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)

go
type MySlice[T any] []T // ✅ type definition type MyAlias[T any] = []T // ❌ до Go 1.24

С Go 1.24 обобщённые aliases поддерживаются.

Прятать сложность за alias/definition

go
type IntSet = Set[int] // простой alias для пользователя type OrderedMap[K comparable, V any] struct { ... } // внутри сложно // Для constraints тоже: type Numeric interface { ~int | ~float64 }

Техника: сложный обобщённый тип прячется за простым именем через alias или частичное инстанцирование.

Variadic + generics

go
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)

go
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)
  • цена: рост бинарника (каждая специализация = новый код в ассемблере)
  • цена: увеличение времени компиляции (генерация кода для каждого типа)
  • редкий случай: компилятор может подставить интерфейс вместо инстанцирования (когда много типов)
  • рантайм оверхед = ноль (код как если бы написали руками для конкретного типа)

Когда использовать

go
// ❌ дублирование — сигнал 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.

Рост бинарника

go
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

go
var x float64 = 3.14 // Неявное преобразование x → any → анализ itab + data t := reflect.TypeOf(x) // reflect.Type v := reflect.ValueOf(x) // reflect.Value

Всё начинается с неявного преобразования в пустой интерфейс. TypeOf возвращает reflect.Type, ValueOfreflect.Value.

Свойство 2: reflect → интерфейс

go
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 — для модификации нужен указатель

go
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.

Почему так

Аналогия с функциями: если передал значение — функция работает с копией. Если хочешь менять — передай указатель. Рефлексия работает по тому же принципу.

text
ValueOf(x) → копия x → CanSet: false ValueOf(&x) → копия указателя → CanSet: false ValueOf(&x).Elem() → оригинал x → CanSet: true

Интерфейсы — дополнительный Elem()

go
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

Анализ структур

go
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() возвращает количество входных параметров включая ресивер.

Создание типов

go
// Структура 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 с указанием направления и буфера.

Вызов методов и функций

go
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

go
type Stringer interface{ String() string } dataType := reflect.TypeOf(Data{}) stringerType := reflect.TypeOf((*Stringer)(nil)).Elem() dataType.Implements(stringerType) // true/false

Nil нужен только для получения reflect.Type интерфейса — не создаём реальный объект.

Каналы и select

go
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

go
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 (крайний случай)

Сериализация — главный кейс

go
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 использует рефлексию для перебора полей структуры, чтения тегов и получения значений. Без рефлексии пришлось бы для каждой структуры писать явную сериализацию.

Валидаторы

go
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-контейнеры используют рефлексию для анализа типов зависимостей функций-конструкторов и автоматического внедрения зарегистрированных зависимостей по типу.

Обобщённый код в рантайме

go
// Функция 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.

Дженерики + рефлексия вместе

go
// 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

Производительность

go
// Прямой вызов user.Name = "Alice" // Через рефлексию — в разы медленнее v := reflect.ValueOf(&user).Elem() v.FieldByName("Name").SetString("Alice")

Рефлексия = аллокации, type assertions, поиск по имени. На горячих путях — критично.

Сложность кода

go
// Прямой код — понятно 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 — десятки методов. Запомнить нереально, всегда гуглить.

Паники в рантайме

go
// Попытка изменить копию 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

go
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()

go
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

Синтаксис тегов

go
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 — свой.

Чтение тегов через рефлексию

go
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

go
// Get — вернёт "" и для пустого тега, и для отсутствующего val := field.Tag.Get("missing") // "" — непонятно, есть тег или нет // Lookup — различает val, ok := field.Tag.Lookup("json") if ok { // тег есть, val может быть "" } else { // тега нет }

Get не позволяет отличить отсутствующий тег от тега с пустым значением — оба вернут "". Lookup возвращает (value, ok)ok=false означает, что тега нет совсем.

Парсинг значения — вручную

go
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

go
// Упрощённо: что делает 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 рефлексия

text
Интроспекция: «что это за объект? какие у него поля? методы?» Рефлексия: «что это за объект? + давай поменяем ему поля и вызовем методы»

Go поддерживает полную рефлексию через пакет reflect.

reflect.Type и reflect.Value

go
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

go
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 для проверок в коде

go
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: самый большой тип

go
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.