Go生成コードガイド(Open)

プロトコルバッファコンパイラが、与えられたプロトコル定義に対してどのようなGoコードを生成するかを正確に記述します。

proto2とproto3で生成されるコード間の相違点が強調表示されています - これらの相違点は、このドキュメントで説明されている生成コードにあり、両バージョンで同じであるベースAPIにはないことに注意してください。このドキュメントを読む前に、proto2言語ガイドおよび/またはproto3言語ガイドを読む必要があります。

コンパイラの起動

プロトコルバッファコンパイラは、Goコードを生成するためにプラグインが必要です。Go 1.16以降を使用してインストールするには、以下を実行します。

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

これにより、protoc-gen-goバイナリが$GOBINにインストールされます。インストール場所を変更するには、$GOBIN環境変数を設定します。プロトコルバッファコンパイラがそれを見つけるには、$PATHに含める必要があります。

プロトコルバッファコンパイラは、go_outフラグを指定して起動されると、Go出力を生成します。go_outフラグの引数は、コンパイラにGo出力を書き込ませたいディレクトリです。コンパイラは、.protoファイル入力ごとに単一のソースファイルを作成します。出力ファイルの名前は、.proto拡張子を.pb.goに置き換えることによって作成されます。

生成された.pb.goファイルが出力ディレクトリのどこに配置されるかは、コンパイラフラグによって異なります。いくつかの出力モードがあります

  • paths=importフラグが指定されている場合、出力ファイルは、Goパッケージのインポートパス(.protoファイル内のgo_packageオプションによって提供されるものなど)にちなんで名付けられたディレクトリに配置されます。たとえば、Goインポートパスがexample.com/project/protos/fizzである入力ファイルprotos/buzz.protoは、example.com/project/protos/fizz/buzz.pb.goの出力ファイルになります。これは、pathsフラグが指定されていない場合のデフォルトの出力モードです。
  • module=$PREFIXフラグが指定されている場合、出力ファイルは、Goパッケージのインポートパス(.protoファイル内のgo_packageオプションによって提供されるものなど)にちなんで名付けられたディレクトリに配置されますが、指定されたディレクトリプレフィックスが出力ファイル名から削除されます。たとえば、Goインポートパスがexample.com/project/protos/fizzであり、example.com/projectmoduleプレフィックスとして指定されている入力ファイルprotos/buzz.protoは、protos/fizz/buzz.pb.goの出力ファイルになります。モジュールパスの外にGoパッケージを生成するとエラーになります。このモードは、生成されたファイルをGoモジュールに直接出力するのに役立ちます。
  • paths=source_relativeフラグが指定されている場合、出力ファイルは、入力ファイルと同じ相対ディレクトリに配置されます。たとえば、入力ファイルprotos/buzz.protoは、protos/buzz.pb.goの出力ファイルになります。

protoc-gen-goに固有のフラグは、protocを起動するときにgo_optフラグを渡すことによって提供されます。複数のgo_optフラグを渡すことができます。たとえば、次を実行する場合

protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto

コンパイラは、srcディレクトリ内の入力ファイルfoo.protoおよびbar/baz.protoを読み取り、出力ファイルfoo.pb.goおよびbar/baz.pb.gooutディレクトリに書き込みます。コンパイラは、必要に応じてネストされた出力サブディレクトリを自動的に作成しますが、出力ディレクトリ自体は作成しません。

パッケージ

Goコードを生成するには、すべての.protoファイル(生成される.protoファイルによって推移的に依存されるファイルを含む)に対して、Goパッケージのインポートパスを提供する必要があります。Goインポートパスを指定する方法は2つあります

  • .protoファイル内で宣言するか、または
  • protocを起動するときにコマンドラインで宣言することによって。

.protoファイル内で宣言することをお勧めします。これにより、.protoファイルのGoパッケージを.protoファイル自体と集中的に識別でき、protocを起動するときに渡されるフラグのセットを簡素化できます。特定の.protoファイルのGoインポートパスが、.protoファイル自体とコマンドラインの両方によって提供されている場合、後者が前者よりも優先されます。

Goインポートパスは、Goパッケージの完全なインポートパスを持つgo_packageオプションを宣言することにより、.protoファイルでローカルに指定されます。使用例

option go_package = "example.com/project/protos/fizz";

Goインポートパスは、コンパイラを起動するときにコマンドラインで指定できます。1つ以上の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.protob.protoをインポートする場合、生成されたa.pb.goファイルは、生成されたb.pb.goファイルを含むGoパッケージをインポートする必要があります(両方のファイルが同じパッケージにない場合)。インポートパスは、出力ファイル名を構築するためにも使用されます。詳細については、上記の「コンパイラの起動」セクションを参照してください。

