GoでRPC APIサーバーを実装する
最近業務でいろんな設計スタイルのAPIを扱った中で、RPCスタイルがいい感じだったのでその話です。
以下のリポジトリでざっくりRPCスタイルのAPIサンプルを書きました。
RPCスタイルのAPIがいい感じ
今業務で結構いろんな設計のAPI触ってて、REST、GraphQL、gRPC、RPC(json)とかを触ってました。
それぞれ触ってみての雑感
REST
リソースをURLで表してそれに対してGET, POSTなどのメソッドを使うスタイル。
GET /users/{userId}
POST /users/{userId}/posts
シンプルで、webサイトくらいだといいが、ガッツリアプリケーションのAPI実装しようとするとメソッドが足りない。
同じuserリソース取得がGETしかないので、idで取得、emailで取得、ステータスで取得とか色々実装しようとするとパラメータ条件が複雑化するのが目に見えてる。
GraphQL
フロントエンドから必要なリソースだけを効率的に取得できる。
スキーマからコードを生成してくれるが、その生成された大量のファイルを管理するのが嫌い。
PR見ずらい。
慣れてないだけかもしれないが、1エンドポイントでリクエストが全部bodyに入るからcurlでサクッとレスポンス確認するのがやりづらい。
最近流行りすぎた?
gRPC
Remote Procedure Call (サーバー上の関数を呼び出す) 方式。
関数名を決めれるのでRESTのメソッド名少ない問題も解決していい感じ。
通信方式は常にHTTP/2のPOSTを使って、データ形式はProtocol Bufferというバイナリ形式になるのでデータサイズが圧縮されて通信効率が良くなる。
GraphQL同様ごちゃごちゃコード生成してくるのがあんまり好きになれない。
あとcurlで動作確認できない。ターミナルから確認したかったらgrpcurlとかのコマンドにわざわざスキーマファイルまで指定してリクエストしないといけない。
これでだいぶ開発体験が下がってる
RPC (json)
RPCスタイルのエンドポイント命名で、普通にjsonでやり取りする方式。
一応JSON-RPCという規格がちゃんと定められてますが、僕の場合はこれには則ってないです。
https://www.jsonrpc.org/
POST /serviceName/v1/useCase/methodのようなエンドポイントで、ユースケース名とメソッド名を指定してjsonでやり取りする感じ。
POST /core/v1/user/getByEmail例えばこんな感じですね。jsonでやり取りする前提なのでget系もPOSTで統一してます。
RESTの、GETだったら安全で冪等、PUTとDELETEは安全ではないが冪等みたいなルール敷ける良さは消えますが、methodの命名である程度カバーできるかなと。
RPC(json)推しポイント
メソッド名を柔軟に決めれるのでRESTの課題を解消している
curlで簡単に動作確認できる
余計なライブラリ無く、JavaScriptのfetch関数で普通にリクエストできる
余計なコード生成不要
特にcurlとfetch普通に使えるのがありがたいですね。
Next.jsとかでAPI呼ぶ時にAPIクライアント周りのコードがブラックボックス化しないのがありがたい。
ただでさえNext.jsだとキャッシュ周りの制御でfetchが拡張されてるので、これ以上色々されると困るんですよね。
Go + RPCのサンプルコード
https://github.com/kanaru-ssk/go-rpc-server
冒頭にも載せたリポジトリです。
単純なtask管理アプリで、データはメモリ保存です。
アーキテクチャはDDDっぽくしてます。
mux.HandleFunc("/core/v1/task/get", taskHandler.HandleGetV1)
mux.HandleFunc("/core/v1/task/list", taskHandler.HandleListV1)
mux.HandleFunc("/core/v1/task/create", taskHandler.HandleCreateV1)
mux.HandleFunc("/core/v1/task/update", taskHandler.HandleUpdateV1)
mux.HandleFunc("/core/v1/task/delete", taskHandler.HandleDeleteV1)ここでAPI一覧が見れて、Handlerのコード追えばなんのエラーでどんなレスポンスかも読みやすいですね。
func (h *TaskHandler) HandleListV1(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if r.Method != http.MethodPost {
slog.WarnContext(ctx, "httphandler.TaskHandler.HandleListV1", "err", errorresponse.ErrMethodNotAllowed)
httpresponse.RenderJson(ctx, w, http.StatusMethodNotAllowed, h.errorMapper.MapErrorResponse(errorresponse.ErrMethodNotAllowed))
return
}
t, err := h.taskUseCase.List(ctx)
switch {
case err == nil:
httpresponse.RenderJson(ctx, w, http.StatusOK, h.taskMapper.MapListResponse(t))
default:
slog.ErrorContext(ctx, "httphandler.TaskHandler.HandleListV1", "err", err)
httpresponse.RenderJson(ctx, w, http.StatusInternalServerError, h.errorMapper.MapErrorResponse(errorresponse.ErrInternalServerError))
}
}type TaskResponse struct {
ID string `json:"id"`
Title string `json:"title"`
Status string `json:"status"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}スキーマ書くかは迷いポイントです。
スキーマ書くよりinterfaceのコードを読みやすくしておく方が、APIの仕様確認するついでに実装追えて楽な気もしてます。
golden fileをapi仕様としておくという手もありますが、レスポンスのjson見ても単純なstringなのかenum的な値なのか、必須なのか必須じゃないのかとかが判断できないのがネックですね。
最後に
ここまで読んで頂きありがとうございました。
ご意見、ご批判などあればコメントしてください。
