Руководство по сгенерированному коду Go (Open)

Подробно описывает, какой именно код Go генерирует компилятор протоколов для любого заданного определения протокола.

Все различия между сгенерированным кодом для proto2, proto3 и editions выделены - обратите внимание, что эти различия относятся к сгенерированному коду, как описано в этом документе, а не к базовому API, который одинаков в обеих версиях. Вам следует прочитать руководство по языку proto2, руководство по языку proto3 или руководство по языку editions перед чтением этого документа.

warning

Вы смотрите документацию для старого API сгенерированного кода (Open Struct API).

Смотрите Сгенерированный код Go (Opaque) для соответствующей документации (нового) Opaque API.

Смотрите Go Protobuf: The new Opaque API для знакомства с Opaque API.

Вызов компилятора

Компилятору протоколов требуется плагин для генерации кода 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 с путем импорта Go example.com/project/protos/fizz приводит к выходному файлу в example.com/project/protos/fizz/buzz.pb.go. Это режим вывода по умолчанию, если флаг paths не указан.
  • Если указан флаг module=$PREFIX, выходной файл помещается в каталог с именем, соответствующим пути импорта пакета Go (например, указанному параметром go_package в файле .proto), но с удалением указанного префикса каталога из имени выходного файла. Например, входной файл protos/buzz.proto с путем импорта Go example.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
proto2Open Struct API
proto3Open Struct API
edition 2023Open 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.

Поля

Компилятор протоколов генерирует поле структуры для каждого поля, определенного в сообщении. Точная природа этого поля зависит от его типа и от того, является ли оно одиночным (singular), повторяющимся (repeated), отображением (map) или полем oneof.

Обратите внимание, что сгенерированные имена полей Go всегда используют верблюжью нотацию (camel-case), даже если имя поля в файле .proto использует нижний регистр с подчеркиваниями (как и должно). Преобразование регистра работает следующим образом:

  1. Первая буква заглавная для экспорта. Если первый символ — подчеркивание, оно удаляется и добавляется заглавная X.
  2. Если за внутренним подчеркиванием следует буква в нижнем регистре, подчеркивание удаляется, а следующая буква заглавная.

Таким образом, поле proto birth_year становится BirthYear в Go, а _birth_year_2 становится XBirthYear_2.

Скалярные поля с явным присутствием (Singular Explicit Presence)

Для определения поля:

int32 birth_year = 1;

компилятор генерирует структуру с полем *int32 с именем BirthYear и методом доступа GetBirthYear(), который возвращает значение int32 в Artist или значение по умолчанию, если поле не установлено. Если значение по умолчанию не задано явно, используется нулевое значение этого типа (0 для чисел, пустая строка для строк).

Для других типов скалярных полей (включая bool, bytes и string) *int32 заменяется соответствующим типом Go в соответствии с таблицей скалярных типов значений.

Скалярные поля с неявным присутствием (Singular Implicit Presence)

Для этого определения поля:

int32 birth_year = 1;

Компилятор сгенерирует структуру с полем int32 с именем BirthYear и методом доступа GetBirthYear(), который возвращает значение int32 в birth_year или нулевое значение этого типа, если поле не установлено (0 для чисел, пустая строка для строк).

Поле структуры FirstActiveYear будет иметь тип *int32, потому что оно помечено как optional.

Для других типов скалярных полей (включая bool, bytes и string) int32 заменяется соответствующим типом Go в соответствии с таблицей скалярных типов значений. Неустановленные значения в proto будут представлены как нулевое значение этого типа (0 для чисел, пустая строка для строк).

Одиночные поля сообщений (Singular Message Fields)

Дан тип сообщения:

message Band {}

Для сообщения с полем Band:

// proto2
message Concert {
  optional Band headliner = 1;
  // Сгенерированный код будет тем же, если используется required вместо optional.
}

// proto3
message Concert {
  Band headliner = 1;
}

