0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goで作りながら学ぶ初心者のためのざっくりgRPC入門

Last updated at Posted at 2025-02-18

なにこれ

この記事は、私が改めて学ぶにあたって、実務レベルのアーキテクチャに近い形で

  • データベース
  • リポジトリ
  • サービス
  • 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.gointernal/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の扱いがなんとなくでも伝わって、理解の助けになったらうれしいです。

ここまで読んでくださってありがとうございました!
コメント・感想等、じゃんじゃんお待ちしています!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?