2021年10月6日水曜日

go で grpc の proto ファイルから生成されたソースをローカル利用する 備忘録

ウィンドウズ環境にて grpc の protoc から go のソースを生成してビルドするまでに、えらいハマったので、その備忘録。
まず、grpc の go チュートリアルを読みましたが、唐突に source_relative という未定義の単語が出てきたり、キーワードが、どこの項目と連動しているのか理解できなくて、ちんぷんかんぷんでした。また、生成されたパッケージを利用する方法がわからず、エラー頻出で、体力消耗しました。普段からgoに親しんでいる人にとっては簡単な事なんでしょうけど、普段goを使わない自分には的確な情報にたどり着く手段が少なくて、正解にたどり着くまでに3日ほど浪費しました。
以下のディレクトリ構成で作業をしている前提で話を進めます。
C:/Projects/Hello/
         bin/                           ... インストールで追加される exe のディレクトリ
         pkg/                           ... 追加されたパッケージのディレクトリ
         proto/hello.proto              ... proto 定義
         my.hello/hello.pb.go           ... 自動生成される proto の go ソース
         client.go                      ... クライアント・アプリケーションの go ソース
         go.mod                         ...  go mod init で生成される go.mod ファイル
         go.sum                         ...  go mod init で生成される go.sum ファイル
         goenv.bat           ... go を使うための環境設定バッチファイル

参考として goenv.bat の中身
@echo off
set GOROOT=D:\go
echo 'set GO install directory : %GOROOT%'

rem 
rem GOPATH は非推奨なので使用しません
rem 
rem set GOPATH=D:\somewhere
rem echo 'set Build directory : %GOPATH%'

rem
rem とりあえず、x86 をターゲットとします
rem 
echo 'set GOARCH win32 application'
set GOARCH=386

echo 'set GOARCH : %GOARCH%'

rem
rem プロキシーを利用している環境では、プロキシーを設定する必要があります
rem
rem set http_proxy=192.168.0.254:8080
rem set https_proxy=%http_proxy%

rem echo 'set proxy settings : %http_proxy%'

rem 
rem vcpkg により、protobuf をインストールしているため、そちらを流用しています
rem   d:\vcpkg\installed\x64-windows\tools\protobuf 
rem  が、それに該当します。この下に protoc.exe が配置されています。
rem 
set path=%PATH%;%GOROOT%\bin;d:\vcpkg\installed\x64-windows\tools\protobuf

さて、コマンド・プロンプトで、
C:/Projects/Hello ディレクトリ下に移動して作業を行います。
最初に go mod init コマンドを実行し、go.mod ファイルを生成します
C:\Projects\Hello> go mod init test-client
test-client.exe を作成する下地の準備ができました。
次に必要なgrpcのパッケージ類をインストールします
C:\Projects\Hello> go get -u google.golang.org/grpc
C:\Projects\Hello> go get -u github.com/golang/protobuf/protoc-gen-go
C:\Projects\Hello> go install google.golang.org/protobuf/cmd/protoc-gen-go
C:\Projects\Hello> go install google.golang.org/grpc/cmd/protoc-gen-go-grpc

C:\Projects\Hello\bin 下に protoc-gen-go.exe, protoc-gen-go-grpc.exe が展開されているので
環境変数 PATH に上記 exe が存在するディレクトリを追加します。
C:\Projects\Hello\bin\windows_386 下に exe が展開されていた場合の PATH の追加例は下記
C:\Projects\Hello> set PATH=%PATH%;C:\Projects\Hello\bin\windows_386
パスの追加を怠ると、protoc コマンド実行時に原因不明のエラーに悩まされます。
*.proto の書き方が悪いのか、protoc-gen-go.exe が見つからない事によるエラーなのか
切り分けが容易ではなくなるので、必ず PATH を設定しましょう。