Goインポートパスと.protoファイル内のpackage指定子の間には相関関係はありません。後者は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
エディション2023Open Struct API
エディション2024+Opaque API

.protoファイルでapi_levelエディション機能を設定することにより、APIを選択できます。これは、ファイルごとまたはメッセージごとに設定できます

edition = "2023";

package log;

import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;

message LogEntry {  }

便宜上、protocコマンドラインフラグを使用してデフォルトのAPIレベルをオーバーライドすることもできます

protoc […] --go_opt=default_api_level=API_HYBRID

特定のファイル(すべてのファイルではなく)のデフォルトのAPIレベルをオーバーライドするには、apilevelMマッピングフラグを使用します(インポートパスのMフラグと同様)。

protoc […] --go_opt=apilevelMhello.proto=API_HYBRID

コマンドラインフラグは、proto2またはproto3構文をまだ使用している.protoファイルでも機能しますが、.protoファイル内からAPIレベルを選択する場合は、最初にそのファイルをエディションに移行する必要があります。

メッセージ

簡単なメッセージ宣言が与えられた場合

message Artist {}

プロトコルバッファコンパイラは、Artistと呼ばれる構造体を生成します。*Artistは、proto.Messageインターフェースを実装します。

protoパッケージは、バイナリ形式との間の変換など、メッセージを操作する関数を提供します。

proto.Messageインターフェースは、ProtoReflectメソッドを定義します。このメソッドは、メッセージのリフレクションベースのビューを提供するprotoreflect.Messageを返します。

optimize_forオプションは、Goコードジェネレーターの出力に影響を与えません。

複数のゴルーチンが同じメッセージに同時にアクセスする場合、次のルールが適用されます

  • フィールドへの同時アクセス(読み取り)は安全ですが、1つの例外があります
  • 同じメッセージ内の異なるフィールドを変更することは安全です。
  • フィールドを同時に変更することは安全ではありません。
  • protoパッケージの関数(proto.Marshalproto.Sizeなど)と同時にメッセージを何らかの方法で変更することは安全ではありません。

ネストされた型

メッセージは別のメッセージ内で宣言できます。例

message Artist {
  message Name {
  }
}

この場合、コンパイラは2つの構造体(ArtistArtist_Name)を生成します。

フィールド

プロトコルバッファコンパイラは、メッセージ内で定義された各フィールドに対して構造体フィールドを生成します。このフィールドの正確な性質は、その型と、それが単数、繰り返し、マップ、またはoneofフィールドであるかどうかによって異なります。

生成されたGoフィールド名は常にキャメルケース命名を使用することに注意してください。.protoファイル内のフィールド名がアンダースコア付きの小文字を使用している場合でも(そうすべきであるように)。ケース変換は次のようになります

  1. 最初の文字はエクスポートのために大文字にされます。最初の文字がアンダースコアの場合、削除され、大文字のXが先頭に追加されます。
  2. 内部のアンダースコアの後に小文字が続く場合、アンダースコアは削除され、後続の文字は大文字にされます。

したがって、protoフィールドbirth_yearはGoではBirthYearになり、_birth_year_2XBirthYear_2になります。

単数スカラーフィールド(proto2)

これらのフィールド定義のいずれかについて

optional int32 birth_year = 1;
required int32 birth_year = 1;

コンパイラは、BirthYearという名前の*int32フィールドと、Artistint32値、またはフィールドが設定されていない場合はデフォルト値を返すアクセスメソッドGetBirthYear()を持つ構造体を生成します。デフォルトが明示的に設定されていない場合、代わりにその型のゼロ値が使用されます(数値の場合は0、文字列の場合は空の文字列)。

他のスカラーフィールド型(boolbytesstringを含む)の場合、スカラー値型テーブルに従って、*int32は対応するGo型に置き換えられます。

単数スカラーフィールド(proto3)

このフィールド定義について

int32 birth_year = 1;
optional int32 first_active_year = 2;

コンパイラは、BirthYearという名前のint32フィールドと、birth_yearint32値、またはフィールドが設定されていない場合はその型のゼロ値を返すアクセスメソッドGetBirthYear()を持つ構造体を生成します(数値の場合は0、文字列の場合は空の文字列)。

FirstActiveYear構造体フィールドは、optionalとマークされているため、*int32型になります。

他のスカラーフィールド型(boolbytesstringを含む)の場合、スカラー値型テーブルに従って、int32は対応するGo型に置き換えられます。protoの設定されていない値は、その型のゼロ値として表されます(数値の場合は0、文字列の場合は空の文字列)。

単数メッセージフィールド

メッセージ型が与えられた場合

