go-grpcからconnect-goに移行する
go-grpcを使っていたプロジェクトを一部connect-goに移行しました。
移行時のTipsをまとめます。
この記事の目的
- go-grpcからconnect-goに移行する。
- なるべく最小限の変更で移行する。
Connect-goとは?
Connect-goは、Bufが開発したGoのモジュールです。
Bufが開発したConnectRPCを使うことで、gRPCとgRPC-Webの互換性を保ちながら、HTTP/1.1の利点を活かしてnet/httpを使ったシンプルな開発ができるようになりました。
移行するメリット
- gRPC、gRPC-Web、Connectプロトコルをサポートしており、旧環境に依存せず移行できる。
- gRPCのみならずHTTP/1.1、HTTP/2、HTTP/3に対応しており、マルチプロトコルをシームレスに利用可能。
- net/httpを基にしたシンプルな開発ができる。
1. go-grpcプロジェクトの準備
1-1. Proto
validate機能付きのシンプルなgreetings APIを作成します。
validateは、protoc-gen-validateを使っているパターンと、protovalidateを使っているパターンがあります。
今回では移行例として両方あったほうがいいので、一緒に使用することにします。
syntax = "proto3";
package greetings.v1;
import "validate/validate.proto";
import "buf/validate/validate.proto";
message GetGreetingsRequest {
string greetings = 1[(validate.rules).string = {
pattern: "^[A-Za-z]+( [A-Za-z]+)*$",
max_bytes: 256,
}];
string name = 2 [ (buf.validate.field).string.min_len = 3 ];
}
message GetGreetingsResponse {
string greetings = 1;
}
service GreetingsService {
rpc GetGreetings (GetGreetingsRequest) returns (GetGreetingsResponse);
}
1-2. Server
localhostの50051ポートでgRPCサーバーを起動します。
lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", 50051))
if err != nil {
log.Error("failed to listen: %v", "error", err)
}
grpcServer := grpc.NewServer(
grpc.ChainUnaryInterceptor(
grpc_validator.UnaryServerInterceptor(),
interceptor.NewUnaryValidationInterceptor(),
))
greetingsv1.RegisterGreetingsServiceServer(grpcServer, &greetings.GreetingsServer{})
grpcServer.Serve(lis)
1-3. Client
localhostの50051ポートにアクセスするgRPCクライアントを作成します。
起動後にGetGreetingsを呼び出します。
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Error("fail to dial: %v", "error", err)
}
defer conn.Close()
client := greetingsv1.NewGreetingsServiceClient(conn)
res, err := client.GetGreetings(
ctx,
&greetingsv1.GetGreetingsRequest{Name: "Jane", Greetings: "Hello"},
)
1-4. Handler
func (s *GreetingsServer) GetGreetings(ctx context.Context, req *greetingsv1.GetGreetingsRequest) (*greetingsv1.GetGreetingsResponse, error) {
if req.Greetings == "" {
return nil, errorInvalidArgument("invalid greetings").Err()
}
if req.Name == "" {
return nil, errorInvalidArgument("invalid name").Err()
}
return &greetingsv1.GetGreetingsResponse{Greetings: fmt.Sprintf("Hello %s", req.Name)}, nil
}
1-5. Error
status、codesを使ってエラーコードとメッセージ付きのデータを作成します。
func error_Unknown(msg string) *status.Status {
return status.New(codes.Unknown, msg)
}
2. connect-goに移行する
2-1. Proto
buf.gen.yamlにgo-grpcのgen設定に加え、protoc-gen-connect-go
を使って、
connect向けにprotoをbuf generateします。
pluginsにprotoc-gen-connect-go
を追加します。
- local: protoc-gen-connect-go
out: ../pkg/gen/proto
opt: paths=source_relative
2-2. Server
Connectサーバーを起動します。
デフォルト設定でgRPC、gRPC-WEB、Connectプロトコルを受付可能にします。
validateInterceptor, err := validate.NewInterceptor()
if err != nil {
log.Error("greetings.server", "error", err)
}
mux := http.NewServeMux()
path, handler := greetingsv1connect.NewGreetingsServiceHandler(&greetings.GreetingsServer{}, connect.WithInterceptors(validateInterceptor, interceptor.NewValidateInterceptor()))
mux.Handle(path, handler)
server := &http.Server{
Addr: "localhost:8080",
Handler: h2c.NewHandler(mux, &http2.Server{}),
}
err = server.ListenAndServe()
if err != nil && err != http.ErrServerClosed {
log.Error("Failed to start server", "error", err)
}
2-3. Client
connectプロトコルで接続します。
client := greetingsv1connect.NewGreetingsServiceClient(
http.DefaultClient,
"http://localhost:8080",
)
res, err := client.GetGreetings(
ctx,
connect.NewRequest(&greetingsv1.GetGreetingsRequest{Name: "Jane", Greetings: "Hello"}),
)
if err != nil {
log.Error("greetings.client", "error", err.Error())
return
}
gRPC、gRPC-Webから接続する
ConnectではgRPC、gRPC-Webの通信も受け付けているので、移行前のgRPCクライアントを今回移行したサーバーにつなげても問題なく通信ができることを確認できます。
移行前のクライアント(gRPC)
conn, err := grpc.NewClient("localhost:8080", grpc.WithTransportCredentials(insecure.NewCredentials()))
gRPC-Web
client := greetingsv1connect.NewGreetingsServiceClient(
http.DefaultClient,
"http://localhost:8080",
connect.WithGRPCWeb(),
)
実行
$ go run cmd/client/main.go
{"time":"***","level":"INFO","msg":"greetings.client","greetings":"Hello Jane"}
2-4. Handler
connect.Request,connect.Responseのジェネリクスになります。
req.Msgで従来のgreetingsv1.GetGreetingsRequestの要素にアクセスできます。
func (s *GreetingsServer) GetGreetings(ctx context.Context, req *connect.Request[greetingsv1.GetGreetingsRequest]) (*connect.Response[greetingsv1.GetGreetingsResponse], error) {
if req.Msg.Greetings == "" {
return nil, errorInvalidArgument("invalid greetings")
}
if req.Msg.Name == "" {
return nil, errorInvalidArgument("invalid name")
}
return connect.NewResponse(&greetingsv1.GetGreetingsResponse{Greetings: fmt.Sprintf("Hello %s", req.Msg.Name)}), nil
}
2-5. Error
connect NewErrorを使ってエラーコードとメッセージ付きのデータを作成します。
func error_Unknown(msg string) error {
return connect.NewError(connect.CodeUnknown, errors.New(msg))
}
おわりに
今回の記事では、go-grpcからconnect-goに移行する手順を記載しました。
改めて見返すと、複雑な工程が必要な変更は少なく、クライアントに関しては移行せずともそのまま使用できることがわかりますね。
connectを使うことで、httpベースのシンプルな実装が書けるため、テストはhttptestを使えばよく、bufconnなども使わなくてよいので、gRPC独自の文化を受け入れる必要がなくなり、より汎用的なサービスを実現できるようになるんじゃないかと思います。
また、移行用に作成したソースの全体像は以下のリポジトリにあります。
記事内ではテストやprotoの生成については省略しているので、完全なパッケージはこちらを参考にしてみてください。
Reference
go-grpcの作成イメージ
connect-goの作成イメージ
go-grpcからconnect-goのマイグレーション
Discussion