
Всем доброе время суток. Я пишу всякое на Go в Ви.Tech (IT-дочка ВсеИнструменты.ру) и, честно говоря, обожаю этот язык. Когда говорят о проблемах Go, обычно вспоминают отсутствие наследования или своеобразную обработку ошибок. Гораздо реже речь заходит о том, что, на мой взгляд, действительно можно отнести к проблемам.
Проблема
Go предоставляет теги структур, которые можно обнаруживать с помощью рефлексии. Они широко используются в пакетах кодирования JSON/XML, парсерах флагов, ORM и других библиотеках.
Реализация проста и минималистична - сами теги являются непрозрачными строками с точки зрения компилятора. Именно этот минимализм и приводит к незапланированным сложностям.
Пакет reflect определяет условный формат key-value для тегов. По соглашению строка тега представляет собой конкатенацию пар ключ:"значение", которые могут быть разделены пробелами. Каждый ключ - это непустая строка, состоящая из непробельных управляющих символов (кроме пробела, кавычек и двоеточия). Каждое значение заключается в кавычки и использует синтаксис строковых литералов.
type S struct {
F string `species:"gopher" color:"blue"`
}
s := S{}
st := reflect.TypeOf(s)
field := st.Field(0)
fmt.Println(field.Tag.Get("color"), field.Tag.Get("species"))
// >> blue gopher
Пакеты поверх этого вводят собственные правила для значений тегов.
// encoding/json - запятая как разделитель
type UserJSON struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Age int `json:"age,omitempty,string"`
}
// gorm - точка с запятой как разделитель
type UserGORM struct {
ID uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);not null`
Age int `gorm:"column:user_age;check:age>0"`
}
Некоторые опции имеют собственный дополнительный синтаксис, интерпретация которого может зависеть от типа значения.
Возвращаясь к примеру выше:
Name string `gorm:"type:varchar(100);not null`
gorm внутри значения тега использует дополнительный слой key-value с двоеточием в качестве разделителя. Многие другие пакеты для тех же целей используют знак равенства.
Таким образом, у нас накапливается слоистая структура синтаксиса:
мини-язык пакета reflect;
микро-язык конкретного пакета;
нано- и даже пико-языки для вложенных значений.

