gRPC サーバーを Go で実装しローカルで動かす

go で gRPC サーバを立てローカルで動かすまでに必要となった知識をまとめた。

実装したコードは以下で公開している。

https://github.com/tetsuya/microservice-learning/

## 概要把握

### RPC とは

Remote Procedure Call (RPC) に関しての説明は、以下のサイトがわかりやすかった。

> 「遠隔手続き呼び出し」とも呼ばれる。ネットワーク上に接続されたほかの端末のプログラムを呼び出し、実行させるための手法。または、そのためのプロトコルを指す。
>
> 同じRPCの仕様に準拠した仕組みを採用していれば、遠隔地にある端末でも、手元の端末からコマンドを入力して処理を実行できるのが特長。機種やOS、プログラミング言語などが異なっていても問題ない。
>
> [RPC | IT用語辞典 | 大塚商会](https://www.otsuka-shokai.co.jp/words/rpc.html)

代表的な RPC の仕様に [SOAP](https://www.w3.org/TR/soap/) や [JSON-RPC](https://www.jsonrpc.org/) などがある。

REST も RPC も HTTP 上で処理を実行し結果を受け取る仕組みだが、REST は [Roy Fielding により提唱された](https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm) HTTP メソッド (GET, POST, PUT, PATCH, DELETE) を活用した設計思想なのに対し、RPC は [RFC 1831](https://tools.ietf.org/html/rfc1831) として策定されている。

### gRPC とは

Google が開発した HTTP/2 上で RPC を実現するフレームワーク。

### Protocol Buffers とは

Protocol Buffers は IDL の一種である。また新しい略語が出てきたので引用しておく。

> IDL（Interface Description Language、インタフェース記述言語）は、特定のプログラミング言語とは別にオブジェクトのインタフェースを指定するために使用される汎用言語です。
>
> [IDL (インタフェース記述言語) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN](https://developer.mozilla.org/ja/docs/Glossary/IDL)

SOAP では WSDL(XML) で記述するが、gRPC では Protocol Buffers を利用する。

Protocol Buffers で定義されたメッセージは、バイナリ列や JSON などにシリアライズできる。公式ドキュメントでは XML と Protocol Buffers を比較し、構造がシンプルでファイルサイズが小さく、パースが早いことをメリットとして押し出している。

[Why not just use XML? | Protocol Buffers](https://developers.google.com/protocol-buffers/docs/overview#whynotxml)

### Protocol Compiler とは

C++ 製のコンパイラで、 Protocol Buffers (`.proto`) ファイルから、gRPC で処理をメッセージの受け渡しを行うためのコードを自動生成してくれる。protoc とも呼ばれる。

利用する際は、バックエンドの言語に合わせてプラグインとセットで使用する。よく、「gRPC は様々な言語に対応している」と言われるが、それが Protocol Compiler のプラグインが用意されているかに依存する。Go の場合、[progo-gen-go](https://github.com/golang/protobuf) がそれにあたる。

[protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format](https://github.com/protocolbuffers/protobuf)

## 環境構築

gRPC と Protocol Buffers の大枠を把握したところで、まずは環境を構築していく。バックエンドの言語は Go を選択した。

[Go Quick Start – gRPC](https://grpc.io/docs/quickstart/go/) を読みながら進めた。protoc は [Homebrew](https://brew.sh/) で、 gRPC と protoc-gen-go は `go get` で入れた。

## 実装

最終的に Protocol Buffers は以下のような形になった。

```protobuf
syntax = "proto3";

import "google/protobuf/timestamp.proto";

service UserService {
  rpc GetUser(GetUserRequest) returns (User) {}
}

message GetUserRequest {
  int32 id = 1;
}

message User {
  int32 id = 1;
  string name = 2;
  string email = 3;

  google.protobuf.Timestamp updated_at = 15;
}
```

### gRPC サービスの作成

Protocol Buffers の書き方は、[Protocol Buffer Basics: Go | Protocol Buffers](https://developers.google.com/protocol-buffers/docs/gotutorial#defining-your-protocol-format) を参考に進めた。ただ、gRPC サービスの構成に関しては [Defining the service – gRPC](https://grpc.io/docs/tutorials/basic/go/#defining-the-service) で触れられており、全体像は[サンプルコード](https://github.com/grpc/grpc-go/blob/8655d473adfe8fa6619fd42cd2a001b412695b21/examples/helloworld/helloworld/helloworld.proto) を参考に掴んでいった。

### 命名規則

RPC メソッドの命名に規則はないため、Google の API 設計ガイドで提唱されている[標準メソッド](https://cloud.google.com/apis/design/standard_methods?hl=ja)を利用することにした。また、メソッド名、リクエスト/レスポンスメッセージも Google の命名規則に従うことにした。

> | 動詞 | 名詞 | メソッド名 | リクエスト メッセージ | レスポンス メッセージ |
> | --- | --- | --- | --- | --- |
> | List | Book | ListBooks | ListBooksRequest | ListBooksResponse |
> | Get | Book | GetBook | GetBookRequest | Book |
> | Create | Book | CreateBook | CreateBookRequest | Book |
> | Update | Book | UpdateBook | UpdateBookRequest | Book |
> | Rename | Book | RenameBook | RenameBookRequest | RenameBookResponse |
> | Delete | Book | DeleteBook | DeleteBookRequest | google.protobuf.Empty |
>
> [命名規則  |  Cloud API  |  Google Cloud](https://cloud.google.com/apis/design/naming_convention?hl=ja#method_names)

### フィールド番号

proto ファイルを見ると、 `= 1` と番号を振ってある。これはフィールド番号と呼ばれ、飛び番になっても構わないが、ユニークな番号を割り当てる必要がある。

1-15 の番号はフィールド番号と型を含めて、1バイトにエンコードされるが、16-2047の範囲は2バイトと、フィールドの番号が大きくなるとエンコード後のサイズが増加する。そのため、利用頻度の高いフィールドは 1-15 の番号を割り当てることが推奨されている。割り当てた番号は後から変えることを推奨されていないので、考えながら割り当てたい。

[Assigning Field Numbers | Protocol Buffers](https://developers.google.com/protocol-buffers/docs/proto3#assigning-field-numbers)

### 型のインポート

Protocol Buffers では独自の型を定義できる他に、公開されている型の定義ファイルをインポートできる。gRPC の go plugin には [timestamp型が含まれおり](https://github.com/golang/protobuf/blob/b285ee9cfc6c881bb20c0d8dc73370ea9b9ec90f/ptypes/timestamp/timestamp.proto)意識せずに使えるようになっている。

一方、[googleapis/googleapis](https://github.com/googleapis/googleapis) などの公開定義ファイルの管理方法は様々なよう。ざっと調べたところ、以下の方法が見つかった。今のプロジェクトでは go mod での管理をすることにした。

1. go のパッケージ管理ツール (dep or go mod) で管理
2. 必要な定義ファイルだけをソースコードを同じように git 管理する
3. [stormcat24/protodep](https://github.com/stormcat24/protodep) で管理する

### インタフェースの生成

ここから、Protocol Compiler + プラグインを使い Go から呼び出せるインターフェースを生成した。

```console
$ protoc --go_out=plugins=grpc:./protogen/ --proto_path=../../pb/ user.proto
```

ディレクトリ構成は [GoogleCloudPlatform/microservices-demo](https://github.com/GoogleCloudPlatform/microservices-demo/tree/master/src/shippingservice) の shippingservice を参考にした。

### サーバサイドの実装

[Creating the server – gRPC](https://grpc.io/docs/tutorials/basic/go/#creating-the-server) を参考にしながら、サービスメソッドの実装を進めていった。

gRPC サーバの実装は [Starging the server – gRPC](https://grpc.io/docs/tutorials/basic/go/#starting-the-server) を参考にしつつも、[helloworld](https://github.com/grpc/grpc-go/blob/8655d473adfe8fa6619fd42cd2a001b412695b21/examples/helloworld/greeter_server/main.go) の実装を真似た。

結果的に、以下のような形になった。

```go
package main

import (
  "context"
  "log"
  "net"
  "time"

  "github.com/golang/protobuf/ptypes"
  pb "github.com/tetsuya/microservice-learning/src/userservice/protogen"
  "google.golang.org/grpc"
)

const (
  port = ":8080"
)

type server struct{}

func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
  log.Printf("Received: %v", req.Id)
  // see: https://stackoverflow.com/a/52089674/1285818
  now, _ := ptypes.TimestampProto(time.Now())
  return &pb.User{Id: req.Id, Name: "John Smith", Email: "johnsmith@example.com", UpdatedAt: now}, nil
}

func main() {
  lis, err := net.Listen("tcp", port)
  if err != nil {
    log.Fatalf("failed to listen: %v", err)
  }
  s := grpc.NewServer()
  pb.RegisterUserServiceServer(s, &server{})
  log.Printf("Listening on %v", port)
  if err := s.Serve(lis); err != nil {
    log.Fatalf("failed to serve: %v", err)
  }
}
```

## 動作確認

### gRPC サーバの起動

```console
$ go run src/userservice/main.go
2019/05/19 17:03:37 Listening on :8080
```

### 利用可能なサービスの一覧を取得する

gRPC サーバに直接リクエストを送るために [fullstorydev/grpcurl](https://github.com/fullstorydev/grpcurl) を利用した。

```console
grpcurl -import-path pb/ -proto user.proto localhost:8080 list
UserService
```

### リクエストの送信

期待通りのレスポンスを受け取ることができた。

```console
$ grpcurl -plaintext -import-path pb/ -proto user.proto -d '{"id":"123"}' localhost:8080 UserService/GetUser
{
  "id": 123,
  "name": "John Smith",
  "email": "johnsmith@example.com",
  "updatedAt": "2019-05-19T08:04:42.777350Z"
}
```

サーバ側のログも期待通りに出力されたことを確認した。

```console
$ go run src/userservice/main.go
...
2019/05/19 17:04:42 Received: 123
```

## テスト
テスティングフレームワークは、[Ginkgo](https://onsi.github.io/ginkgo/) を使うことにした。Ruby を使っていた時に Rspec を利用していた経験から書きっぷりに既視感があることと、CircleCI との連携を考えた時に JUnit 互換の XML が出力できることが採用の理由。

```go
package main

import (
	"context"

	. "github.com/onsi/ginkgo"
	. "github.com/onsi/gomega"
	pb "github.com/tetsuya/microservice-learning/src/userservice/protogen"
)

var _ = Describe("Main", func() {
	Context("Request with total price", func() {
		var (
			resp *pb.User
			err  error
		)

		BeforeEach(func() {
			s := server{}
			id := int32(123)
			req := &pb.GetUserRequest{Id: id}
			resp, err = s.GetUser(context.Background(), req)
		})

		It("will return no error", func() {
			Expect(err).ShouldNot(HaveOccurred())
		})
		It("will return requested ID", func() {
			Expect(resp.Id).To(Equal(int32(123)))
		})
		It("will return mocked name", func() {
			Expect(resp.Name).To(Equal("John Smith"))
		})
		It("will return mocked email", func() {
			Expect(resp.Email).To(Equal("johnsmith@example.com"))
		})
	})
})
```