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

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

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

コンパイラの呼び出し

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

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

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

protocol bufferコンパイラは、go_outフラグを付けて呼び出されるとGoの出力を生成します。go_outフラグの引数は、コンパイラにGoの出力を書き込ませたいディレクトリです。コンパイラは、入力.protoファイルごとに1つのソースファイルを作成します。出力ファイルの名前は、.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.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
edition 2023Open Struct API
edition 2024+Opaque API

APIは、.protoファイルでapi_level editions feature を設定することで選択できます。これはファイルごと、またはメッセージごとに設定できます。

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 {}

protocol bufferコンパイラはArtistという名前のstructを生成します。*Artistproto.Messageインターフェースを実装します。

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

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

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

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

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

ネストされた型

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

message Artist {
  message Name {
  }
}

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

フィールド

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

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

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

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

明示的なプレゼンスを持つ単数スカラーフィールド

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

int32 birth_year = 1;

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

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

暗黙的なプレゼンスを持つ単数スカラーフィールド

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

int32 birth_year = 1;

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

FirstActiveYear structフィールドは、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のstructを生成します。

type Concert struct {
    Headliner *Band
}

メッセージフィールドはnilに設定できます。これはフィールドが設定されていないことを意味し、事実上フィールドをクリアします。これは、値をメッセージstructの「空の」インスタンスに設定することとは異なります。

コンパイラはまた、ヘルパー関数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())

繰り返しフィールド

各repeatedフィールドは、GoのstructにTのスライスフィールドを生成します。ここで、Tはフィールドの要素型です。repeatedフィールドを持つこのメッセージの場合:

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

コンパイラはGoのstructを生成します。

type Concert struct {
    SupportActs []*Band
}

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

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

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フィールドは、structにmap[TKey]TValue型のフィールドを生成します。ここでTKeyはフィールドのキー型、TValueはフィールドの値型です。mapフィールドを持つこのメッセージの場合:

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のstructを生成します。

type MerchBooth struct {
    Items map[string]*MerchItem
}

Oneof フィールド

oneofフィールドの場合、protobufコンパイラはインターフェース型isMessageName_MyFieldを持つ単一のフィールドを生成します。また、oneof内の単数フィールドごとにstructを生成します。これらはすべて、このisMessageName_MyFieldインターフェースを実装します。

oneofフィールドを持つこのメッセージの場合:

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

コンパイラはstructを生成します。

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;
  // ...
}

protocol bufferコンパイラは、型とその型の一連の定数を生成します。

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

protocol bufferコンパイラは、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;
}

protocol bufferコンパイラは、E_Promo_idという名前のprotoreflect.ExtensionType値を生成します。この値は、proto.GetExtensionproto.SetExtensionproto.HasExtension、およびproto.ClearExtension関数と組み合わせて、メッセージ内のエクステンションにアクセスするために使用できます。GetExtension関数とSetExtension関数は、それぞれエクステンション値の型を含むinterface{}値を返したり受け入れたりします。

単数スカラーエクステンションフィールドの場合、エクステンション値の型はスカラー値型テーブルの対応するGoの型です。

単数埋め込みメッセージエクステンションフィールドの場合、エクステンション値の型は*Mです。ここでMはフィールドメッセージの型です。

repeatedエクステンションフィールドの場合、エクステンション値の型は単数型のスライスです。

たとえば、次の定義が与えられた場合:

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