Это делает синтаксис всё более запутанным и повышает когнитивную нагрузку на разработчика.
// Элементарно пропустить кавычки
type Oops struct {
Text string `json:text`
}
Поскольку с точки зрения компилятора тег - это просто строка, он не выдаст ошибку, и приложение запустится, оставив разработчика разбираться, почему маршаллинг работает неправильно. Да, здесь мог бы помочь статический анализ, но не каждый пакет имеет линтер, а даже если он есть - его могут просто не запускать.
Многие пакеты позволяют использовать в тегах внешние языки:
gorm разрешает SQL,
cuego позволяет вставлять выражения на CUE,
participle поддерживает собственную грамматику в стиле EBNF.
// страшно, да?
type Group struct {
Expression *Expression `"(" @@ ")"`
}
type Option struct {
Expression *Expression `"[" @@ "]"`
}
type Repetition struct {
Expression *Expression `"{" @@ "}"`
}
type Literal struct {
Start string `@String`
End string `("…" @String)?`
}
В попытке навести хоть какой-то порядок и задокументировать известные теги структур, используемые публичными пакетами, команда Go опубликовала материал Well Known Tags
Tag |
Documentation |
|---|---|
asn1 |
|
bigquery |
|
bson |
|
cue |
|
datastore |
|
db |
|
dynamodbav |
https://docs.aws.amazon.com/sdk-for-go/api/service/dynamodb/dynamodbattribute/#Marshal |
egg |
|
feature |
|
gorm |
|
graphql |
|
json |
|
mapstructure |
|
parser |
|
properties |
https://pkg.go.dev/github.com/magiconair/properties#Properties.Decode |
protobuf |
|
reform |
|
spanner |
|
toml |
|
url |
|
validate |
|
xml |
|
yaml |
Не говоря уже о том, что список устарел (например, в нём до сих пор ссылка на yaml.v2). В целом он слабо помогает из-за недостатка документации и разнообразия подходов в самих пакетах. Вообще, как только мы выходим за пределы стандартной библиотеки, всё становится чудесатее и чудесатее.
Многие библиотеки резервируют сразу набор тегов, повышая риск конфликтов из-за отсутствия пространств имён для ключей тегов (json, yaml и т. д.).
Например, thunder резервирует graphql, sqlgen, livesql и sql.
go-querystring резервирует url, layout и del.
Некоторые пакеты позволяют во время выполнения переопределять имя ключа тега, что, безусловно, похвально с точки зрения предотвращения конфликтов, но делает статическую проверку невозможной. Кроме того, допустимые значения тегов могут изменяться в рантайме - тот же gorm позволяет регистрировать пользовательские сериализаторы.
Что мы имеем в итоге?
зоопарк в синтаксисе опций тегов;
ещё больший зоопарк в способах записи параметров;
отсутствие пространств имён;
компилятор нам не помощник, IDE и линтеры в большинстве случаев тоже;
автодополнение? Забудьте;
экосистема никак не стимулирует авторов библиотек писать хорошую документацию по тегам.
Что можно сделать ?
Я с лета слежу за Proposal 74472 По результатам последней встречи Language Proposal Review Group он был переведён в Proposal-Hold с пока неясными перспективами.
This proposal has a lot going for it. Perhaps most appealing is that it resolves ambiguity around the tag namespace.
We generally agree that it would make the type of metaprogramming that struct tags enable safer and easier to use.
...but are worried that this would make this style of programming more prevalent and complicated.
Perhaps we don't need to resolve the question of annotations before deciding on this proposal, because generalized annotations would enable a variety of new programming styles, whereas this just adds structure and safety to an existing style.
Furthermore, the proposed syntax is natural. There was some concern about 'brace confusion', but what else would we use for something that is so conceptually similar to a slice of values? Others have suggested using
@in various ways, which would probably be important to consider if this were to generalize to annotations. I think we have more to unpack here.I think, on balance, this would have been a good design for struct tags, but we're not yet convinced that the churn it would cause is worth it.
(с) Robert Findley
Несмотря на отдельные спорные моменты, я считаю этот proposal крайне перспективным и практически уверен, что в том или ином виде он вернётся в обсуждение в ближайшее время.
В чем суть
Предлагается расширить синтаксис тегов структур, разрешив, помимо строковых литералов, использовать список константных выражений, заключённый в фигурные скобки {}.
Форма нового синтаксиса:
{ ConstExpr1, ConstExpr2, ... }
Где:
каждый элемент — это константное выражение;
-
константа может быть:
числом,
строкой,
типизированной константой (например, json.OmitEmpty),
результатом вызова константной функции (например, json.Name("foo"));
все выражения должны быть compile-time константами;
допускается смешивание строковых и типизированных значений в одном списке.
Пакет encoding/json в новой версии (v2) мог бы объявлять типы тегов примерно так:
package json
type Name string
func Name(s string) Name { return Name(s) }
type OmitEmpty struct{}
type OmitZero struct{}
type Ignore struct{}
type IgnoreCase struct{}
type Format string
func Format(layout string) Format { return Format(layout) }
Тогда использование выглядело бы так:
// Компилятор проверит:
// – json.OmitEmpty действительно существует,
// – json.Format(...) получает корректный аргумент,
// – отсутствуют синтаксические ошибки.
type Example struct {
ID int {json.Name("id"), json.OmitEmpty}
CreatedAt time.Time {json.Format("2006-01-02")}
}
Вместо парсинга текстовых тегов (reflect.StructTag) пакеты смогут определять собственные типы тегов и работать с ними как с compile-time объектами.
Опечатки, синтаксические ошибки и недопустимые значения будут выявляться компилятором.
Появится автодополнение в IDE.
Благодаря пространствам имён по пакетам конфликты станут невозможны.
Исчезнет необходимость в сложном разборе строк в рантайме.
Существенно снизится количество синтаксических ошибок.
Явное объявление тегов будет подталкивать (а в сочетании с линтерами — практически вынуждать) разработчиков писать документацию.
При этом proposal сохраняет обратную совместимость. В пакете reflect в дополнение к существующему StructTag, хранящему строковые теги, планируется добавить поле StructTags, содержащее список значений тегов в виде []any.
// Старый синтаксис можно использовать паралельно с новым
F int `yaml:"x"` {json.Name("x")}
Естественно, потребуется переходный период, чтобы экосистема начала использовать новый синтаксис. Библиотеки будут постепенно добавлять типизированные теги наряду со строковыми, а приложения — переписывать теги в новую форму по мере необходимости.
Несмотря на некоторую непривычность, на мой взгляд, плюсы типизированных тегов неоспоримы. В этой части языка давно стоило навести порядок.
Буду продолжать следить за ситуацией вокруг этого proposal и, как писал выше, практически не сомневаюсь, что заложенные в нём идеи так или иначе будут реализованы.
Комментарии (11)

Berrserker
23.12.2025 19:00Может быть пора отпукстить пхп, джаву горм в могилу и перестать тащить ненужные в го идеи - и использовать теги для того для чего они были созданы = перегрузка имени поля? Мне искренне жаль что в теги лезет все что угодно.

