Go言語でgRPC-gatewayの門を叩いたお話

# はじめに
少し前にgRPCの入門記事を書きました。
前回記事：[Go言語でgRPCの門を叩いたお話](https://qiita.com/kito_engineer/items/1bba11082b0a69697ead)
その時はgRPC通信でserver側のメソッドを利用しましたが、今回はprotoファイルに少しオプションを追加することでAPIも用意できるようにしてくれる gRPC-gateway の門を叩こうと思います。

# 準備
必要なprotocol buffersのライブラリをインストールするところから始めます。

```
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/golang/protobuf/protoc-gen-go
```

また下記のGoパッケージが必要になるので自動でインストールしてくれるようgo.modファイルを生成しておきます。

- github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway
- github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2

go.mod生成

```
go mod init
```

# 実装
今回はそのまま実装を見てもらったほうが早いと思うのでさっそく実装に入っていきます。
使うメソッドは前回同様、名前と年齢を渡して挨拶コメントが返ってくるもので試していきます。
github: https://github.com/jgvkmea/grpc-gateway-entry

## ディレクトリ構造

```
.
├── README.md
├── go.mod
├── go.sum
├── grpc
│   └── main.go
├── pb
│   ├── sample.pb.go
│   ├── sample.pb.gw.go
│   └── sample.proto
└── rest
    └── main.go
```

## protoファイル
gRPCでインターフェースを定義するときと同様にリクエストとレスポンスのmessage型とserviceを定義します。
まずはgRPC-gatewayを使うために必要なprotoファイルをimportします。

```protobuf
import "google/api/annotations.proto";
```

gRPC-gateway を使う場合は下のフォーマットでAPIのURLを定義します。

```protobuf
option (google.api.http) = {
  HTTPメソッド: "APIのパス"  （{フィールド名} でパスにパラメータ指定も可能）
  （リクエストボディがある場合）
  body: "フィールド指定"  （リクエストのフィールドすべての場合は * を指定）
};
```

protoファイル全体はこちらになります。

```protobuf:pb/sample.proto
syntax = "proto3";

package sample;

option go_package = "./pb";

import "google/api/annotations.proto";

message HelloPersonMessageRequest {
  string name = 1;
  uint32 age = 2;
}

message HelloPersonMessageResponse {
  string hello_message = 1;
}

service Hello {
  rpc HelloPerson (HelloPersonMessageRequest) returns (HelloPersonMessageResponse) {
    option (google.api.http) = {
      get: "/v1/greet"
    };
  }
}
```

## Goファイル生成
まずはgRPCの時と同様、``.pb.go``ファイルを生成するコマンドを叩きます。
gRPCの時のコマンドに加え、``annotations.proto``をimportするためのパスを追加してます。（2行目）

```
protoc -I. \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --go_out=plugins=grpc:. \
  ./pb/sample.proto
```

ちなみに``-I``オプションはimportするprotoファイルのパスを指定しているので、``-I``でパス指定した分protoファイル内のimportは省略できます。

```
例： annotations.proto
// ファイルパス
$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api/annotations.proto

// -I で下記を指定する場合
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api

// protoファイル内のimport文は下記でOK
import "annotations.proto";
```

次は``.gw.pb.go``ファイルを生成します。
先程のprotocコマンドの3行目を書き換えて以下のコマンドで生成します。

```
protoc -I. \
  -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
  --grpc-gateway_out=. \
  ./pb/sample.proto
```

## gRPC server 実装
まずはgRPC側のserverから実装していきます。
といっても前と同じです。

```grpc/main.go
package main

import (
	"context"
	"fmt"
	"log"
	"net"

	"github.com/jgvkmea/grpc-gateway-entry/pb"

	"google.golang.org/grpc"
)

// HelloServer serverの構造体
type HelloServer struct{}

var grpcPort = ":8080"

// HelloPerson HelloPersonのメソッド
func (h *HelloServer) HelloPerson(ctx context.Context, req *pb.HelloPersonMessageRequest) (res *pb.HelloPersonMessageResponse, e error) {
	text := fmt.Sprintf("%sさん(%d歳)、こんにちは。", req.Name, req.Age)
	res = &pb.HelloPersonMessageResponse{HelloMessage: text}
	return res, nil
}

func main() {
	listenPort, err := net.Listen("tcp", grpcPort)
	if err != nil {
		log.Fatalln(err)
	}
	// サーバー登録
	s := grpc.NewServer()
	pb.RegisterHelloServer(s, &HelloServer{})
	s.Serve(listenPort)
}
```

## REST API server 実装
次にAPIのサーバーを実装していきます。
基本はgithubのサンプルから取ってきて必要箇所だけ変更してます。
サンプル： https://github.com/grpc-ecosystem/grpc-gateway#usage の5番です。

変更箇所

- grpcServerEndpoint のポート番号をgRPC serverでリッスンしているポートに変更
- ``RegisterYourServiceHandlerFromEndpoint``を自分の実装したものに合わせて名前を変える。

実際に変更したあとのコードはこちらです。

```rest/main.go
package main

import (
	"context"
	"flag"
	"net/http"

	"github.com/golang/glog"
	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"

	gw "github.com/jgvkmea/grpc-gateway-entry/pb"
)

var (
	// command-line options:
	// gRPC server endpoint
	grpcServerEndpoint = flag.String("grpc-server-endpoint", "localhost:8080", "gRPC server endpoint")
	restPort           = ":8081"
)

func run() error {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	defer cancel()

	// Register gRPC server endpoint
	// Note: Make sure the gRPC server is running properly and accessible
	mux := runtime.NewServeMux()
	opts := []grpc.DialOption{grpc.WithInsecure()}
	err := gw.RegisterHelloHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
	if err != nil {
		return err
	}

	// Start HTTP server (and proxy calls to gRPC server endpoint)
	return http.ListenAndServe(restPort, mux)
}

func main() {
	flag.Parse()
	defer glog.Flush()

	if err := run(); err != nil {
		glog.Fatal(err)
	}
}
```

## 実行
それではここまで実装したものを実行してみます。
まずは gRPC server から起動してみます。

```
$ go run grpc/main.go
```

続いて別のセッションから REST API server を起動します。

```
$ go run rest/main.go
```

この状態でさらに別セッションからcurlでAPIを叩いてみます。

```
$ curl 'localhost:8081/v1/greet?name=Qiita&age=9'
{"helloMessage":"Qiitaさん(0歳)、こんにちは。"}
```

ちゃんと挨拶コメントが返ってきましたね。

# おわりに
前回に引き続き、今回はgRPC-gatewayを使って前回作ったサーバーをAPIとして公開できるようにしてみました。
APIの設定等かなり平易で初心者でもすぐに使えそうな印象でした。
ぜひ皆さんも門を叩いてみてください。