gRPCを試してみたく、個人開発でGoとgRPCを使って簡単なメモサービスのCRUDを実装したので振り返ってみます。
作ったもの
sqliteでid、title、descriptionレコードを持つmemosテーブルを作成してgRPCのリクエストからメモの一覧取得、詳細取得、作成、更新、削除ができるだけの簡単なサービスです。
フロントエンドもなしでCLIでのみ使えます。
この後別テーブル作ってもう少し作り込むかもしれませんが現時点ではこれのみ。
プロジェクトのレポジトリ
gRPCの教材
以下の記事がまとまっていていい教材になりました。 2023年現在だとgRPCを深く扱った書籍はなさそうです(オライリーから英語で出版されてるっぽいけど未翻訳)
正直Restでの開発経験があるエンジニアであれば技術書に頼らなくてもネット上の情報でキャッチアップできるんじゃないかな。と思ってます。
gRPCでの開発フロー
- protoファイルを作成する
- protoファイルを元に必要なコードを自動生成する
- 自動生成されたコードを使ってサーバでリクエストを受けた後の処理を実装していく
protoファイルの作成と自動コード生成
任意のフォルダ(今回の場合はprotoフォルダ)に以下ようなprotofileを作成します。今回はmemoservice.proto
という命名のファイルとして保存しています。
これはGoに限らずその言語でもgRPCプロジェクトであれば共通の形式。大体みたまんまでCRUDのリクエスト、レスポンスのインターフェイスを定義してます。
syntax = "proto3"; package service; option go_package = "pkg/grpc"; service MemoAPI { rpc GetMemo(GetMemoRequest) returns (GetMemoResponse) {} rpc ListMemos(ListMemosRequest) returns (ListMemosResponse) {} rpc CreateMemo(CreateMemoRequest) returns (CreateMemoResponse) {} rpc UpdateMemo(UpdateMemoRequest) returns (UpdateMemoResponse) {} rpc DeleteMemo(DeleteMemoRequest) returns (DeleteMemoResponse) {} } message Memo { string id = 1; string title = 2; string description = 3; } message GetMemoRequest { string id = 1; } message GetMemoResponse { Memo memo = 1; } message ListMemosRequest {} message ListMemosResponse { repeated Memo memos = 1; } message CreateMemoRequest { Memo memo = 1; } message CreateMemoResponse { bool success = 1; string id = 2; } message UpdateMemoRequest { Memo memo = 1; } message UpdateMemoResponse { bool success = 1; string id = 2; } message DeleteMemoRequest { string id = 1; } message DeleteMemoResponse { string id = 1; }
自動コード生成するためにprotobufコマンドを取得します。macであればhomebrewで取得できます。
brew install protobuf
その後protoファイルが保存されているパスに移動して次のようなコマンドを実行すると自動でコードが生成されます。
cd proto protoc --go_out=../pkg/grpc --go_opt=paths=source_relative memoservice.proto --go-grpc_out=../pkg/grpc --go-grpc_opt=paths=source_relative memoservice.proto
自動生成されたコードを使ったサーバの実装
とりあえずgRPCでのリクエスト受信~DB接続を試したかっただけなのでフォルダ分割せずにmain.goファイルに全部実装してます。 エラーハンドリングも適当。
package main import ( "context" "database/sql" "fmt" "log" "net" "os" "os/signal" _ "github.com/mattn/go-sqlite3" "google.golang.org/grpc" "google.golang.org/grpc/reflection" memopb "grpc-memoapp/pkg/grpc" ) type memoServer struct { memopb.UnimplementedMemoAPIServer } func NewMemoServer() *memoServer { return &memoServer{} } func (s *memoServer) GetMemo(ctx context.Context, req *memopb.GetMemoRequest) (*memopb.GetMemoResponse, error) { db, err := sql.Open("sqlite3", "./grpc_memoapp.db") if err != nil { log.Fatal(err) } defer db.Close() id := req.Id row := db.QueryRow("SELECT title, description FROM memos WHERE id = ?", id) memo := &memopb.Memo{} err = row.Scan(&memo.Title, &memo.Description) if err != nil { if err == sql.ErrNoRows { log.Println("Memo not found") return nil, err } log.Println("Error fetching memo") return nil, err } response := &memopb.GetMemoResponse{ Memo: memo, } return response, nil } func (s *memoServer) ListMemos(ctx context.Context, req *memopb.ListMemosRequest) (*memopb.ListMemosResponse, error) { db, err := sql.Open("sqlite3", "./grpc_memoapp.db") if err != nil { log.Fatal(err) } defer db.Close() sqlStmt := ` SELECT * FROM memos; ` rows, err := db.Query(sqlStmt) if err != nil { log.Println(err) } defer rows.Close() var grpcMemos []*memopb.Memo for rows.Next() { var id int var title, description string err := rows.Scan(&id, &title, &description) if err != nil { log.Println(err) return nil, err } grpcMemo := &memopb.Memo{ Title: title, Description: description, } grpcMemos = append(grpcMemos, grpcMemo) } response := &memopb.ListMemosResponse{ Memos: grpcMemos, } log.Println("success ListMemos") return response, nil } func (s *memoServer) CreateMemo(ctx context.Context, req *memopb.CreateMemoRequest) (*memopb.CreateMemoResponse, error) { db, err := sql.Open("sqlite3", "./grpc_memoapp.db") if err != nil { log.Fatal(err) } defer db.Close() tx, err := db.Begin() if err != nil { log.Fatal(err) } insertSqlStmt := "INSERT INTO memos(title, description) VALUES(?, ?);" _, err = db.Exec(insertSqlStmt, req.Memo.Title, req.Memo.Description) if err != nil { log.Println(err) } err = tx.Commit() if err != nil { log.Println(err) } var lastInsertID string // SELECT last_insert_rowid()で最後のIDが取得できないのでこの実装。今回はgRPCの実験なので深追いしない err = db.QueryRow("SELECT id FROM memos ORDER BY id DESC LIMIT 1").Scan(&lastInsertID) if err != nil { log.Println(err) } response := &memopb.CreateMemoResponse{ Success: true, Id: lastInsertID, } log.Println("success CreateMemo") return response, nil } func (s *memoServer) UpdateMemo(ctx context.Context, req *memopb.UpdateMemoRequest) (*memopb.UpdateMemoResponse, error) { db, err := sql.Open("sqlite3", "./grpc_memoapp.db") if err != nil { log.Fatal(err) } defer db.Close() tx, err := db.Begin() if err != nil { log.Fatal(err) } updateSqlStmt := "UPDATE memos SET title = ?, description = ? where id = ?;" _, err = db.Exec(updateSqlStmt, req.Memo.Title, req.Memo.Description, req.Memo.Id) if err != nil { log.Println(err) } err = tx.Commit() if err != nil { log.Println(err) } response := &memopb.UpdateMemoResponse{ Success: true, Id: req.Memo.Id, } log.Println("success UpdateMemo") return response, nil } func (s *memoServer) DeleteMemo(ctx context.Context, req *memopb.DeleteMemoRequest) (*memopb.DeleteMemoResponse, error) { db, err := sql.Open("sqlite3", "./grpc_memoapp.db") if err != nil { log.Fatal(err) } defer db.Close() id := req.Id _, err = db.Exec("DELETE FROM memos WHERE id = ?", id) if err != nil { log.Fatal(err) } response := &memopb.DeleteMemoResponse{ Id: id, } return response, nil } func main() { port := 8080 listener, err := net.Listen("tcp", fmt.Sprintf(":%d", port)) if err != nil { panic(err) } s := grpc.NewServer() memopb.RegisterMemoAPIServer(s, NewMemoServer()) reflection.Register(s) go func() { log.Printf("start gRPC server port: %v", port) s.Serve(listener) }() quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt) <-quit log.Println("stopping gRPC server...") s.GracefulStop() }
動作検証
protodファイルからgRPCでクライアントコードを実装するためのコードも生成されているのでそれを使ってクライアント側を実装して上記のコードにリクエストしてもいいんですがgrpcurlコマンドを使うとREST APIにおけるcurlと同じように動作検証できます。
brew install grpcurl
などでコマンドをinstallしたのちに以下のようにリクエストをシェルから送信できます。
grpcurl -plaintext -d '{"id": "1"}' localhost:3333 service.MemoAPI.GetMemo grpcurl -plaintext localhost:3333 service.MemoAPI.ListMemos grpcurl -plaintext -d '{ "memo": { "title": "titleDemo", "description": "descriptionDemo" } }' localhost:3333 service.MemoAPI.CreateMemo grpcurl -plaintext -d '{ "memo": { "id": "1", "title": "updateDemo", "description": "updateDemo" } }' localhost:3333 service.MemoAPI.UpdateMemo grpcurl -plaintext -d '{"id": "1"}' localhost:3333 service.MemoAPI.DeleteMemo
終わり
次はクライアント側のコードも書きたい。