😁

GoでgRPCサーバーの構築

2022/02/21に公開

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=falsemustEmbedUnimplementedTodoServerというメソッドが自動生成されないように指定しています。

実装

コードが自動生成できたら、実装を書いていきます。

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を使用しました。

https://github.com/google/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
}

※リフレクションを有効にすると、gRPCurlEvansといったツールを使う際にメソッドを呼び出すことができる。

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