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

protocol bufferコンパイラが、任意のプロトコル定義に対してどのような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.protobar/baz.protoを読み取り、出力ファイルfoo.pb.gobar/baz.pb.gooutディレクトリに書き込みます。コンパイラは必要に応じて入れ子になった出力サブディレクトリを自動的に作成しますが、出力ディレクトリ自体は作成しません。

パッケージ

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

  • .protoファイル内で宣言する、または
  • protoc を呼び出す際にコマンドラインで宣言する。

.protoファイルのGoパッケージを.protoファイル自体と一元的に識別し、protocを呼び出す際に渡されるフラグのセットを簡素化するために、.protoファイル内で宣言することをお勧めします。特定の.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レベルを選択したい場合は、まずそのファイルをeditionsに移行する必要があります。

メッセージ

単純なメッセージ宣言を考えます。

message Artist {}

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

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

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

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

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

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

ネストされた型

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

message Artist {
  message Name {
  }
}

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

フィールド

プロトコルバッファコンパイラは、メッセージ内に定義された各フィールドに対して、アクセサメソッド(セッターとゲッター)を生成します。

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

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

したがって、Goではbirth_yearというプロトフィールドにGetBirthYear()メソッドでアクセスでき、_birth_year_2にはGetXBirthYear_2()でアクセスできます。

単数フィールド

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

// proto2 and 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)

暗黙的な存在の場合、ゲッターはbirth_yearint32値、またはフィールドが設定されていない場合はその型のゼロ値(数値の場合は0、文字列の場合は空文字列)を返します。明示的な存在の場合、ゲッターはbirth_yearint32値、またはフィールドが設定されていない場合はデフォルト値を返します。デフォルトが明示的に設定されていない場合は、代わりにゼロ値が使用されます。

他のスカラーフィールドタイプ(boolbytesstringを含む)については、スカラー値タイプのテーブルに従って、int32が対応するGoタイプに置き換えられます。

明示的なプレゼンスを持つフィールドでは、これらのメソッドも使用できます。

func (m *Artist) HasBirthYear() bool
func (m *Artist) ClearBirthYear()

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

メッセージ型を考えると:

message Band {}

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

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

// proto3 and 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であっても安全に呼び出すことができます。これにより、中間でnilチェックを行わずにget呼び出しを連鎖させることができます。

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

フィールドが未設定の場合、ゲッターはフィールドのデフォルト値を返します。メッセージの場合、デフォルト値はnilポインタです。

ゲッターとは対照的に、セッターはnilチェックを実行しません。したがって、nilである可能性のあるメッセージに対してセッターを安全に呼び出すことはできません。

繰り返しフィールド

繰り返しフィールドの場合、アクセサメソッドはスライス型を使用します。繰り返しフィールドを持つこのメッセージの場合

message Concert {
  // Best practice: use pluralized names for repeated fields:
  // /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メッセージを構築する方法を示しています。

concert := Concert_builder{
  SupportActs: []*Band{
    {}, // First element.
    {}, // Second element.
  },
}.Build()

あるいは、セッターを使用することもできます。

concert := &Concert{}
concert.SetSupportActs([]*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.

マップフィールド

各マップフィールドは、フィールドのキー型であるTKeyとフィールドの値型であるTValueを持つmap[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 { ... }

func (m *MerchBooth) GetItems() map[string]*MerchItem { ... }
func (m *MerchBooth) SetItems(v map[string]*MerchItem) { ... }

Oneof フィールド

oneof フィールドの場合、プロトバッファ コンパイラは、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() { ... }

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

p1 := accountpb.Profile_builder{
  ImageUrl: proto.String("https://example.com/image.png"),
}.Build()

…または、同等に、セッターを使用する。

// imageData is []byte
imageData := getImageData()
p2 := &accountpb.Profile{}
p2.SetImageData(imageData)

フィールドにアクセスするには、WhichAvatar()の結果に対してswitch文を使用できます。

switch m.WhichAvatar() {
case accountpb.Profile_ImageUrl_case:
    // Load profile image based on URL
    // using m.GetImageUrl()

case accountpb.Profile_ImageData_case:
    // Load profile image based on bytes
    // using m.GetImageData()

case accountpb.Profile_Avatar_not_set_case:
    // The field is not set.

default:
    return fmt.Errorf("Profile.Avatar has an unexpected new oneof field %v", x)
}

ビルダー

ビルダーは、特に単体テストのようなネストされたメッセージを扱う場合に、単一の式でメッセージを構築および初期化する便利な方法です。

他の言語(Javaなど)のビルダーとは異なり、Go protobufビルダーは関数間で渡されることを意図していません。代わりに、すぐにBuild()を呼び出し、結果のプロトメッセージを渡し、セッターを使用してフィールドを変更します。

列挙型

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

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型名は、プロト列挙型名から変更されません。

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
)

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

const (
    Genre_GENRE_UNSPECIFIED   Genre = 0
    Genre_GENRE_ROCK          Genre = 1
    Genre_GENRE_INDIE         Genre = 2
    Genre_GENRE_DRUM_AND_BASS Genre = 3
)

プロトバッファコンパイラは、整数値から文字列名へのマップと、名前から値へのマップも生成します。

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 をサポートするためのコードが生成されます。