この記事は虎の穴ラボ Advent Calendar 2024の14日目の記事です。
こんにちは、虎の穴ラボFantiaエンジニアのNSYです。
今回はconnect-goによるAPIの開発に入門してみました。
connectとは、gRPC(Google Remote Procedure Call)の構築をシンプルに行うことができるGo言語のライブラリです。 これを利用することでサーバーとクライアントを効率的に開発することができます。この記事では加えてGo言語のコード生成ができるライブラリを組み合わせ、TodoのCRUDを行うAPIを構築してみました。 connectrpc.com
目次
完成後の構成
. ├── backend │ ├── Dockerfile.dev │ ├── buf.gen.yaml │ ├── buf.yaml │ ├── cmd │ │ └── server │ │ └── main.go │ ├── db │ │ ├── migrations │ │ │ ├── 000001_create_todos_table.down.sql │ │ │ └── 000001_create_todos_table.up.sql │ │ └── models │ │ ├── boil_queries.go │ │ ├── boil_table_names.go │ │ ├── boil_types.go │ │ ├── boil_view_names.go │ │ ├── psql_upsert.go │ │ └── todos.go │ ├── gen │ │ └── todo │ │ └── v1 │ │ ├── todo.pb.go │ │ └── todov1connect │ │ └── todo.connect.go │ ├── go.mod │ ├── go.sum │ ├── proto │ │ └── todo │ │ └── v1 │ │ └── todo.proto │ ├── service │ │ └── todo │ │ └── v1 │ │ └── todo.go │ └── sqlboiler.toml └── docker-compose.yml
APIの構築
Dockerで環境構築
まずはDockerを用いて環境の構築をします。設定は以下の通りで、GoとDB用のコンテナを定義しています。
また、Dockerfileでは必要なライブラリのインストールを行っています。
ここから先の作業はGoコンテナ中で進めていきますのでコンテナをビルドして起動します。
# docker-compose.yml services: backend: build: context: ./backend dockerfile: Dockerfile.dev volumes: - ./backend:/app tty: true environment: - DB_USER=user - DB_PASSWORD=postgres - DB_HOST=db - DB_PORT=5432 - DB_NAME=connect ports: - "8080:8080" db: image: postgres:13.18 environment: POSTGRES_USER: user POSTGRES_PASSWORD: postgres POSTGRES_DB: connect volumes: - db-data:/var/lib/postgresql/data ports: - "5432:5432" volumes: db-data:
# Dockerfile.dev FROM golang:1.23 RUN go install github.com/bufbuild/buf/cmd/buf@latest RUN go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest RUN go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest RUN go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@v4.18.1 RUN go install github.com/volatiletech/sqlboiler/v4@latest RUN go install github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-psql@latest
Goの初期化
まずは、go mod init backend
でGoのプロジェクトの初期化を行います。ここではコンテナ名とそろえてbackend
にしたいと思います。
DBマイグレーションの生成
PostgreSQLを使用して、APIで利用するテーブルを作成します。 マイグレーションにはgolang-migrate
を使用します。
データ保存をするTodosテーブルの定義ファイルを、migrate create -ext sql -dir db/migrations -seq create_todos_table
で生成します。
backend/db/migrations
以下に生成されたファイルを元に、マイグレーションを行うSQLを作成します。
最後にmigrate -database postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=disable -path db/migrations up
でマイグレーションを実行します。
-- db/migrations/000001_create_todos_table.up.sql CREATE TABLE IF NOT EXISTS todos ( id SERIAL PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, completed BOOLEAN NOT NULL DEFAULT FALSE );
-- db/migrations/000001_create_todos_table.down.sql DROP TABLE IF EXISTS todos;
ORMによるコード生成
GoのORM/クエリビルダにはgormやsqlx、goquなどがあります。
この記事ではDB上に定義されたテーブルを元にして、ORMを生成することができるsqlboilerを利用したいと思います。
DBへの接続情報をsqlboiler.toml
に設定します。その後sqlboiler psql
を実行します。backend/db/models以下にファイルが生成されているはずです。
# sqlboiler.toml pkgname="models" output="db/models" wipe=true add-global-variants=false no-tests=true [psql] dbname = "connect" host = "db" port = 5432 user = "user" pass = "postgres" sslmode = "disable" schema = "public" blacklist = ["schema_migrations"]
※could not find sqlboiler version in go.mod
のエラーが表示される場合は、go get -u github.com/volatiletech/sqlboiler/v4
を実行後に試してみてください。
protoの定義
次はprotoファイルにAPIの関数や引数・戻り値の定義を行います。 この記事ではCRUDを行うため、以下の関数を定義しています。
- CreateTodo: Todoの作成.
- ListTodo: Todoの一覧取得.
- UpdateTodo: Todoの更新.
- DeleteTodo: Todoの削除.
// todo.proto syntax = "proto3"; package proto.todo.v1; option go_package = "backend/gen/todo/v1;todov1"; message Todo { int32 id = 1; string title = 2; string description = 3; bool completed = 4; } message TodoList { repeated Todo todos = 1; } message CreateTodoRequest { string title = 1; string description = 2; } message CreateTodoResponse { bool success = 1; } message ListTodoRequest {} message ListTodoResponse { repeated Todo todos = 1; } message UpdateTodoRequest { int32 id = 1; string title = 2; string description = 3; bool completed = 4; } message UpdateTodoResponse { bool success = 1; } message DeleteTodoRequest { int32 id = 1; } message DeleteTodoResponse { bool success = 1; } service TodoService { rpc CreateTodo (CreateTodoRequest) returns (CreateTodoResponse); rpc ListTodo (ListTodoRequest) returns (ListTodoResponse); rpc UpdateTodo (UpdateTodoRequest) returns (UpdateTodoResponse); rpc DeleteTodo (DeleteTodoRequest) returns (DeleteTodoResponse); }
connectによるコード生成
それではprotoの定義を元に、connectによるgRPCの生成を行っていきます。
はじめにbuf config init
で初期化を行います。これによりbuf.yaml
が生成されるので、modulesの項目に設定を追加してください。
次にbuf.gen.yaml
を作成して、buf generate
を実行してください。backend/gen以下にファイルが生成されているはずです。
# buf.yaml version: v2 lint: use: - STANDARD breaking: use: - FILE modules: - path: proto
# buf.gen.yaml version: v2 plugins: - local: protoc-gen-go out: gen opt: paths=source_relative - local: protoc-gen-connect-go out: gen opt: paths=source_relative
APIの実装
ここまでに作成してきたコードを元に、APIサーバーの実装を行います。
はじめにconnectで生成された、backend/gen/todo/v1/todov1connect/todo.connect.go
に定義されているTodoServiceHandlerを確認してください。このインタフェースにはprotoで定義された4つの関数が定義されています。サーバーの開発ではこの生成された関数を実装していくことになります。
まずはNewTodoServiceHandlerインタフェースの実装を行います。
// service/todo/v1/todo.go package servicev1todo import ( "backend/db/models" v1 "backend/gen/todo/v1" "backend/gen/todo/v1/todov1connect" "context" "database/sql" "log" "net/http" "connectrpc.com/connect" "github.com/volatiletech/null/v8" "github.com/volatiletech/sqlboiler/v4/boil" "github.com/volatiletech/sqlboiler/v4/queries/qm" ) type ServiceV1Todo struct { exec *sql.DB } func NewServiceV1Todo(db *sql.DB) (string, http.Handler) { srv := &ServiceV1Todo{exec: db} path, handler := todov1connect.NewTodoServiceHandler(srv) return path, handler } func (srv *ServiceV1Todo)ListTodo(ctx context.Context, req *connect.Request[v1.ListTodoRequest]) (*connect.Response[v1.ListTodoResponse], error){ todoModels, err := models.Todos(qm.Select()).All(ctx, srv.exec) if err != nil { log.Println(err) return nil, err } todos := make([]*v1.Todo, len(todoModels)) for index, todoModel := range todoModels { todos[index] = &v1.Todo{ Id: int32(todoModel.ID), Title: todoModel.Title, Description: todoModel.Description.String, Completed: todoModel.Completed, } } res := connect.NewResponse(&v1.ListTodoResponse{Todos: todos}) res.Header().Set("Todo-Version", "1") return res, nil } func (srv *ServiceV1Todo)CreateTodo(ctx context.Context, req *connect.Request[v1.CreateTodoRequest]) (*connect.Response[v1.CreateTodoResponse], error){ todo := &models.Todo{ Title: req.Msg.Title, Description: null.StringFrom(req.Msg.Description), Completed: false, } if err := todo.Insert(ctx, srv.exec, boil.Infer()); err != nil { log.Println(err) return nil, err } res := connect.NewResponse(&v1.CreateTodoResponse{Success: true}) res.Header().Set("Todo-Version", "1") return res, nil } func (srv *ServiceV1Todo)UpdateTodo(ctx context.Context, req *connect.Request[v1.UpdateTodoRequest]) (*connect.Response[v1.UpdateTodoResponse], error){ todo, err := models.Todos(qm.Select(),models.TodoWhere.ID.EQ(int(req.Msg.Id))).One(ctx, srv.exec) if err != nil { log.Println(err) return nil, err } todo.Title = req.Msg.Title todo.Description = null.StringFrom(req.Msg.Description) todo.Completed = req.Msg.Completed if _,err := todo.Update(ctx, srv.exec, boil.Infer()); err != nil { log.Println(err) return nil, err } res := connect.NewResponse(&v1.UpdateTodoResponse{Success: true}) res.Header().Set("Todo-Version", "1") return res, nil } func (srv *ServiceV1Todo)DeleteTodo(ctx context.Context, req *connect.Request[v1.DeleteTodoRequest]) (*connect.Response[v1.DeleteTodoResponse], error){ todo, err := models.Todos(qm.Select(),models.TodoWhere.ID.EQ(int(req.Msg.Id))).One(ctx, srv.exec) if err != nil { log.Println(err) return nil, err } if _,err := todo.Delete(ctx, srv.exec); err != nil { log.Println(err) return nil, err } res := connect.NewResponse(&v1.DeleteTodoResponse{Success: true}) res.Header().Set("Todo-Version", "1") return res, nil }
次にAPIサーバー本体のコードを実装します。
grpcreflectはGraphQLのイントロスペクションと同様にサーバーに定義されたAPIの仕様を取得するための設定になります。
これを利用することで、開発時にPostmanなどが補完を行ってくれるようになります。
// cmd/server/main.go package main import ( servicev1todo "backend/service/todo/v1" "fmt" "os" "database/sql" "log" "net/http" connctcors "connectrpc.com/cors" "connectrpc.com/grpcreflect" _ "github.com/lib/pq" "github.com/rs/cors" "github.com/volatiletech/sqlboiler/v4/boil" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" ) func withCORS(h http.Handler) http.Handler { middleware := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, AllowedMethods: connctcors.AllowedMethods(), AllowedHeaders: connctcors.AllowedHeaders(), ExposedHeaders: connctcors.ExposedHeaders(), }) return middleware.Handler(h) } func main() { mux := http.NewServeMux() db, err := sql.Open( "postgres", fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", os.Getenv("DB_HOST"), os.Getenv("DB_PORT"), os.Getenv("DB_USER"), os.Getenv("DB_PASSWORD"), os.Getenv("DB_NAME"), ), ) if err != nil { log.Fatal(err) } defer db.Close() path, handler := servicev1todo.NewServiceV1Todo(db) mux.Handle(path, handler) reflector := grpcreflect.NewStaticReflector( "proto.todo.v1.TodoService", ) mux.Handle(grpcreflect.NewHandlerV1(reflector)) mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector)) log.Println("Listening on :8080") http.ListenAndServe(":8080", withCORS(h2c.NewHandler(mux, &http2.Server{}))) }
Postmanから実行する
はじめにgo run ./cmd/server/main.go
でサーバーを起動してください。
次にPostmanなどのクライアントツールを立ち上げてください。ここでは例としてPostmanで説明を行います。
通信方法をgRPCに設定してください。その後にService definitionタブをからUsing server reflection.を実行してください。 これで定義されたAPIを取得できているはずです。
次にSelect a methodから選択ボックスを開き実行したい関数を選択してください。選択後に、Messageタブを開きUse Example Messageを選択してください。 試しに実行してみると、想定どおりのレスポンスが返り無事に実装できていることが確認できます。
所感とまとめ
この記事ではコードを生成してくれるライブラリを活用しながら、connectを使ったAPI開発に入門してみました。 実際に使用してみて雛形やコードを生成してくれるのは効率的だと感じました。
今回は基本的なUnary RPC(単方向リモートプロシージャコール)という通信方式だけを利用しました。この方式は、従来のAPIと同じようにリクエストとレスポンスの一対一の通信を行うものです。そのため、gRPCに慣れていなくても比較的簡単に実装することができました。しかし、gRPCの強みは単一通信だけでなく、双方向通信やストリーミングなど、様々な通信方法を選択できる点にあります。これにより複雑で高度な通信を実現することが可能です。
今後は、その他の通信方式を試したり、connect-webを使ってクライアントと通信してみたいと思います。
Fantia開発採用情報
虎の穴ラボでは現在、一緒にFantiaを開発していく仲間を積極募集中です!
多くのユーザーに使っていただけるtoCサービスの開発をやってみたい方は、ぜひ弊社の採用情報をご覧ください。
toranoana-lab.co.jp