Zenn
🌊

go-grpcからconnect-goに移行する

2025/02/17に公開

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の生成については省略しているので、完全なパッケージはこちらを参考にしてみてください。

https://github.com/baleen-dyamaguchi/go-grpc-to-connect

Reference

https://connectrpc.com/docs/introduction/

go-grpcの作成イメージ
https://grpc.io/docs/languages/go/basics/

connect-goの作成イメージ
https://connectrpc.com/docs/go/getting-started

go-grpcからconnect-goのマイグレーション
https://connectrpc.com/docs/go/grpc-compatibility/?ref=awarefy.dev#migration

株式会社BALEEN STUDIO

Discussion

ログインするとコメントできます