
Всем доброе время суток. Я пишу всякое на 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 и, как писал выше, практически не сомневаюсь, что заложенные в нём идеи так или иначе будут реализованы.