5.

.protoのgo_packageオプションとは|Goコード生成時のパッケージ指定

編集

go_packageは、Protocol Buffers(プロトコルバッファ)の定義ファイル(.proto)に記述するオプションの一つで、その.protoからGoのコードを生成するときに、生成コードのimportパス(Goのパッケージのパス)とパッケージ名を指定するためのものです。option go_package = "..."という構文で書きます。

この記事の要点
  • go_packageは、.protoファイルに書くファイルレベルのオプションで、Goコード生成時のimportパスパッケージ名を決めるために使われます。
  • 書き方はoption go_package = "github.com/user/repo/proto;packagename";。セミコロンの前がimportパス、後ろがGoのパッケージ名という意味合いで使われます。
  • コード生成プラグイン(protoc-gen-go)が、生成ファイルを正しいimportパスで出力し、他の.protoからの参照を正しく解決するために参照します。
  • 近年のツールチェーンでは、go_packageを指定しないと生成自体がエラーになる構成が一般的になっており、明示指定が事実上の前提とされる場面が増えています。
  • セミコロンの扱い、Goモジュールのパスとの整合、複数.proto間での一貫性が、つまずきやすいポイントとして挙げられます。

go_packageとは

Protocol Buffersは、構造化データのスキーマを.protoというテキストファイルで定義し、そこから各言語向けのコードを自動生成する仕組みです。Goを対象に生成する場合、生成されたGoファイルがどのimportパスに属し、どのパッケージ名を名乗るのかを、ツール側が知る必要があります。これを.protoファイル内で明示するのがgo_packageオプションです。

go_packageはメッセージ単位ではなくファイルレベルのオプションとして、.protoファイルの先頭付近(syntaxpackage宣言と並ぶ位置)に1つ記述します。ここで指定した値が、生成されるGoコードのpackage行や、他のパッケージから生成型を参照する際のimportパスに反映されます。

なお、.protopackage宣言(例: package example;)はprotobuf上の名前空間であり、Goのpackage名やimportパスとは別物です。go_packageは、このprotoの名前空間とは独立して、Go側の都合(モジュールパスやディレクトリ構成)に合わせた値を指定するためにあります。

書き方

基本形は次のとおりです。importパスとパッケージ名をセミコロン(;)で区切って1つの文字列にまとめます。

syntax = "proto3";

package example.v1;

option go_package = "github.com/user/repo/gen/examplev1;examplev1";

message MyMessage {
  string id = 1;
}

この例では、セミコロンのにあたるgithub.com/user/repo/gen/examplev1がGoのimportパス、後ろにあたるexamplev1が生成されるGoファイルのpackage名として扱われます。つまり、他のGoコードからは次のように利用することが想定されます。

package main

import (
  "fmt"
  examplev1 "github.com/user/repo/gen/examplev1"
)

func main() {
  m := &examplev1.MyMessage{Id: "abc"}
  fmt.Println(m)
}

セミコロン以降のパッケージ名は省略できます。その場合は、importパスの最後の要素がパッケージ名として使われるのが一般的な挙動です。たとえばoption go_package = "github.com/user/repo/gen/examplev1";と書くと、パッケージ名はexamplev1とみなされます。importパスの末尾の語をそのままパッケージ名にしてよい場合は、セミコロン以降を省略して簡潔に書けます。

セミコロンの前後の役割を整理すると、次の表のようになります。

記述 意味
"パス;名前" セミコロン前をimportパス、後ろをGoのパッケージ名として扱う。
"パス"(セミコロンなし) 全体をimportパスとして扱い、パッケージ名はパスの末尾の要素を採用する挙動が一般的。

なぜ必要か

.protoからGoコードを生成する際は、protoc(またはbufなどの上位ツール)が、Go向けプラグインであるprotoc-gen-goを呼び出してコードを書き出します。このときprotoc-gen-goは、各.protoから生成するGoファイルが「どのimportパスに属するか」を知る必要があります。理由は主に2つあります。

  • 生成ファイル自身のpackage行を決めるため。 生成されたGoファイルの先頭に書かれるpackage名は、go_packageの指定(またはその末尾要素)に基づきます。
  • 他の.protoからの参照を正しいimportに変換するため。 ある.protoが別の.protoimportしてメッセージ型を利用している場合、生成コードでは相手側パッケージをGoのimportパスで参照する必要があります。参照先のgo_packageがわかって初めて、正しいimport文を生成できます。