sl4mmer Автор
23.12.2025 19:00Ну по поводу ORM, соглашусь плностью. Мне кажется люди приходят с других языков и такие, хм а где ту орм? надо написать.
По поводу тегов ну их уже используют, кто как кому не лень, ну и во многих вещах(например форматы кодирования) определенно есть смысл - типизированные теги, хотя бы навели некоторый порядок

manyakRus
23.12.2025 19:00С тегами итак всё хорошо, не надо их трогать.
Можно сделать "стандартизацию" тегов чтоб было у всех похоже
Можно сделать проверку синтаксиса тегов, только не линтерами, а каждая внешняя библиотека сама будет описывать список своих тегов.

Octagon77
23.12.2025 19:00Чем мне нравится Go, так это взрослым подходом свойственным профессионалам. Это не Rust какой-нибудь, где авторы выглядят как способные, может быть даже талантливые, дети. Как пример - дженерики. 12 лет их не было потому, что было не понятно как их сделать нормально. Такой же подход был, отчасти и есть, у Эппл. Например, если нет достойного iOS калькулятора - значит в iOS калькулятора не будет.
О тегах начну с предполагаемых причин. Кому-то было неудобно парсить JSON и он придумал теги - из шкурных интересов решил нагадить всем. Как называется такая личность? Правильно, сволочь она называется.
Почему нагадить? Потому, что это значит городить второй язык поверх Go. Вот с этим делом и проблемы, дотпространств имён докатились. Это безумие, как бы кому ни казалось что лично ему профит выйдет.
Почему сейчас не ужас? А упомянутый выше профессионализм (пока) спас, думается мне. Тэги как они есть - не мешают никому, ну есть и есть. Более того, чтобы узнать, есть ли от них польза, другого способа чем попробывать - нету. Попробывали, есть польза, вот JSON парсить приспособили - теперь лучше понимаем как его парсить, положительный результат достигнут.
Но нармально парсить через теги - безумие, структура сама по себе, парсер сам по себе, смешение понятий - детский сад. По предложениям стандартизировать теги видно - чего хочется достичь понятно. Ещё раз - в том числе потому понятно, что с тэгами поиграли, спасибо им. Но рабочий механизм будьте добры реализовать в другом месте.
И ещё раз, для верности. Стандартизация и прочая дурь вокруг тегов а) смешивает концепты и б) мешает другим способом использовать тэги чтобы разобраться ещё с чем нибудь ограничивая свободу их использования.
Пример аналогичного этим предложениям, как по мне весьма, безумия из Rust - времена жизни в определении процедуры. Процедура не может интересоваться временами жизни что параметров, что возвращаемого значения - это автономная сущность. Если нужно обязательно делать ещё одну проверку не дождавшись пока компилятор поумнеет достаточно для того, чтобы её можно было реализовать нормально - пишите свои времена жизни там, где они не выглядят творением школьного кружка, при вызове функции, например. Или при объявлении переменной...
Премного я опечалился сию статью прочитав. Линуса на вас нет...

yttrium
23.12.2025 19:00Однако в rust додумались до варианта как сделать структуру так чтобы она не знала, что она json или чтото там еще.
Ну а время жизни гошнику зачем понимать? фигачишь и не убираешься.
NeoCode
Нужно просто добавить атрибуты или аннотации, два разных названия одного и того же. По сути атрибуты - это лишь константные данные для инициализации "атрибутных" объектов произвольных типов (в классическом ООП это то что передается в конструктор объекта, в Go пусть будет то что нужно для инициализации структуры конкретного типа - разницы нет). Существующие теги структур в этом случае становятся частным случаем - данными для инициализации объекта типа string.
sl4mmer Автор
Если честно, не очень из коммента понял, как вы видите аннотации/атрибуты в го. Можно пример каким нибудь псевдокодом?
NeoCode
Как аннотации/атрибуты в тех языках где они есть. Если уж в Go принято писать теги после типа переменной, то так и надо оставить, просто кроме собственно строк разрешить писать там инициализаторы структурных объектов.
можно через пробел:
можно через запятую:
К сожалению синтаксис Go в целом заточен именно на аннотирование полей, т.е. его напрямую не прикрутить к аннотированию других языковых конструкций - типов, функций и т.д. Но это тоже решается, пусть даже введением нового ключевого слова.
sl4mmer Автор
Нууу, вариант из пропозала, выглядит симпатичнее, не требует таких изменений в reflect и ast + честно говоря, я не понимаю смысл инициализиторов как таковых - это решается конструкторами, зачем два способа делать одно и то же
NeoCode
Можно и конструкторы, без разницы - главное чтобы все аргументы были константами времени компиляции.