Всем доброе время суток. Я пишу всякое на 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

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