このように、go_packageはGoのモジュール/パッケージ体系と、protobufのスキーマ定義をつなぐ橋渡しの役割を担っています。

指定しないと起きる問題

かつてのprotoc-gen-goでは、go_packageが無い場合にコマンドラインのオプション(パスとパッケージ名の対応付け)から補う方式が用いられていました。しかし現在広く使われているバージョンでは、各.protogo_packageを明示することが推奨され、未指定だと生成時にエラーや警告となる構成が一般的です。

仮に指定が無い、あるいは不適切な値になっていると、次のような問題につながり得ます。

  • 生成コードのimportパスが、実際に配置されるディレクトリやGoモジュールのパスと食い違い、importが解決できずビルドに失敗する。
  • 複数の.protoが互いを参照している場合に、相手側のimportパスを決められず、参照解決に失敗する。
  • 生成ファイルのpackage名が意図せず.protoの名前空間由来の値になり、Goの命名規約やディレクトリ構成と合わなくなる。

こうした問題を避けるため、各.protogo_packageを明示し、実際のディレクトリ構成・モジュールパスと一致させておくのが基本となります。

protocとの関係

protocはProtocol Buffersのコンパイラ本体で、.protoを解析し、各言語向けプラグインを呼び出して生成処理を委譲します。Goの場合、生成はprotoc-gen-go(gRPCサービス向けにはprotoc-gen-go-grpc)というプラグインが担当します。go_packageprotoc自身ではなく、これらGo向けプラグインが解釈するオプションです。

典型的な生成コマンドの例を示します(実際のオプションは利用するバージョンやプロジェクト構成によって異なります)。

# Goコードを生成する例(パスやオプションは構成に依存)
protoc --go_out=. --go_opt=paths=source_relative example.proto

ここで生成先のディレクトリ構造は--go_out--go_opt(たとえばpaths=source_relativeなど)によって制御されますが、生成された各ファイルが名乗るimportパス・パッケージ名はgo_packageが基準になります。出力先の物理的な配置と、go_packageが示す論理的なimportパスが一致するように、オプションとgo_packageの両方を整合させておく必要があります。なお、bufのような上位ツールを使う場合も、内部的に同様のプラグインが動くため、go_packageの考え方は共通します。

落とし穴

つまずきやすいポイント
  • セミコロンの前後の意味を取り違える。 セミコロン前はimportパス、後ろはパッケージ名です。両者を逆に書いたり、パッケージ名のつもりでパスを書いたりすると、生成コードのimportやパッケージ名が意図と異なります。
  • importパスとGoモジュールパスの不整合。 go_packageのパスが、go.modのモジュールパスと配置ディレクトリから導かれる実際のimportパスと食い違うと、生成コードのimportが解決できません。モジュールパスを起点に、実ディレクトリと一致する値を指定します。
  • 複数.protoでの一貫性欠如。 同じディレクトリ(=同じGoパッケージにまとめたい.proto群)には、同一のimportパス・パッケージ名を指すgo_packageを一貫して指定する必要があります。ファイルごとにばらつくと、生成物が想定どおりに1パッケージへまとまらないことがあります。
  • パッケージ名の命名。 Goの慣習では短く小文字のパッケージ名が好まれます。.protoの名前空間(例: example.v1)をそのまま使うとドットを含み不正になるため、セミコロン以降でexamplev1のような有効な識別子を明示するのが無難です。

FAQ

Q1. .protopackage宣言とgo_packageは同じものですか。
別物です。package宣言はprotobuf上の名前空間で、メッセージ名の衝突回避などに使われます。go_packageはGoの生成コードのimportパスとパッケージ名を決めるためのもので、Go側のディレクトリ構成やモジュールパスに合わせて指定します。両者は独立に設定できます。

Q2. セミコロン以降のパッケージ名は必ず書く必要がありますか。
必須ではありません。省略するとimportパスの末尾要素がパッケージ名として使われるのが一般的です。末尾要素をそのままGoのパッケージ名にしてよければ省略でき、別名にしたい場合や末尾要素が識別子として不適切な場合に明示します。

Q3. go_packageはどのバージョンのproto構文で使えますか。
ファイルレベルのオプションとして、proto2・proto3のいずれでも記述できます。実際の解釈や未指定時の挙動は、利用するprotoc-gen-goのバージョンによって異なるため、プロジェクトで採用しているツールチェーンのドキュメントを確認して値を決めるのが確実です。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. go mod init
  2. go mod tidy
  3. go mod download
  4. go build
  5. go_package
  6. protoc

最近更新/作成されたページ