golangでgRPC作ってみた

Mac（M1）使ってます。
# 作りたいサービス
名前送ったら（Cli)、なんか（SV)返ってくるやつ。
具体的には以下の４つのメソッドを作成する。
1. 名前を送ると、”Hello 名前”って返ってくる。
2. 名前を送ると、”Hello 名前!”, "Hello 名前!!"みたいに「！」が増えて返ってくる。
3. 名前を複数回送ると、まとめて"Hello 名前１、名前２、、、"って返ってくる。
4. 名前を何個も送ると、送った分"Hello 名前"って返ってくる。



# gRPCとは（超簡単に）

 [これよかった](https://qiita.com/gold-kou/items/a1cc2be6045723e242eb)
# ディレクトリ構成
```
current dir
    └──hello
          ├── client
          ├── proto
          └── server
```
# まずはProtocol Bufferの記述


```protobuf:hello/proto/hello.proto
// バージョン
syntax = "proto3";

// Goのパッケージ名として使われる。
// 別Protoファイルで同じmessageが使用されていた場合に衝突を回避できる。
package hello;

// Goのインポートパス
option go_package = "github.com/hirohokke/grpc-go/hello/proto";

// リクエストを宣言（Goのstructのようなもの)
message HelloRequest {
    // 型　フィールド名 = タグ;
    string first_name = 1;
}

// レスポンスを宣言（Goのstructのようなもの)
message HelloResponse {
    string result = 1;
}

// サービスを宣言
// ここに宣言されたRPCをクライアントから呼び出す
service HelloService {
    // リクエストを渡して、レスポンスを返す
    rpc Hello (HelloRequest) returns (HelloResponse);
    // レスポンスにstreamを付けることでSVは複数レスポンスを返せる
    rpc HelloAmp (HelloRequest) returns (stream HelloResponse);
    // リクエストにstreamを付けることでクライアントは複数リクエストを渡せる
    rpc HelloManyTimes (stream HelloRequest) returns (HelloResponse);
    // リクエスト、レスポンスにstreamを付けることで双方向でストリーミングできる
    rpc HelloEveryone (stream HelloRequest) returns (stream HelloResponse);
}
```
詳しいことは、[公式](https://protobuf.dev/)
とか、[これ日本語](https://qiita.com/CyLomw/items/9aa4551bd6bb9c0818b6)

**protobufについて簡単に超ざっくり説明**
* データを構造化してシリアライズするための、Googleによって作られた機構。
* バイナリでシリアライズされるので、サイズが小さくPayloadが小さくて通信が早くなる。（よく使われるJSONやXMLに比べて←プレーンテキスト）
* 型宣言するので、型の安全性が保証される。
* いろんな言語に対応
etc.

# protobufのコンパイル
まず、protobufのインストール
```terminal
$ brew install protobuf
```

Go用のprotobufランタイムをインストール
```terminal
$ go get -u google.golang.org/grpc
$ go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
```

Go用にコンパイル
```terminal
$ protoc -Ihello/proto \
         --go_opt=module=github.com/hirohokke/grpc-go \
         --go_out=. \
         --go-grpc_opt=module=github.com/hirohokke/grpc-go\
         --go-grpc_out=. hello/proto/*.proto
```
こうすると、hello/proto配下にgoコードが生成される。
[`-I`は依存関係を解決するみたい。](https://christina04.hatenablog.com/entry/protoc-usage)今回はないけど。

# Goファイル作成
準備ができたので、Goファイルの作成
※エラーハンドリングしてたりしてなかったり等ありますが、気にしないでください。適当です。
## Server
```golang:hello/server/main.go
package main

import (
	pb "github.com/hirohokke/grpc-go/hello/proto"
	"google.golang.org/grpc"
	"log"
	"net"
)

// gRPCサーバーで受け付けるアドレス
var addr string = "0.0.0.0:50051"

// gRPCサーバーの構造体を宣言
type Server struct {
	pb.HelloServiceServer
}

func main() {
	// 指定したアドレスでTCPプロトコルで通信受付インスタンス生成
	lis, err := net.Listen("tcp", addr)
	if err != nil {
		log.Fatalf("Failed to listen on: %v\n", err)
	}

	log.Printf("Listen on %s\n", addr)

	// grpcサーバーを生成し、Helloサービスを設定
	s := grpc.NewServer()
	pb.RegisterHelloServiceServer(s, &Server{})

	// サーバー起動
	if err = s.Serve(lis); err != nil {
		log.Fatalf("Failed to serve: %v\n", err)
	}
}

```

次に、protobufに記述したRPCサービスを実装
```go:hello/client/hello.go
package main

import (
	"context"
	"fmt"
	pb "github.com/hirohokke/grpc-go/hello/proto" // protobufのインポート
	"io"
	"log"
	"strings"
)

// 1. 名前を送ると、”Hello FirstName”を返す
func (s *Server) Hello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
	log.Printf("Helloが呼び出されました: %v\n", in)
	// ここでは、文字列を生成し、レスポンスを返すだけ。
	return &pb.HelloResponse{
		Result: fmt.Sprintf("Hello %s", in.FirstName),
	}, nil
}

// 2. !が増幅して返ってくる
func (s *Server) HelloAmp(in *pb.HelloRequest, stream pb.HelloService_HelloAmpServer) error {
	log.Printf("HelloAmpが呼び出されました: %v\n", in)

	for i := 0; i < 5; i++ {
		// レスポンス作成
		res := fmt.Sprintf("Hello %s%s", in.FirstName, strings.Repeat("!", i))

		// クライアントにレスポンスを送信
		err := stream.Send(&pb.HelloResponse{Result: res})
		if err != nil {
			log.Fatalf("レスポンス送信中にエラーが発生: %v\n", err)
		}

	}

	return nil
}

// 3. 複数回送られてくるリクエストを結合して返す
func (s *Server) HelloManyTimes(stream pb.HelloService_HelloManyTimesServer) error {
	log.Println("HelloManyTimesが呼び出されました。")

	res := "Hello"

	// 無限ループでリクエストを受け取り続ける
	for {
		// リクエストを取得
		req, err := stream.Recv()

		// リクエスト終端の場合、レスポンスを送信
		if err == io.EOF {
			return stream.SendAndClose(&pb.HelloResponse{
				Result: res,
			})
		}

		// 終端以外のエラーの場合
		if err != nil {
			log.Fatalf("リクエスト読み取りでエラー発生: %v\n", err)
		}

		res = res + " " + req.FirstName
	}
}

// 4. リクエストごとに返信する
func (s *Server) HelloEveryone(stream pb.HelloService_HelloEveryoneServer) error {
	log.Println("HelloEveryoneが呼び出されました")

	for {
		// streamからリクエストを受け取り
		req, err := stream.Recv()
		if err == io.EOF {
			return nil
		}
		if err != nil {
			log.Fatalf("リクエスト読み取り中にエラー発生: %v\n", err)
		}

		res := fmt.Sprintf("Hello %s", req.FirstName)
		// streamにレスポンスを送信
		err = stream.Send(&pb.HelloResponse{Result: res})
		if err != nil {
			log.Fatalf("レスポンス送信中にエラー発生: %v\n", err)
		}
	}
}
```

次にクライアント
# Client
```golang:hello/client/main.go
package main

import (
	pb "github.com/hirohokke/grpc-go/hello/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"log"
)

// 作成したgRPCサーバーのアドレス
var addr string = "localhost:50051"

func main() {
	// 作ったサーバーとのコネクタを作成
	// ssl認証なし
	conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("接続失敗: %v\n", err)
	}
	defer conn.Close()

	// gRPCクライアントの生成
	c := pb.NewHelloServiceClient(conn)

	doHello(c)
	doHelloAmp(c)
	doHelloManyTimes(c)
	doHelloEveryone(c)
}
```
呼び出し部分
```golang:hello/client/hello.go
package main

import (
	"context"
	pb "github.com/hirohokke/grpc-go/hello/proto"
	"io"
	"log"
	"time"
)

// 1. Hello呼び出し
func doHello(c pb.HelloServiceClient) {
	log.Println("---doHello---")

	// リクエストを作成
	req := &pb.HelloRequest{FirstName: "Hokke"}

	// Helloサービス呼び出し
	res, err := c.Hello(context.Background(), req)
	if err != nil {
		log.Fatalf("Helloの呼び出しでエラー: %v\n", err)
	}
	log.Printf("%s\n", res.Result)
}

// 2. HelloAmp呼び出し
func doHelloAmp(c pb.HelloServiceClient) {
	log.Println("---doHelloAmp---")

	// リクエスト作成
	req := &pb.HelloRequest{FirstName: "Hokke"}

	// gRPCとのstream生成
	stream, err := c.HelloAmp(context.Background(), req)
	if err != nil {
		log.Fatalf("HelloAmp呼び出しでエラー: %v\n", err)
	}

	// EOFが送られるまでレスポンスを取得し続けるため、無限ループ
	for {
		// レスポンス取得
		res, err := stream.Recv()

		// レスポンスが終わった場合
		if err == io.EOF {
			break
		}

		if err != nil {
			log.Fatalf("レスポンス取得でエラー発生: %v\n", err)
		}

		log.Printf("Response is %s\n", res.Result)
	}
}

// 3. HelloManyTimes呼び出し
func doHelloManyTimes(c pb.HelloServiceClient) {
	log.Println("---doHelloManyTimes---")

	// stream生成
	stream, err := c.HelloManyTimes(context.Background())
	if err != nil {
		log.Fatalf("HelloManyTimes呼び出しでエラー")
	}

	// リクエスト生成
	reqs := []*pb.HelloRequest{
		{FirstName: "Hokke"},
		{FirstName: "Hiro"},
		{FirstName: "Hoge"},
	}

	// リクエスト数分送信
	for _, req := range reqs {
		log.Printf("Sending Request: %v\n", req)
		stream.Send(req)
	}

	res, err := stream.CloseAndRecv()
	if err != nil {
		log.Fatalf("レスポンス取得でエラー: %v\n", err)
	}

	log.Println(res.Result)
}

// 4. HelloEveryone呼び出し
func doHelloEveryone(c pb.HelloServiceClient) {
	log.Println("---doHelloEveryone---")

	stream, err := c.HelloEveryone(context.Background())
	if err != nil {
		log.Fatalf("呼び出しでエラー")
	}

	reqs := []*pb.HelloRequest{
		{FirstName: "Hokke"},
		{FirstName: "Hiro"},
		{FirstName: "Hoge"},
	}

	// ゴルーチンを使うので、待機チャネルを使用
	waitc := make(chan struct{})

	go func() {
		for _, req := range reqs {
			log.Printf("Sending request: %v\n", req)
			stream.Send(req)
			// わかりやすいように、１s待つ
			time.Sleep(1 * time.Second)
		}
		stream.CloseSend()
	}()

	go func() {
		for {
			res, err := stream.Recv()
			if err == io.EOF {
				break
			}
			if err != nil {
				log.Fatalf("レスポンス取得中にエラー: %v\n", err)
			}
			log.Printf("Response is %s\n", res.Result)
		}
		close(waitc)
	}()

	<-waitc
}
```

# 実行
サーバー、クライアントそれぞれモジュール作成
```terminal
$ go build -o bin/server ./hello/server
$ go build -o bin/client ./hello/client
```
あとは実行するだけ
```terminal:terminal1
$ ./bin/server
2023/04/26 22:04:55 Listen on 0.0.0.0:50051
```

```terminal:terminal2
$ ./bin/client
2023/04/26 22:05:03 ---doHello---
2023/04/26 22:05:03 Hello Hokke
2023/04/26 22:05:03 ---doHelloAmp---
2023/04/26 22:05:03 Response is Hello Hokke
2023/04/26 22:05:03 Response is Hello Hokke!
2023/04/26 22:05:03 Response is Hello Hokke!!
2023/04/26 22:05:03 Response is Hello Hokke!!!
2023/04/26 22:05:03 Response is Hello Hokke!!!!
2023/04/26 22:05:03 ---doHelloManyTimes---
2023/04/26 22:05:03 Sending Request: first_name:"Hokke"
2023/04/26 22:05:03 Sending Request: first_name:"Hiro"
2023/04/26 22:05:03 Sending Request: first_name:"Hoge"
2023/04/26 22:05:03 Hello Hokke Hiro Hoge
2023/04/26 22:05:03 ---doHelloEveryone---
2023/04/26 22:05:03 Sending request: first_name:"Hokke"
2023/04/26 22:05:03 Response is Hello Hokke
2023/04/26 22:05:04 Sending request: first_name:"Hiro"
2023/04/26 22:05:04 Response is Hello Hiro
2023/04/26 22:05:05 Sending request: first_name:"Hoge"
2023/04/26 22:05:05 Response is Hello Hoge
```
```terminal:terminal1クライアント実行後
$ ./bin/server 
2023/04/26 22:04:55 Listen on 0.0.0.0:50051
2023/04/26 22:05:03 Helloが呼び出されました: first_name:"Hokke"
2023/04/26 22:05:03 HelloAmpが呼び出されました: first_name:"Hokke"
2023/04/26 22:05:03 HelloManyTimesが呼び出されました。
2023/04/26 22:05:03 HelloEveryoneが呼び出されました
```
クライアントで一気にサービスを呼び出してるけど、別々で呼び出した方が処理わかりやすい。

# おまけ
#### エラーについて
サーバーからクライアントにエラーも返せる
```golang:server.go
// 処理
if err != nil {
    return status.Errorf(
        codes.Internal, // 例）内部エラー
        fmt.Sprinf("内部エラー発生"),
    )
}
```
```golang:client.go
// 例
err := callGRPC()
if err != nil {
    e, ok := status.FromError(err)
    if ok {
        if e.Code() == codes.Internal {
            // エラーハンドリング
        } else {
            log.Fatalf("Unexpected gRPC error: %v\n", err)
        }
    } else {
        log.Fatalf("A non gRPC error: %v\n", err)
    }
}
```