message Band {}

Bandフィールドを持つメッセージの場合

// proto2
message Concert {
  optional Band headliner = 1;
  // The generated code is the same result if required instead of optional.
}

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

コンパイラはGo構造体を生成します

type Concert struct {
    Headliner *Band
}

メッセージフィールドはnilに設定できます。これは、フィールドが設定解除され、事実上フィールドがクリアされることを意味します。これは、値をメッセージ構造体の「空の」インスタンスに設定することとは同等ではありません。

コンパイラは、func (m *Concert) GetHeadliner() *Bandヘルパー関数も生成します。この関数は、mがnilまたはheadlinerが設定解除されている場合、nil *Bandを返します。これにより、中間nilチェックなしでget呼び出しをチェーンすることが可能になります

var m *Concert // defaults to nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())

繰り返しフィールド

各繰り返しフィールドは、Goの構造体でTフィールドのスライスを生成します。ここで、Tはフィールドの要素型です。繰り返しフィールドを持つこのメッセージの場合

message Concert {
  // Best practice: use pluralized names for repeated fields:
  // /programming-guides/style#repeated-fields
  repeated Band support_acts = 1;
}

コンパイラはGo構造体を生成します

type Concert struct {
    SupportActs []*Band
}

同様に、フィールド定義repeated bytes band_promo_images = 1;の場合、コンパイラはBandPromoImageという名前の[][]byteフィールドを持つGo構造体を生成します。repeated MusicGenre genres = 2;のような繰り返しの列挙型の場合、コンパイラはGenreと呼ばれる[]MusicGenreフィールドを持つ構造体を生成します。

次の例は、フィールドを設定する方法を示しています

concert := &Concert{
  SupportActs: []*Band{
    {}, // First element.
    {}, // Second element.
  },
}

フィールドにアクセスするには、次のようにします

support := concert.GetSupportActs() // support type is []*Band.
b1 := support[0] // b1 type is *Band, the first element in support_acts.

マップフィールド

各マップフィールドは、map[TKey]TValue型の構造体のフィールドを生成します。ここで、TKeyはフィールドのキー型であり、TValueはフィールドの値型です。マップフィールドを持つこのメッセージの場合

message MerchItem {}

message MerchBooth {
  // items maps from merchandise item name ("Signed T-Shirt") to
  // a MerchItem message with more details about the item.
  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 {
    // Types that are valid to be assigned to 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 is []byte
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_ImageData{ImageData: imageData},
}

フィールドにアクセスするには、値に対して型スイッチを使用して、さまざまなメッセージ型を処理できます。

switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
    // Load profile image based on URL
    // using x.ImageUrl
case *account.Profile_ImageData:
    // Load profile image based on bytes
    // using x.ImageData
case nil:
    // The field is not set.
default:
    return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}

コンパイラは、getメソッドfunc (m *Profile) GetImageUrl() stringfunc (m *Profile) GetImageData() []byteも生成します。各get関数は、そのフィールドの値、または設定されていない場合はゼロ値を返します。

列挙型

次のような列挙型が与えられた場合

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
)

メッセージ内のenum(上記のようなもの)の場合、型名はメッセージ名で始まります

type Venue_Kind int32

パッケージレベルのenumの場合

enum Genre {
  GENRE_UNSPECIFIED = 0;
  GENRE_ROCK = 1;
  GENRE_INDIE = 2;
  GENRE_DRUM_AND_BASS = 3;
  // ...
}

Go型名はproto enum名から変更されません

type Genre int32

この型には、指定された値の名前を返すString()メソッドがあります。

Enum()メソッドは、新しく割り当てられたメモリを指定された値で初期化し、対応するポインタを返します

func (Genre) Enum() *Genre

プロトコルバッファコンパイラは、enum内の各値に対して定数を生成します。メッセージ内のenumの場合、定数は囲んでいるメッセージの名前で始まります

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
)

パッケージレベルのenumの場合、定数は代わりにenum名で始まります

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言語では、複数のenumシンボルが同じ数値を共有できることに注意してください。同じ数値を持つシンボルは同義語です。これらはGoでまったく同じように表現され、同じ数値に対応する複数の名前があります。逆マッピングには、.protoファイルで最初に表示される名前への数値の単一のエントリが含まれています。

拡張機能(proto2)

拡張機能の定義が与えられた場合

extend Concert {
  optional int32 promo_id = 123;
}

プロトコルバッファコンパイラは、E_Promo_idという名前のprotoreflect.ExtensionType値を生成します。この値は、メッセージ内の拡張機能にアクセスするために、proto.GetExtensionproto.SetExtensionproto.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をサポートするためのコードが生成されます。