補足:現在は非推奨であるが、GOPATH=c:\gopub というように GOPATHを指定していた場合には
C:\gopub\bin 下に protoc-gen-go.exe, protoc-gen-go-grpc.exe が展開されているかもしれません。
グローバルにインストールした場合は、もしかしたら、%USERPROFILE%\go\bin ディレクトリ下に展開されているかもしれません。

C:\Projects\Hello\proto\hello.proto から
C:\Projects\Hello\ のディレクトリ下に hello.pb.go 等のソースを展開したいので
下記コマンドを実行します。
C:\Projects\Hello> protoc --go_out=plugins=grpc:./ proto/hello.proto
参考として hello.proto の一部抜粋を記載します
syntax = "proto3";

option java_multiple_files = true;
option java_package = "my.hello";
option java_outer_classname = "Hello";

option go_package="my.hello";

package hello;

service Hello {

  rpc SayHello (SendRequest) returns (SendResponse) {}

}

message SendRequest {
  string msg = 1;
}
  
message SendResponse {
  string msg = 1;
}
そうすると、
C:\Projects\Hello\my.hello というディレクトリ下に
(コマンドを実行したディレクトリ直下の
   option go_package="my.hello"; // パッケージ名は . を含んでいなければならない
   で指定したパッケージ名をディレクトリとして)
hello.pb.go というソースが生成されます。

参考として、下記コマンドを実行すると hello.grpc.pb.go も生成されます
C:\Projects\Hello> protoc --go_out=plugins=grpc:./ --go-grpc_out=./ proto/hello.proto
しかしながら、hello.grpc.pb.go で定義されている内容は hello.pb.go に定義されているため
hello.grpc.pb.go を出力してしまうと、再定義のエラーが発生します。
従って、こちらは出力しません。
C++のソースを生成する場合は、対応する両方のファイルが必要だったので、言語間でインターフェイスの統一が取れていないと感じました。


出力されたパッケージをローカルパッケージとして利用するために下記コマンドを実行する必要があります。
C:\Projects\Hello> go mod edit --require my.hello@v0.0.0-local --replace my.hello@v0.0.0-local=./my.hello
コマンド中の v0.0.0 はバージョン情報です。公開して管理するわけではないので、version 0.0.0 の指定をしています。
コマンド中の -local は、ローカルパッケージだよというオマジナイです。
コマンド中で指定するパッケージ名は、あくまで protoファイル中で指定した option go_package="my.hello" を指定します。
go のソースに展開された時にパッケージ名が my_hello へ変更されていたとしても、元の名前を指定します。

このコマンドを実行すると、go.mod ファイルへ
   require () 節中に my.hello v0.0.0-local の行が追加され、
   replace () 節中に my.hello v0.0.0-local => ./my.hello の行が追加されます。
これにより、my.hello が外部モジュールから、ローカルの ./my.hello ディレクトリへとマッピング検索されるようになります。

go ソース内での go_package は、proto ファイルで "my.hello" を指定していますが
"my_hello" という . を _ に置換した名前に変換される仕様となっています。
尚、proto ファイルで指定した package の名前は、go では利用されません。

これでようやく、client.go ファイルに
 import (
   myhello "my.hello"
 )
と書いて、インポートが可能になります。
名前が紛らわしいので、import 時にエイリアス指定("myhello")しておきましょう。パッケージ名に振り回されなくて済みます。

go build するのには、もう一手間が必要です。
C:\Projects\Hello> cd my.hello
C:\Projects\Hello\my.hello> go mod init my_hello
C:\Projects\Hello\my.hello> go mod tidy
C:\Projects\Hello\my.hello> cd ..
これで、protoc から生成された go パッケージが利用できるようになり、go build が可能になりました。

整理すると import myhello "my.hello" は、
   go.mod の require の my.hello v0.0.0-local で参照され、
   go.mod の replace 指定によりローカルの ./my.hello に置き換えられ
   ローカルディレクトリ ./my.hello のソース my_hello パッケージが読み込まれ、
   my_hello を myhello というエイリアスにより参照する事ができる、
という流れになります。

疲れた。ほんとに疲れた。

0 件のコメント: