はじめに
この記事ではgo × gRPCで簡単なリクエスト送信・レスポンスの表示を説明していきます。
大きく分けて、下記の3つを順番に実行していきます。
- protoファイルからコードを自動生成
 - goでgRPCサーバーを起動
 - goでクライアントを実行してリクエスト送信・レスポンス表示
 
最初はローカルで実行して、次に同じことをDocker環境でも実行します。
この記事を書くにあたり、下記を参考にさせていただきました。
今回実装したリポジトリです。
全体のコードはこちらをご覧くだしさい。
ローカル環境で実行する場合
パッケージインストール
まずは必要なパッケージをインストールします。
$ brew install protobuf
$ go get -u google.golang.org/grpc
$ go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
パッケージの依存関係を管理するためのgo.modファイルを生成します。
$ cd study_grpc # 作業ディレクトリ
$ go mod init mygrpc
ディレクトリ構成
今回は下記のようなディレクトリ構成にしました。
./study_grpc # 作業ルートディレクトリ
├─ api
│   └─ hello.proto # protoファイル
├─ cmd
│   ├─ server
│   │ └─ main.go
│   └─ client
│        └─ main.go
├─ pkg
│   └─ grpc # ここにコードを自動生成させる
│         ├─ hello.pb.go
│         └─ hello_grpc.pb.go
├─ go.mod
└─ go.sum
protoファイル作成
api/hello.proto
// protoのバージョンの宣言
syntax= "proto3";
// protoファイルから自動生成させるGoのコードの置き先
// (詳細は4章にて)
option go_package = "pkg/grpc";
// packageの宣言
package myapp;
// サービスの定義
service GreetingService {
   // サービスが持つメソッドの定義
  rpc Hello (HelloRequest) returns (HelloResponse);
}
// 型の定義
message HelloRequest {
   stringname = 1;
}
message HelloResponse {
   stringmessage = 1;
}
protocコマンドでコードを自動生成
下記のコマンドを実行するとpkg/grpcディレクトリ直下に、2つのファイルが生成されます。
$  cd api
$ protoc --go_out=../pkg/grpc --go_opt=paths=source_relative \
         --go-grpc_out=../pkg/grpc --go-grpc_opt=paths=source_relative \
         hello.proto
