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

protocol bufferコンパイラが、任意のプロトコル定義に対してどのようなGoコードを生成するかを正確に説明します。

proto2、proto3、およびエディションの生成コード間の違いが強調されています。これらの違いは、このドキュメントで説明されている生成コードにあり、ベース API ではありません。ベース 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 で、module プレフィックスとして example.com/project が指定されている入力ファイル 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 ファイルの 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 つの構造体を生成します。

フィールド

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

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

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

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

単一の明示的な存在スカラーフィールド

フィールド定義の場合

int32 birth_year = 1;

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

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

単一の暗黙的な存在スカラーフィールド

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

int32 birth_year = 1;

コンパイラは、BirthYear という名前の int32 フィールドと、birth_year 内の int32 値を返す、またはフィールドが設定されていない場合はその型のゼロ値を返すアクセサーメソッド 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;
}

// editions
message Concer {
  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 ファイルに最初に現れる名前への単一のエントリが含まれます。

エクステンション

エクステンション定義が与えられた場合:

extend Concert {
  int32 promo_id = 123;
}

プロトコルバッファコンパイラは、E_Promo_id という名前の protoreflect.ExtensionType 値を生成します。この値は、proto.GetExtensionproto.SetExtensionproto.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 Quickstart ガイドを参照)、gRPC をサポートするためのコードが生成されます。