Руководство по сгенерированному коду Go (Opaque)"
Подробно описывает, какой именно код Go генерирует компилятор протоколов для любого заданного определения протокола.
Все различия между сгенерированным кодом для proto2 и proto3 выделены - обратите внимание, что эти различия относятся к сгенерированному коду, как описано в этом документе, а не к базовому API, который одинаков в обеих версиях. Вам следует прочитать руководство по языку proto2 и/или руководство по языку proto3 перед чтением этого документа.
{{% alert title="Примечание" color="warning" %}}Вы смотрите документацию для Opaque API, который является текущей версией. Если вы работаете с файлами .proto, которые используют старый Open Struct API (вы можете определить это по настройке уровня API в соответствующих файлах .proto), смотрите Сгенерированный код Go (Open) для соответствующей документации. Смотрите Go Protobuf: The new Opaque API для знакомства с Opaque API. {{% /alert %}}
Вызов компилятора
Компилятору протоколов требуется плагин для генерации кода Go. Установите его с помощью Go 1.16 или выше, выполнив:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
Это установит бинарный файл protoc-gen-go в $GOBIN. Установите переменную окружения $GOBIN,
чтобы изменить местоположение установки. Она должна быть в вашем $PATH,
чтобы компилятор протоколов мог её найти.
Компилятор протоколов выводит код Go при вызове с флагом go_out.
Аргумент флага go_out — это каталог, куда вы хотите, чтобы компилятор
записывал ваш вывод Go. Компилятор создает один исходный файл для
каждого входного файла .proto. Имя выходного файла создается путем замены
расширения .proto на .pb.go.
То, где в выходном каталоге размещается сгенерированный файл .pb.go, зависит от
флагов компилятора. Существует несколько режимов вывода:
- Если указан флаг
paths=import, выходной файл помещается в каталог с именем, соответствующим пути импорта пакета Go (например, указанному параметромgo_packageв файле.proto). Например, входной файлprotos/buzz.protoс путем импорта Goexample.com/project/protos/fizzприводит к выходному файлу вexample.com/project/protos/fizz/buzz.pb.go. Это режим вывода по умолчанию, если флагpathsне указан. - Если указан флаг
module=$PREFIX, выходной файл помещается в каталог с именем, соответствующим пути импорта пакета Go (например, указанному параметромgo_packageв файле.proto), но с удалением указанного префикса каталога из имени выходного файла. Например, входной файлprotos/buzz.protoс путем импорта Goexample.com/project/protos/fizzиexample.com/project, указанным в качестве префиксаmodule, приводит к выходному файлу вprotos/fizz/buzz.pb.go. Генерация любых пакетов Go вне пути модуля приводит к ошибке. Этот режим полезен для вывода сгенерированных файлов непосредственно в модуль Go. - Если указан флаг
paths=source_relative, выходной файл помещается в тот же относительный каталог, что и входной файл. Например, входной файлprotos/buzz.protoприводит к выходному файлу вprotos/buzz.pb.go.
Флаги, специфичные для protoc-gen-go, предоставляются путем передачи флага go_opt при
вызове protoc. Можно передать несколько флагов go_opt. Например, при запуске:
protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
компилятор будет читать входные файлы foo.proto и bar/baz.proto из
каталога src и записывать выходные файлы foo.pb.go и bar/baz.pb.go в
каталог out. Компилятор автоматически создает вложенные выходные
подкаталоги, если необходимо, но не создает сам выходной каталог.
Пакеты
Для генерации кода Go путь импорта пакета Go должен быть предоставлен для
каждого файла .proto (включая те, от которых транзитивно зависят
генерируемые файлы .proto). Есть два способа указать путь импорта Go:
- объявив его внутри файла
.protoили - объявив его в командной строке при вызове
protoc.
Мы рекомендуем объявлять его внутри файла .proto, чтобы пакеты Go для
файлов .proto могли быть централизованно идентифицированы вместе с самими файлами .proto
и чтобы упростить набор флагов, передаваемых при вызове protoc. Если путь импорта Go для данного файла .proto предоставлен как в самом файле .proto, так и
в командной строке, то последний имеет приоритет над первым.
Путь импорта Go локально указывается в файле .proto путем объявления
опции go_package с полным путем импорта пакета Go. Пример использования:
option go_package = "example.com/project/protos/fizz";
Путь импорта Go может быть указан в командной строке при вызове
компилятора, путем передачи одного или нескольких флагов M${PROTO_FILE}=${GO_IMPORT_PATH}.
Пример использования:
protoc --proto_path=src \
--go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
--go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
protos/buzz.proto protos/bar.proto
Поскольку сопоставление всех файлов .proto с их путями импорта Go может быть довольно
большим, этот способ указания путей импорта Go обычно выполняется
некоторым инструментом сборки (например, Bazel), который
имеет контроль над всем деревом зависимостей. Если есть дублирующиеся записи для
данного файла .proto, то используется последняя указанная.
Как для опции go_package, так и для флага M значение может включать
явное имя пакета, отделенное от пути импорта точкой с запятой.
Например: "example.com/protos/foo;package_name". Такое использование не рекомендуется,
поскольку имя пакета по умолчанию будет выводиться из пути импорта разумным образом.
Путь импорта используется для определения того, какие операторы импорта должны быть сгенерированы,
когда один файл .proto импортирует другой файл .proto. Например, если a.proto
импортирует b.proto, то сгенерированному файлу a.pb.go нужно импортировать Go-пакет,
содержащий сгенерированный файл b.pb.go (если только оба файла не находятся в
одном пакете). Путь импорта также используется для построения выходных имен файлов.
Подробности см. в разделе «Вызов компилятора» выше.
Нет корреляции между путем импорта Go и
спецификатором package
в файле .proto. Последний относится только к пространству имен protobuf,
тогда как первый относится только к пространству имен Go. Также нет
корреляции между путем импорта Go и путем импорта .proto.
Уровень API
Сгенерированный код использует либо Open Struct API, либо Opaque API. Смотрите Go Protobuf: The new Opaque API (блогпост) для введения.
В зависимости от синтаксиса вашего файла .proto, вот какой API будет использоваться:
Синтаксис .proto | Уровень API |
|---|---|
| proto2 | Open Struct API |
| proto3 | Open Struct API |
| edition 2023 | Open Struct API |
| edition 2024+ | Opaque API |
Вы можете выбрать API, установив функцию editions api_level в вашем
файле .proto. Это можно установить для каждого файла или для каждого сообщения:
edition = "2023";
package log;
import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;
message LogEntry { … }
Для вашего удобства вы также можете переопределить уровень API по умолчанию с помощью
флага командной строки protoc:
protoc […] --go_opt=default_api_level=API_HYBRID
Чтобы переопределить уровень API по умолчанию для конкретного файла (вместо всех файлов),
используйте флаг сопоставления apilevelM (аналогично флагу M для путей импорта):
protoc […] --go_opt=apilevelMhello.proto=API_HYBRID
Флаги командной строки также работают для файлов .proto, все еще использующих синтаксис proto2 или proto3,
но если вы хотите выбрать уровень API из самого файла .proto,
вам нужно сначала перенести этот файл на editions.
Сообщения
Дано простое объявление сообщения:
message Artist {}
компилятор протоколов генерирует структуру с именем Artist. *Artist
реализует интерфейс
proto.Message.
Пакет
proto
предоставляет функции, которые работают с сообщениями, включая преобразование в бинарный формат и из него.
Интерфейс proto.Message определяет метод ProtoReflect. Этот метод
возвращает
protoreflect.Message,
который предоставляет представление сообщения на основе рефлексии.
Опция optimize_for не влияет на вывод генератора кода Go.
Когда несколько горутин одновременно обращаются к одному и тому же сообщению, применяются следующие правила:
- Доступ (чтение) к полям одновременно безопасен, за одним исключением:
- Первый доступ к ленивому полю является модификацией.
- Модификация разных полей в одном сообщении безопасна.
- Модификация поля одновременно не безопасна.
- Модификация сообщения любым способом одновременно с функциями
пакета
proto, такими какproto.Marshalилиproto.Size, не безопасна.
Вложенные типы
Сообщение может быть объявлено внутри другого сообщения. Например:
message Artist {
message Name {
}
}
В этом случае компилятор генерирует две структуры: Artist и Artist_Name.
Поля
Компилятор протоколов генерирует методы доступа (сеттеры и геттеры) для каждого поля, определенного в сообщении.
Обратите внимание, что сгенерированные методы доступа Go всегда используют верблюжью нотацию (camel-case),
даже если имя поля в файле .proto использует нижний регистр с подчеркиваниями
(как и должно).
Преобразование регистра работает следующим образом:
- Первая буква заглавная для экспорта. Если первый символ — подчеркивание, оно удаляется и добавляется заглавная X.
- Если за внутренним подчеркиванием следует буква в нижнем регистре, подчеркивание удаляется, а следующая буква заглавная.
Таким образом, вы можете получить доступ к полю proto birth_year с помощью метода GetBirthYear()
в Go, а _birth_year_2 с помощью GetXBirthYear_2().
Одиночные поля
Для этого определения поля:
// proto2 и proto3
message Artist {
optional int32 birth_year = 1;
}
// editions
message Artist {
int32 birth_year = 1 [features.field_presence = EXPLICIT];
}
компилятор генерирует структуру Go со следующими методами доступа:
func (m *Artist) GetBirthYear() int32
func (m *Artist) SetBirthYear(v int32)
При неявном присутствии (implicit presence) геттер возвращает значение int32 в birth_year или
нулевое значение этого
типа, если поле не установлено (0 для чисел, пустая строка для строк). При
явном присутствии (explicit presence) геттер возвращает значение int32 в birth_year или
значение по умолчанию, если поле не установлено. Если значение по умолчанию не задано явно,
используется нулевое значение.
Для других типов скалярных полей (включая bool, bytes и string) int32
заменяется соответствующим типом Go в соответствии с
таблицей скалярных типов значений.
В полях с явным присутствием вы также можете использовать эти методы:
func (m *Artist) HasBirthYear() bool
func (m *Artist) ClearBirthYear()
Одиночные поля сообщений (Singular Message Fields)
Дан тип сообщения:
message Band {}
Для сообщения с полем Band:
// proto2
message Concert {
optional Band headliner = 1;
// Сгенерированный код будет тем же, если используется required вместо optional.
}
// proto3 и editions
message Concert {
Band headliner = 1;
}
Компилятор сгенерирует структуру Go со следующими методами доступа:
type Concert struct { ... }
func (m *Concert) GetHeadliner() *Band { ... }
func (m *Concert) SetHeadliner(v *Band) { ... }
func (m *Concert) HasHeadliner() bool { ... }
func (m *Concert) ClearHeadliner() { ... }
Метод доступа GetHeadliner() безопасно вызывать, даже если m равен nil. Это
делает возможной цепочку вызовов get без промежуточных проверок на nil:
var m *Concert // по умолчанию nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())
Если поле не установлено, геттер вернет значение по умолчанию для поля. Для сообщений значение по умолчанию — это нулевой указатель.
В отличие от геттеров, сеттеры не выполняют проверки на nil за вас. Поэтому вы не можете безопасно вызывать сеттеры на возможно nil сообщениях.
Повторяющиеся поля (Repeated Fields)
Для повторяющихся полей методы доступа используют тип среза (slice). Для этого сообщения с повторяющимся полем:
message Concert {
// Лучшая практика: используйте имена во множественном числе для повторяющихся полей:
// /programming-guides/style#repeated-fields
repeated Band support_acts = 1;
}
компилятор генерирует структуру Go со следующими методами доступа:
type Concert struct { ... }
func (m *Concert) GetSupportActs() []*Band { ... }
func (m *Concert) SetSupportActs(v []*Band) { ... }
Аналогично, для определения поля repeated bytes band_promo_images = 1;
компилятор сгенерирует методы доступа, работающие с типом [][]byte. Для
повторяющегося перечисления repeated MusicGenre genres = 2;, компилятор
генерирует методы доступа, работающие с типом []MusicGenre.
Следующий пример показывает, как создать сообщение Concert с помощью
строителя (builder).
concert := Concert_builder{
SupportActs: []*Band{
{}, // Первый элемент.
{}, // Второй элемент.
},
}.Build()
Альтернативно, вы можете использовать сеттеры:
concert := &Concert{}
concert.SetSupportActs([]*Band{
{}, // Первый элемент.
{}, // Второй элемент.
})
Чтобы получить доступ к полю, вы можете сделать следующее:
support := concert.GetSupportActs() // тип support - []*Band.
b1 := support[0] // тип b1 - *Band, первый элемент в support_acts.
Поля-отображения (Map Fields)
Каждое поле-отображение генерирует методы доступа, работающие с типом map[TKey]TValue, где
TKey — это тип ключа поля, а TValue — тип значения поля. Для этого
сообщения с полем-отображением:
message MerchItem {}
message MerchBooth {
// items отображает название товара ("Signed T-Shirt") в
// сообщение MerchItem с дополнительной информацией о товаре.
map<string, MerchItem> items = 1;
}
компилятор генерирует структуру Go со следующими методами доступа:
type MerchBooth struct { ... }
func (m *MerchBooth) GetItems() map[string]*MerchItem { ... }
func (m *MerchBooth) SetItems(v map[string]*MerchItem) { ... }
Поля Oneof
Для поля oneof компилятор protobuf генерирует методы доступа для каждого из одиночных полей внутри oneof.
Для этого сообщения с полем oneof:
package account;
message Profile {
oneof avatar {
string image_url = 1;
bytes image_data = 2;
}
}
компилятор генерирует структуру Go со следующими методами доступа:
type Profile struct { ... }
func (m *Profile) WhichAvatar() case_Profile_Avatar { ... }
func (m *Profile) GetImageUrl() string { ... }
func (m *Profile) GetImageData() []byte { ... }
func (m *Profile) SetImageUrl(v string) { ... }
func (m *Profile) SetImageData(v []byte) { ... }
func (m *Profile) HasAvatar() bool { ... }
func (m *Profile) HasImageUrl() bool { ... }
func (m *Profile) HasImageData() bool { ... }
func (m *Profile) ClearAvatar() { ... }
func (m *Profile) ClearImageUrl() { ... }
func (m *Profile) ClearImageData() { ... }
Следующий пример показывает, как установить поле с помощью строителя (builder):
p1 := accountpb.Profile_builder{
ImageUrl: proto.String("https://example.com/image.png"),
}.Build()
...или, что то же самое, с помощью сеттера:
// imageData имеет тип []byte
imageData := getImageData()
p2 := &accountpb.Profile{}
p2.SetImageData(imageData)
Чтобы получить доступ к полю, вы можете использовать оператор switch по результату WhichAvatar():
switch m.WhichAvatar() {
case accountpb.Profile_ImageUrl_case:
// Загрузить изображение профиля на основе URL
// используя m.GetImageUrl()
case accountpb.Profile_ImageData_case:
// Загрузить изображение профиля на основе байтов
// используя m.GetImageData()
case accountpb.Profile_Avatar_not_set_case:
// Поле не установлено.
default:
return fmt.Errorf("Profile.Avatar имеет неожиданное новое поле oneof %v", x)
}
Строители (Builders)
Строители — это удобный способ конструирования и инициализации сообщения в рамках одного выражения, особенно при работе с вложенными сообщениями, как в модульных тестах.
В отличие от строителей в других языках (таких как Java), строители Go protobuf
не предназначены для передачи между функциями. Вместо этого немедленно вызывайте Build()
и передавайте полученное proto-сообщение, используя сеттеры для
модификации полей.
Перечисления
Дано перечисление вида:
message Venue {
enum Kind {
KIND_UNSPECIFIED = 0;
KIND_CONCERT_HALL = 1;
KIND_STADIUM = 2;
KIND_BAR = 3;
KIND_OPEN_AIR_FESTIVAL = 4;
}
Kind kind = 1;
// ...
}
компилятор протоколов генерирует тип и серию констант с этим типом:
type Venue_Kind int32
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
Для перечислений внутри сообщения (как приведенное выше) имя типа начинается с имени сообщения:
type Venue_Kind int32
Для перечисления на уровне пакета:
enum Genre {
GENRE_UNSPECIFIED = 0;
GENRE_ROCK = 1;
GENRE_INDIE = 2;
GENRE_DRUM_AND_BASS = 3;
// ...
}
имя типа Go не изменяется относительно имени enum в proto:
type Genre int32
Этот тип имеет метод String(), который возвращает имя заданного значения.
Метод Enum() инициализирует вновь выделенную память заданным значением и
возвращает соответствующий указатель:
func (Genre) Enum() *Genre
Компилятор протоколов генерирует константу для каждого значения в перечислении. Для перечислений внутри сообщения константы начинаются с имени охватывающего сообщения:
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
Для перечисления на уровне пакета константы начинаются с имени перечисления:
const (
Genre_GENRE_UNSPECIFIED Genre = 0
Genre_GENRE_ROCK Genre = 1
Genre_GENRE_INDIE Genre = 2
Genre_GENRE_DRUM_AND_BASS Genre = 3
)
Компилятор protobuf также генерирует отображение целочисленных значений в строковые имена и отображение имен в значения:
var Genre_name = map[int32]string{
0: "GENRE_UNSPECIFIED",
1: "GENRE_ROCK",
2: "GENRE_INDIE",
3: "GENRE_DRUM_AND_BASS",
}
var Genre_value = map[string]int32{
"GENRE_UNSPECIFIED": 0,
"GENRE_ROCK": 1,
"GENRE_INDIE": 2,
"GENRE_DRUM_AND_BASS": 3,
}
Обратите внимание, что язык .proto позволяет нескольким символам перечисления иметь один и тот же
числовое значение. Символы с одинаковым числовым значением являются синонимами. Они
представлены в Go точно так же, с несколькими именами, соответствующими
одному числовому значению. Обратное отображение содержит единственную запись для
числового значения к имени, которое появляется первым в файле .proto.
Расширения (proto2)
Дано определение расширения:
extend Concert {
optional int32 promo_id = 123;
}
Компилятор протоколов сгенерирует значение
protoreflect.ExtensionType
с именем E_Promo_id. Это значение может быть использовано с функциями
proto.GetExtension,
proto.SetExtension,
proto.HasExtension,
и
proto.ClearExtension
для доступа к расширению в сообщении. Функция GetExtension и
функция SetExtension возвращают и принимают значение interface{}
содержащее тип значения расширения.
Для одиночных скалярных полей расширения тип значения расширения является соответствующим типом Go из таблицы скалярных типов значений.
Для одиночных полей расширения встроенного сообщения тип значения расширения —
*M, где M — тип сообщения поля.
Для повторяющихся полей расширения тип значения расширения — это срез одиночного типа.
Например, дано следующее определение:
extend Concert {
optional int32 singular_int32 = 1;
repeated bytes repeated_strings = 2;
optional Band singular_message = 3;
}
Значения расширений могут быть доступны как:
m := &somepb.Concert{}
proto.SetExtension(m, extpb.E_SingularInt32, int32(1))
proto.SetExtension(m, extpb.E_RepeatedString, []string{"a", "b", "c"})
proto.SetExtension(m, extpb.E_SingularMessage, &extpb.Band{})
v1 := proto.GetExtension(m, extpb.E_SingularInt32).(int32)
v2 := proto.GetExtension(m, extpb.E_RepeatedString).([][]byte)
v3 := proto.GetExtension(m, extpb.E_SingularMessage).(*extpb.Band)
Расширения могут быть объявлены вложенными внутри другого типа. Например, распространенный шаблон — сделать что-то вроде этого:
message Promo {
extend Concert {
optional int32 promo_id = 124;
}
}
В этом случае значение ExtensionType называется E_Promo_Concert.
Сервисы
Генератор кода Go по умолчанию не выдает вывод для сервисов. Если вы включите плагин gRPC (см. руководство по быстрому старту gRPC Go), то код будет сгенерирован для поддержки gRPC.