😁
GoでgRPCサーバーの構築
gPRCサーバーの構築を実装した備忘録となります。
今回はTodoリストのGet
メソッドとCreate
メソッドを実行できるようにしていきます。
DBはMySQLを使用します。
protoファイルの作成
まずはprotoファイルを作成していきます。
proto/todo.proto
syntax = "proto3";
package service;
option go_package = "./pb";
service TodoQuery {
rpc Get (TodoGetRuest) returns (TodoGetResponse);
}
message TodoGetRuest {
string id = 1;
};
message TodoGetResponse {
Todo item = 1;
message Todo {
string id = 1;
string title = 2;
string body = 3;
}
};
service TodoCommand {
rpc Create (TodoCreateRuest) returns (TodoCreateResponse);
}
message TodoCreateRuest {
string title = 1;
string body = 2;
};
message TodoCreateResponse {
string id = 1;
};
protoc
コマンドでProtocol Buffersのコードを自動生成します。
protoc --proto_path=proto/. \
--go-grpc_opt require_unimplemented_servers=false,paths=source_relative \
--go-grpc_out proto/pb/ \
--go_opt paths=source_relative \
--go_out proto/pb/ proto/*.proto
require_unimplemented_servers=false
でmustEmbedUnimplementedTodoServer
というメソッドが自動生成されないように指定しています。
実装
コードが自動生成できたら、実装を書いていきます。
handler/todo.go
package handler
import (
"context"
"time"
"grpc-golang/adapter"
"grpc-golang/domain"
"grpc-golang/proto/pb"
)
func NewTodoQuery(dbFactory adapter.DB, todoRepo adapter.TodoRepository) pb.TodoQueryServer {
return &todoQuery{
dbFactory: dbFactory,
todoRepo: todoRepo,
}
}
type todoQuery struct {
dbFactory adapter.DB
todoRepo adapter.TodoRepository
}
func (q *todoQuery) Get(ctx context.Context, req *pb.TodoGetRuest) (*pb.TodoGetResponse, error) {
db := q.dbFactory(ctx)
todo, err := q.todoRepo.Get(ctx, db, domain.TodoId(req.Id))
if err != nil {
return nil, err
}
return &pb.TodoGetResponse{
Item: TodoFrom(todo),
}, nil
}
func NewTodoCommand(dbFactory adapter.DB, todoRepo adapter.TodoRepository) pb.TodoCommandServer {
return &todoCommand{
dbFactory: dbFactory,
todoRepo: todoRepo,
}
}
type todoCommand struct {
dbFactory adapter.DB
todoRepo adapter.TodoRepository
}
func (c *todoCommand) Create(ctx context.Context, req *pb.TodoCreateRuest) (*pb.TodoCreateResponse, error) {
db := c.dbFactory(ctx)
item := domain.NewTodo(req.Title, req.Body, time.Now())
if err := c.todoRepo.Insert(ctx, db, item); err != nil {
return nil, err
}
return &pb.TodoCreateResponse{
Id: item.Id.String(),
}, nil
}
インフラ層の実装をしていきます。
infra/persistence/entity.go
package todo_table
import (
"grpc-golang/domain"
"time"
)
func (e *Entity) TableName() string {
return "todos"
}
type Entity struct {
Id string `gorm:"column:id;primary_key"`
Title string `gorm:"column:title"`
Body string `gorm:"column:body"`
CreatedAt time.Time `gorm:"column:created_at"`
UpdatedAt time.Time `gorm:"column:updated_at"`
}
func entityFrom(d *domain.Todo) *Entity {
return &Entity{
Id: d.Id.String(),
Title: d.Title,
Body: d.Body,
CreatedAt: d.CreatedAt,
UpdatedAt: d.UpdatedAt,
}
}
func (e *Entity) ToDomain() *domain.Todo {
return &domain.Todo{
Id: domain.TodoId(e.Id),
Title: e.Title,
Body: e.Body,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
}
}
infra/persistence/repository.go
package todo_table
import (
"context"
"grpc-golang/adapter"
"grpc-golang/domain"
"gorm.io/gorm"
)
func NewRepository() adapter.TodoRepository {
return &repository{}
}
type repository struct {}
func (r *repository) Get(ctx context.Context, db *gorm.DB, id domain.TodoId) (*domain.Todo, error) {
var e Entity
if err := db.Where("id = ?", id.String()).First(&e).Error; err != nil {
return nil, err
}
return e.ToDomain(), nil
}
func (r *repository) Insert(ctx context.Context, db *gorm.DB, item *domain.Todo) error {
if err := db.Create(entityFrom(item)).Error; err != nil {
return err
}
return nil
}
DI
DIツールにwire
を使用しました。
di/provider.go
// +build wireinject
package di
import (
"github.com/google/wire"
"google.golang.org/grpc"
"grpc-golang/handler"
)
var providerSet = wire.NewSet(
handler.NewTodoQuery,
handler.NewTodoCommand,
provideRawDB,
provideDB,
todo_table.NewRepository,
handler.NewAPIHandler,
)
func provideRawDB() *sql.DB {
url := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true",
os.Getenv("MYSQL_USER"),
os.Getenv("MYSQL_PASS"),
os.Getenv("MYSQL_HOST"),
os.Getenv("MYSQL_PORT"),
os.Getenv("MYSQL_DATABASE"),
)
return persistence.NewStandardRawDB(url)
}
func provideDB(rawDB *sql.DB) adapter.DB {
maxOpenConns, _ := strconv.ParseInt(os.Getenv("DB_MAX_OPEN_CONNS"), 10, 64)
maxIdleConns, _ := strconv.ParseInt(os.Getenv("DB_MAX_IDLE_CONNS"), 10, 64)
connMaxLifetimeSec, _ := strconv.ParseInt(os.Getenv("DB_CONN_MAX_LIFETIME_SECOND"), 10, 64)
conf := adapter.DBConfig{
MaxOpenConns: int(maxOpenConns),
MaxIdleConns: int(maxIdleConns),
ConnMaxLifetime: time.Duration(connMaxLifetimeSec) * time.Second,
}
return persistence.NewDB(rawDB, conf)
}
func ResolveAPIHandler() *grpc.Server {
wire.Build(providerSet)
return nil
}
handler/server.go
package handler
import (
"github.com/mrmts/grpc-golang/proto/pb"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
func NewAPIHandler(
todoQueryServer pb.TodoQueryServer,
todoCommandServer pb.TodoCommandServer,
) *grpc.Server {
server := grpc.NewServer()
pb.RegisterTodoQueryServer(server, todoQueryServer)
pb.RegisterTodoCommandServer(server, todoCommandServer)
// リフレクションを有効化(gRPCサーバがどのようなサービス、メソッドを公開しているかを知るための機能)
reflection.Register(server)
return server
}
※リフレクションを有効にすると、gRPCurl
やEvans
といったツールを使う際にメソッドを呼び出すことができる。
wire
コマンドを実行してDI用の関数を自動生成します。
cd di
wire
main.go
package main
import (
"log"
"net"
"os"
"grpc-golang/di"
)
func main() {
grpcServer := di.ResolveAPIHandler()
grpcPort := "3000"
log.Printf("listening grpc on port %s", grpcPort)
li, err := net.Listen("tcp", ":"+grpcPort)
if err != nil {
log.Fatalf("failed to open grpc listener: %+v", err)
}
if err := grpcServer.Serve(li); err != nil {
log.Println("unexpected end of process")
os.Exit(1)
}
}
docker-composeでサーバーを起動させようと思います。
今回はMySQLを使用しました。
docker-compose.yml
version: "3.8"
services:
db:
build: ./db
ports:
- "3306:3306"
volumes:
- ./db/initdb.d:/docker-entrypoint-initdb.d
- ./db/data:/var/lib/mysql
environment:
MYSQL_DATABASE: todo
MYSQL_USER: todo
MYSQL_PASSWORD: todo
MYSQL_ROOT_PASSWORD: password
MYSQL_PORT: 3306
app:
build:
context: .
dockerfile: Dockerfile
target: local-dev
ports:
- "3000:3000"
volumes:
- .:/app
- .mod:/go/pkg/mod
working_dir: /app
command:
- "sh"
- "-c"
- "make gen && go run /app/main.go"
動作確認
docker-compose up
立ち上がったら動作確認していきます。
今回はgrpcui
を使用しました。
grpcui -plaintext -port 4000 localhost:3000
Discussion