なにこれ
この記事は、私が改めて学ぶにあたって、実務レベルのアーキテクチャに近い形で
- データベース
- リポジトリ
- サービス
- APIサーバー
を実践的に作成しながら、Go言語でgRPCにざっくりと入門できるように学んだ際の備忘録的記事です。
実際に作るものは以下のようなアーキテクチャになっており、DBに保存したユーザーデータをリポジトリ層が取得し、サービス層でビジネスロジックを適用して整形、gRPCサーバが外部クライアントからのリクエストに応じてレスポンスを返すものになっています。
なお、今回ざっくりと理解するということで、具体的なコードの解説はあまり多くなく、コメントアウトで対応しています。
もし、ここでつまった!ここがわかりにくい!等があれば、気軽にコメントください!
サンプルコードはこちら!
https://github.com/haruotsu/go-grpc-tutorial
なぜgRPCが便利か
細かいことは世にある書籍や文書がいい感じに記載してくれてるので、そちらを参照してもらうとして、今回はざっくりと行きます。
- スキーマ駆動設計
- APIの仕様がProtocol Buffersで明確に定義され、クライアントとサーバの仕様が統一される
- 自動生成されたコードにより、型安全な実装が保証される
- 高効率な通信
- バイナリフォーマットを利用することで、テキストベースのプロトコルに比べて高速・低遅延な通信が実現
- 省リソースでのデータ転送が可能
- 多言語間の互換性
- スキーマに基づいて各言語用のクライアントやサーバコードを自動生成でき、フロントエンドやバックエンドで異なる言語でも統一したインターフェースで通信可能
- マイクロサービス、テストとの相性
- スキーマによりサービス間の厳格な制約により、システム全体の信頼性と保守性が向上
開発とテストの効率化 - スキーマにより、モックやスタブの作成が容易になり、テストがしやすい
- APIの変更がスキーマを更新するだけで済むため、メンテナンスがシンプルになる
- スキーマによりサービス間の厳格な制約により、システム全体の信頼性と保守性が向上
必要なツールのインストール
以下はHomebrewを利用できる環境を想定していますが、各自の環境でやりやすいもので導入していただければ問題ありません。
goのインストール
brew install go
Protocol Buffersコンパイラ (protoc)のインストール
今回protocol bufferでスキーマを定義するため、そのコンパイラを用意します。
brew install protobuf
インストール後以下でバージョン確認ができればOK
protoc --version
grpcurlのインストール
gRPCサーバーへのリクエストテストツールとしてgrpcurlをインストールします。
brew install grpcurl
MySQLクラインアントのインストール
MySQLの接続確認などに利用できるクライアントもインストールしておくと便利です。
brew install mysql-client
ディレクトリ構成
以下のようにディレクトリを作成してください。
grpc-test/
├── cmd/
│ └── server/ # サーバ起動用 main.go
├── internal/
│ ├── pb/ # proto ファイルと生成コード
│ ├── model/ # データモデル定義
│ ├── repository/ # DB アクセス層 (database/sql)
│ └── service/ # ビジネスロジック層
├── docker-compose.yml
├── go.mod # 後ほどgo mod initで作成
└── go.sum # 後ほどgo getで作成
MySQLコンテナの準備と初期データ投入
今回はMySQLを用いてDBを作成します。
docker-comnpose.yml
以下の内容を docker-compose.yml として保存してください。
version: "3.3"
services:
mysql:
image: mysql:8.0
container_name: db-for-go
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --sql-mode=ONLY_FULL_GROUP_BY,NO_ENGINE_SUBSTITUTION
environment:
MYSQL_ROOT_USER: root # MySQL のルートユーザー名 (本来は.envとかで管理すべき)
MYSQL_ROOT_PASSWORD: root_pass # MySQL のルートパスワード (本来は.envとかで管理すべき)
MYSQL_DATABASE: test-db # 作成する初期データベース名 (本来は.envとかで管理すべき)
MYSQL_USER: hoge # 通常ユーザー名 (本来は.envとかで管理すべき)
MYSQL_PASSWORD: hoge_pass # 通常ユーザーパスワード (本来は.envとかで管理すべき)
TZ: "Asia/Tokyo"
ports:
- "3306:3306"
volumes:
- db-volume:/var/lib/mysql
volumes:
db-volume:
コンテナの起動
docker-compose up
テーブル作成と初期データ投入
docker exec -it db-for-go mysql -u hoge -p # パスワードはhoge_pass
その後、データを投入するために以下のSQLクエリを実行
USE test-db;
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL UNIQUE
);
INSERT INTO users (name, email) VALUES
('haruotsu', 'haruotsu@example.com'),
('paruotsu', 'paruotsu@example.com');
Goプロジェクトの作成
初期化
go mod init github.com/<your-github-name>/grpc-test
必要パッケージのインストール
# gRPC と Protocol Buffers 関連
go get google.golang.org/grpc
go get google.golang.org/protobuf
# gRPC 用のコード生成プラグイン(最新バージョン)
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# MySQL ドライバ
go get github.com/go-sql-driver/mysql
gRPC APIの定義とコード生成
ここが本番であり、メインです。gRPCの定義はprotoファイルに記載します。
protoファイルの作成
internal/pb/user.proto
を作成し、以下の内容を記述します。
syntax = "proto3";
package pb;
option go_package = "internal/pb";
// ユーザー情報の定義
message User {
int64 id = 1;
string name = 2;
string email = 3;
}
// ユーザー取得リクエスト
message GetUserRequest {
int64 id = 1;
}
// ユーザー取得レスポンス
message GetUserResponse {
User user = 1;
}
// gRPC サービス定義
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse);
}
この実装をもとに、gRPCのprotoファイルのスキーマの書き方は以下のようにまとめることができます。
- メッセージ定義
- message User { ... } のように、データ構造を定義します。各フィールドには一意な番号(1,2,3, ...)が振られており、これによりバイナリ形式でシリアライズする際の順序が固定。
- サービス定義
- service UserService { ... } の部分では、gRPCのサービス(=API)を定義しています。
- サービス内に定義された各RPCメソッド(この例では GetUser)は、入力として GetUserRequest を受け取り、出力として GetUserResponse を返すというように、どのようなリクエストがどのレスポンスを返すのかを示す。
protoコードの生成
以下のコマンドを実行して、先ほど定義したprotoスキーマをもとに、コンパイルをしてGoのコードを生成します。
protoc --go_out=. --go-grpc_out=. internal/pb/user.proto
これにより、internal/pb/user.pb.go
とinternal/pb/user_grpc.pb.go
が生成されており、自動的に構造体やインタフェースが生成されており、型安全な通信が担保されることが確認できるかと思います。
モデルの定義とRepository層の実装
モデル定義
internal/model/user.go
を作成し、ユーザー情報のモデルを定義します。
package model
// User はユーザー情報を表すモデル。
type User struct {
ID int64
Name string
Email string
}
Repository層の実装
internal/repository/user_repository.go
を作成し、MySQLからユーザー情報を取得する処理を実装します。
package repository
import (
"context"
"database/sql"
"errors"
"github.com/haruotsu/grpc-test/internal/model"
)
// UserRepository はユーザー情報取得のためのインターフェースです。
type UserRepository interface {
GetUserByID(ctx context.Context, id int64) (*model.User, error)
}
type userRepository struct {
db *sql.DB
}
// NewUserRepository は新しいリポジトリインスタンスを生成します。
func NewUserRepository(db *sql.DB) UserRepository {
return &userRepository{db: db}
}
// GetUserByID は SQL クエリを利用して、指定されたIDのユーザー情報を取得します。
func (r *userRepository) GetUserByID(ctx context.Context, id int64) (*model.User, error) {
query := `SELECT id, name, email FROM users WHERE id = ?`
row := r.db.QueryRowContext(ctx, query, id)
var user model.User
if err := row.Scan(&user.ID, &user.Name, &user.Email); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return nil, err
}
return &user, nil
}
Service層の実装
internal/service/user_service.go
を作成し、リポジトリ層を利用してビジネスロジックを実装します。
package service
import (
"context"
"github.com/haruotsu/grpc-test/internal/model"
"github.com/haruotsu/grpc-test/internal/repository"
)
// UserServiceは、ユーザーに関するビジネスロジックを提供するインターフェース。
type UserService interface {
GetUser(ctx context.Context, id int64) (*model.User, error)
}
// userServiceはUserServiceインターフェースの実装です。
type userService struct {
repo repository.UserRepository
}
// NewUserServiceは新しいUserService を生成します。
func NewUserService(repo repository.UserRepository) UserService {
return &userService{repo: repo}
}
// GetUserは、repository層を利用してユーザー情報を取得します。
func (s *userService) GetUser(ctx context.Context, id int64) (*model.User, error) {
return s.repo.GetUserByID(ctx, id)
}
gRPCサーバーの実装と起動
ここまできたら大詰めです。あとはこれまで作ってきたものをサーバーでつなげて起動すればよいです。
サーバーの実装
internal/server/user_server.go
を作成し、protoで定義したgRPC APIを実装します。
package server
import (
"context"
"errors"
"github.com/haruotsu/grpc-test/internal/pb"
"github.com/haruotsu/grpc-test/internal/service"
)
// UserServer は pb.UserServiceServer インターフェースの実装です。
type UserServer struct {
pb.UnimplementedUserServiceServer
userService service.UserService
}
// NewUserServer は新しい UserServer を生成します。
func NewUserServer(us service.UserService) *UserServer {
return &UserServer{userService: us}
}
// GetUser は gRPC の GetUser エンドポイントを実装します。
func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
user, err := s.userService.GetUser(ctx, req.Id)
if err != nil {
return nil, err
}
if user == nil {
return nil, errors.New("user not found")
}
return &pb.GetUserResponse{
User: &pb.User{
Id: user.ID,
Name: user.Name,
Email: user.Email,
},
}, nil
}
サーバー起動用mainの実装
cmd/server/main.go
を作成し、MySQLへの接続、依存性注入、gRPC サーバの起動を行います。
// cmd/server/main.go
package main
import (
"database/sql"
"fmt"
"log"
"net"
"github.com/haruotsu/grpc-test/internal/pb"
"github.com/haruotsu/grpc-test/internal/repository"
"github.com/haruotsu/grpc-test/internal/server"
"github.com/haruotsu/grpc-test/internal/service"
_ "github.com/go-sql-driver/mysql" // MySQL ドライバ
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
)
func main() {
// DSN の形式: <username>:<password>@tcp(<host>:<port>)/<dbname>?charset=utf8mb4&parseTime=True&loc=Local
dsn := "hoge:hoge_pass@tcp(localhost:3306)/test-db?charset=utf8mb4&parseTime=True&loc=Local"
// MySQL への接続
db, err := sql.Open("mysql", dsn)
if err != nil {
log.Fatalf("failed to open database: %v", err)
}
if err := db.Ping(); err != nil {
log.Fatalf("failed to ping database: %v", err)
}
// Repository と Service の初期化
userRepo := repository.NewUserRepository(db)
userSvc := service.NewUserService(userRepo)
// gRPC サーバの初期化とサービス登録
grpcServer := grpc.NewServer()
userServer := server.NewUserServer(userSvc)
pb.RegisterUserServiceServer(grpcServer, userServer)
reflection.Register(grpcServer)
// サーバリスンの設定
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
fmt.Println("gRPC server listening on :50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
動作確認
MySQLコンテナを起動した状態でgRPCサーバーも起動します。
go run cmd/server/main.go
その後、grpcurl を利用して、ユーザー ID 1 の情報を取得できれば完成です🎉
grpcurl -plaintext -d '{"id":1}' localhost:50051 pb.UserService/GetUser
おわりに
本当にコードをぺたぺた貼って動かすだけでしたが、この記事を通してGoでのgRPCの扱いがなんとなくでも伝わって、理解の助けになったらうれしいです。
ここまで読んでくださってありがとうございました!
コメント・感想等、じゃんじゃんお待ちしています!