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

# なにこれ
この記事は、私が改めて学ぶにあたって、実務レベルのアーキテクチャに近い形で
- データベース
- リポジトリ
- サービス
- APIサーバー
を実践的に作成しながら、Go言語でgRPCにざっくりと入門できるように学んだ際の備忘録的記事です。

実際に作るものは以下のようなアーキテクチャになっており、DBに保存したユーザーデータをリポジトリ層が取得し、サービス層でビジネスロジックを適用して整形、gRPCサーバが外部クライアントからのリクエストに応じてレスポンスを返すものになっています。
```mermaid
flowchart LR
    Client["Client (grpcurl / Web Client)"]
    subgraph API_Server["gRPC サーバ (user_server.go)"]
      A["GetUser"]
    end
    subgraph Service["サービス層 (user_service.go)"]
      B["GetUser(ctx, id)"]
    end
    subgraph Repository["リポジトリ層 (user_repository.go)"]
      C["GetUserByID(ctx, id)"]
    end
    subgraph DB["データベース (MySQL)"]
      D["(SQL クエリ)"]
    end

    Client -->|リクエスト| A
    A -->|呼び出し: GetUser| B
    B -->|呼び出し: GetUserByID| C
    C -->|SQL クエリ発行| D
    D -->|結果返却| C
    C -->|ユーザーモデル返却| B
    B -->|レスポンス生成| A
    A -->|レスポンス返却| Client

```

なお、今回ざっくりと理解するということで、具体的なコードの解説はあまり多くなく、コメントアウトで対応しています。

もし、ここでつまった！ここがわかりにくい！等があれば、気軽にコメントください！

サンプルコードはこちら！　
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 として保存してください。
```yaml
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`を作成し、以下の内容を記述します。
```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のコードを生成します。
```bash
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`を作成し、ユーザー情報のモデルを定義します。
```go
package model

// User はユーザー情報を表すモデル。
type User struct {
	ID    int64
	Name  string
	Email string
}
```

## Repository層の実装
`internal/repository/user_repository.go`を作成し、MySQLからユーザー情報を取得する処理を実装します。
```go
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`を作成し、リポジトリ層を利用してビジネスロジックを実装します。
```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を実装します。
```go
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 サーバの起動を行います。
```go
// 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の扱いがなんとなくでも伝わって、理解の助けになったらうれしいです。

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