// editions
message Concert {
  Band headliner = 1;
}

Компилятор сгенерирует структуру Go

type Concert struct {
    Headliner *Band
}

Поля сообщений могут быть установлены в nil, что означает, что поле не установлено, эффективно очищая поле. Это не эквивалентно установке значения в «пустой» экземпляр структуры сообщения.

Компилятор также генерирует вспомогательную функцию func (m *Concert) GetHeadliner() *Band. Эта функция возвращает nil *Band, если m равен nil или headliner не установлен. Это делает возможным цепочку вызовов get без промежуточных проверок на nil:

var m *Concert // по умолчанию nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())

Повторяющиеся поля (Repeated Fields)

Каждое повторяющееся поле генерирует в структуре Go поле-срез (slice) типа T, где T — это тип элемента поля. Для этого сообщения с повторяющимся полем:

message Concert {
  // Лучшая практика: используйте имена во множественном числе для повторяющихся полей:
  // /programming-guides/style#repeated-fields
  repeated Band support_acts = 1;
}

компилятор генерирует структуру Go:

type Concert struct {
    SupportActs []*Band
}

Аналогично, для определения поля repeated bytes band_promo_images = 1; компилятор сгенерирует структуру Go с полем [][]byte с именем BandPromoImages. Для повторяющегося перечисления, например repeated MusicGenre genres = 2;, компилятор генерирует структуру с полем []MusicGenre с именем Genres.

Следующий пример показывает, как установить поле:

concert := &Concert{
  SupportActs: []*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 {
    Items map[string]*MerchItem
}

Поля Oneof

Для поля oneof компилятор protobuf генерирует одно поле с интерфейсным типом isMessageName_MyField. Он также генерирует структуру для каждого из одиночных полей внутри oneof. Все они реализуют этот интерфейс isMessageName_MyField.

Для этого сообщения с полем oneof:

package account;
message Profile {
  oneof avatar {
    string image_url = 1;
    bytes image_data = 2;
  }
}

компилятор генерирует структуры:

type Profile struct {
    // Типы, которые могут быть присвоены Avatar:
    //  *Profile_ImageUrl
    //  *Profile_ImageData
    Avatar isProfile_Avatar `protobuf_oneof:"avatar"`
}

type Profile_ImageUrl struct {
        ImageUrl string
}
type Profile_ImageData struct {
        ImageData []byte
}

И *Profile_ImageUrl, и *Profile_ImageData реализуют isProfile_Avatar посредством предоставления пустого метода isProfile_Avatar().

Следующий пример показывает, как установить поле:

p1 := &account.Profile{
  Avatar: &account.Profile_ImageUrl{ImageUrl: "http://example.com/image.png"},
}

// imageData имеет тип []byte
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_ImageData{ImageData: imageData},
}

Чтобы получить доступ к полю, вы можете использовать type switch для значения, чтобы обработать различные типы сообщений.

switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
    // Загрузить изображение профиля на основе URL
    // используя x.ImageUrl
case *account.Profile_ImageData:
    // Загрузить изображение профиля на основе байтов
    // используя x.ImageData
case nil:
    // Поле не установлено.
default:
    return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}

Компилятор также генерирует методы получения func (m *Profile) GetImageUrl() string и func (m *Profile) GetImageData() []byte. Каждая функция получения возвращает значение для этого поля или нулевое значение, если оно не установлено.

Перечисления

Дано перечисление вида:

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.

Расширения

Дано определение расширения:

extend Concert {
  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 {
  int32 singular_int32 = 1;
  repeated bytes repeated_strings = 2;
  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 {
    int32 promo_id = 124;
  }
}

В этом случае значение ExtensionType называется E_Promo_Concert.

Сервисы

Генератор кода Go по умолчанию не выдает вывод для сервисов. Если вы включите плагин gRPC (см. руководство по быстрому старту gRPC Go), то код будет сгенерирован для поддержки gRPC.