--go_opt=paths=source_relativeは相対パスを指定するオプションです。
./study_grpc/apiでコマンドを実行したため、相対パスでファイルの保存先を指定しています。
ちなみに--go_opt=paths=source_relativeを使わず、./study_grpc/から実行する場合は、
下記のコマンドになります。
$ protoc --go_out=./ --go-grpc_out=./ api/hello.proto
gRPCサーバーを作成
cmd/server/main.go
package main
import (
  "fmt"
  "net"
  "log"
  "os"
  "context"
  "os/signal"
  "google.golang.org/grpc"
  "google.golang.org/grpc/reflection"
  hellopb "mygrpc/pkg/grpc"
)
func NewMyServer() *myServer {
  return &myServer{}
}
func main() {
  // 1. 8080番portのLisnterを作成
  port := 8080
  listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
  if err != nil {
    panic(err)
  }
  // 2. gRPCサーバーを作成
  s := grpc.NewServer()
  // 3. gRPCサーバーにGreetingServiceを登録
  hellopb.RegisterGreetingServiceServer(s, NewMyServer())
  // 4. サーバーリフレクションの設定
  reflection.Register(s)
  // 5. 作成したgRPCサーバーを、8080番ポートで稼働させる
  go func() {
    log.Printf("start gRPC server port: %v", port)
    s.Serve(listener)
  }()
  // 6.Ctrl+Cが入力されたらGraceful shutdownされるようにする
  quit := make(chan os.Signal, 1)
  signal.Notify(quit, os.Interrupt)
  <-quit
  log.Println("stopping gRPC server...")
  s.GracefulStop()
}
type myServer struct {
  hellopb.UnimplementedGreetingServiceServer
}
func (s *myServer) Hello(ctx context.Context, req *hellopb.HelloRequest) (*hellopb.HelloResponse, error) {
  // リクエストからnameフィールドを取り出して
  // "Hello, [名前]!"というレスポンスを返す
  return &hellopb.HelloResponse{
    Message: fmt.Sprintf("Hello, %s!", req.GetName()),
  }, nil
}
gRPCurlをインストール
gPRCはcurlで動作確認できませんが、gPRCurlを使うことで,
ローカル環境でも動作確認することができます。
$ brew  install grpcurl
gRPCサーバーを起動
これでgRPCサーバーを起動する準備ができました!
下記コマンドを実行してgRPCが起動すれば成功です!
$ go cmd/server/run main.go
2023/02/13 10:01:38 start gRPC server port: 8080
gRPCクライアントを作成
cmd/server/main.go
package main
import (
  "bufio"
  "fmt"
  "os"
  "log"
  "context"
  "google.golang.org/grpc"
  "google.golang.org/grpc/credentials/insecure"
  hellopb "mygrpc/pkg/grpc"
)
var (
  scanner *bufio.Scanner
  client  hellopb.GreetingServiceClient
)
func main() {
  fmt.Println("start gRPC Client.")
  // 1. 標準入力から文字列を受け取るスキャナを用意
  scanner = bufio.NewScanner(os.Stdin)
  // 2. gRPCサーバーとのコネクションを確立
  address := "localhost:8080"
  conn, err := grpc.Dial(
    address,
    grpc.WithTransportCredentials(insecure.NewCredentials()),
    grpc.WithBlock(),
  )
  if err != nil {
    log.Fatal("Connection failed.")
    return
  }
  defer conn.Close()
  // 3. gRPCクライアントを生成
  client = hellopb.NewGreetingServiceClient(conn)
  for {
    fmt.Println("1: send Request")
    fmt.Println("2: exit")
    fmt.Print("please enter >")
    scanner.Scan()
    in := scanner.Text()
    switch in {
    case "1":
      Hello()
    case "2":
      fmt.Println("bye.")
      goto M
    }
  }
M:
}
func Hello() {
  fmt.Println("Please enter your name.")
  scanner.Scan()
  name := scanner.Text()
  req := &hellopb.HelloRequest{
    Name: name,
  }
  res, err := client.Hello(context.Background(), req)
  if err != nil {
    fmt.Println(err)
  } else {
    fmt.Println(res.GetMessage())
  }
}
クライアントを実行して動作確認
gRPCサーバーとは別のターミナルを開いて、下記を実行することで、
gPRCサーバーに対して、リクエスト送信・レスポンス表示をすることができます。
$ go run cmd/client/main.go
start gRPC Client.
1: Hello
2: exit please enter >1
Please enter your name.
shungo
Hello, shungo!
1: Hello
2: exit please enter >2
bye.
このように、リクエスト送信・レスポンスの表示ができれば成功です!
Docker環境で実行する場合
これまではローカル環境にパッケージをインストールして、ローカル環境でgRPC用のコードを生成していました。
gRPC入門の記事を見るとローカルで実行するものが多いですが、バージョン管理などを考慮したらやはりDocker環境で実行したいですよね。
そこで、これまでやってきたことを今後はDocker環境で実行してみようと思います。
Dockerfile
FROM golang:1.19.5-buster
ENV PROTOBUF_VERSION3.17.3
RUN apt-getupdate \
    && apt-getinstall -y protobuf-compiler unzip --no-install-recommends \
    && apt-getclean \
    && rm-rf /var/lib/apt/lists/*
WORKDIR /grpc
COPY. /grpc
## build時のみ使いたいので、runで実行。
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
RUN go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
RUN go get google.golang.org/grpc/internal/channelz@v1.53.0
docker-compose.yml
version:'3'
services:
  grpc:
    build: .
    volumes:
      - .:/grpc
    ports:
      - "8080:8080"
   tty: true
Docker環境で実行する
1つ目のターミナルでdocker-composeを起動します。
$ docker-compose up
2つ目のターミナルでコンテナに入り、gRPCサーバーを起動します。
$ docker-compose exec grpc bash
> go run cmd/server/main.go
2023/02/13 10:01:38 start gRPC server port: 8080
3つ目のターミナルでコンテナに入り、クライアントを動かします。
$ docker-compose exec grpc bash
> go run cmd/client/main.go
1: Hello
2:exit
please enter>1
Please enter your name.
shungo
Hello, shungo!
1: Hello
2:exit
please enter>2
bye.
これでDocker環境でもgRPCを動かせることを確認できました!
上記では実行していませんが、コンテナ内で同じようにprotocコマンドでコードを生成することも可能です。