DRYを極める:go generateという選択肢
本記事は CyberAgent 26th Fresh Engineer's Advent Calendar 2025 の20日目の記事です。
はじめに
Goは「シンプルさ」を重視する言語だ。ジェネリクスの導入も慎重に進められ、マクロやメタプログラミングの機能は意図的に排除されている。
しかし、クリーンアーキテクチャを採用したプロジェクトでは、この「シンプルさ」が逆に足かせになることがある。
構造体の定義、インターフェースの宣言、コンストラクタ、エラーハンドリングの定型文。本当に書きたいビジネスロジックは、全体の2割にも満たない。
この記事では、go generateを使ってこの問題を解決する方法を紹介する。
なぜDRYが難しいのか
Go言語でDRY(Don't Repeat Yourself)を実現するのは、実は難しい。
他の言語なら、マクロ、デコレータ、アノテーションプロセッサ、リフレクションといった手段がある。しかしGoは、これらの「魔法」を意図的に避けている。
コードは明示的であるべきだ
その結果、クリーンアーキテクチャのような多層構造を採用すると、どうしても似たようなコードを何度も書くことになる。
// handler/order_handler.go
type OrderHandler struct {
uc usecase.OrderUsecase
}
func NewOrderHandler(uc usecase.OrderUsecase) *OrderHandler {
return &OrderHandler{uc: uc}
}
// handler/user_handler.go - ほぼ同じ構造
// handler/product_handler.go - ほぼ同じ構造
// handler/payment_handler.go - ほぼ同じ構造
このパターンは、エンティティの数だけ繰り返される。
go generateという解決策
Goにはgo generateという仕組みがある。ソースコード中の特別なコメントを検出し、指定したコマンドを実行する。
//go:generate stringer -type=Status
この仕組みを使えば、定義を「単一の情報源」として、ボイラープレートを自動生成できる。
では、何を「起点」にすればいいのか?
ここで一つ疑問が湧くかもしれない。「protoファイルからすべて生成すればいいのでは?」と。
実際、gRPCを使うならprotoファイルはすでにある。そこからHandler、Usecase、Repositoryまで全部生成できれば楽だ。しかし、それではクリーンアーキテクチャの本質を見失ってしまう。
なぜ2つの「起点」が必要なのか
レイヤーの責務を理解する
例で考える
「注文を作成する」というユースケースを考えてみる。
// usecase層:何をするか
func (u *orderUsecase) Create(ctx context.Context, userID string, amount int64) (*dto.CreateOrder, error) {
// 問題を分離している
// 「注文を作成する」という問題を、service層に委譲
order, err := u.svc.CreateOrder(ctx, userID, amount)
if err != nil {
return nil, err
}
// 結果をDTOに変換して返す
return &dto.CreateOrder{OrderID: order.ID}, nil
}
// service層:どうするか
func (s *orderService) CreateOrder(ctx context.Context, userID string, amount int64) (*entity.Order, error) {
// 問題を解決している
// 「どうやって注文を作成するか」の具体的なロジック
order := entity.NewOrder(userID, amount)
if err := s.repo.Create(ctx, order); err != nil {
return nil, err
}
return order, nil
}
Proto起点とEntity起点を分ける理由
| 起点 | 責務 | 依存の方向 |
|---|---|---|
| Proto | 外部とのインターフェース(API契約) | 外→内 |
| Entity | ドメインの核(ビジネスルール) | 内→外 |
Protoはあくまで「入り口」だ。 ドメイン層はAPIの形に依存すべきではない。
だからこそ:
- Proto起点 → Handler、Usecase Interface、DTO
- Entity起点 → Repository Interface、Service
この分離により、APIが変わってもドメイン層は影響を受けない。
設計思想:2つの起点
Proto起点(API層)
// proto/order/v1/order.proto
service OrderService {
rpc Create(CreateRequest) returns (CreateResponse);
rpc Get(GetRequest) returns (GetResponse);
}
message CreateRequest {
string user_id = 1;
int64 amount = 2;
}
message CreateResponse {
string order_id = 1;
}
protoファイルは**「APIの契約」**。ここから以下を生成する:
| 生成物 | ファイル | 説明 |
|---|---|---|
| Handler | *_gen.go |
Connect RPCハンドラー(完全自動) |
| Usecase Interface | *_usecase_gen.go |
ユースケースのインターフェース |
| Usecase Scaffold | *_usecase.go |
実装の雛形 |
| DTO | dto/*_gen.go |
レスポンス用データ構造 |
Entity起点(ドメイン層)
// domain/entity/order.go
// +gen:repository Create,GetByID,ListByUserID
type Order struct {
ID string
UserID string
Amount int64
CreatedAt time.Time
}
func NewOrder(userID string, amount int64) *Order {
return &Order{
ID: uuid.New().String(),
UserID: userID,
Amount: amount,
CreatedAt: time.Now(),
}
}
entityは**「ドメインの核」**。アノテーションで必要なリポジトリメソッドを宣言する:
| 生成物 | ファイル | 説明 |
|---|---|---|
| Repository Interface | *_repository_gen.go |
リポジトリのインターフェース |
| Repository Scaffold | *_repository.go |
実装の雛形 |
| Service Scaffold | *_service.go |
ドメインサービスの雛形 |
アノテーションのルール
+gen:repository に指定できるメソッドパターン:
| パターン | 生成されるシグネチャ | 例 |
|---|---|---|
Create |
Create(ctx, e *entity.X) error |
新規作成 |
Update |
Update(ctx, e *entity.X) error |
更新 |
Delete |
Delete(ctx, id string) error |
削除 |
GetByXxx |
GetByXxx(ctx, xxx string) (*entity.X, error) |
単一取得 |
FindByXxx |
FindByXxx(ctx, xxx string) (*entity.X, error) |
検索(単一) |
ListByXxx |
ListByXxx(ctx, xxx string) ([]*entity.X, error) |
検索(複数) |
List |
List(ctx) ([]*entity.X, error) |
全件取得 |
例:Orderエンティティ
// +gen:repository Create,GetByID,ListByUserID
type Order struct { ... }
↓ 以下のインターフェースが生成される
type OrderRepository interface {
Create(ctx context.Context, e *entity.Order) error
GetByID(ctx context.Context, id string) (*entity.Order, error)
ListByUserID(ctx context.Context, userID string) ([]*entity.Order, error)
}
生成されるファイルの2種類
1. 完全自動生成(_gen.go)
// Code generated by layergen. DO NOT EDIT.
- 毎回上書きされる
- 編集禁止
- 定義が変われば自動追従
Handler生成例
// Code generated by layergen. DO NOT EDIT.
package handler
type OrderHandler struct {
uc usecase.OrderUsecase
}
func NewOrderHandler(uc usecase.OrderUsecase) *OrderHandler {
return &OrderHandler{uc: uc}
}
func (h *OrderHandler) Create(
ctx context.Context,
req *connect.Request[pb.CreateRequest],
) (*connect.Response[pb.CreateResponse], error) {
out, err := h.uc.Create(ctx, req.Msg.UserId, req.Msg.Amount)
if err != nil {
return nil, err
}
return connect.NewResponse(&pb.CreateResponse{
OrderId: out.OrderID,
}), nil
}
2. スキャフォールド(編集可能)
// Code scaffolded by layergen. Edit as needed.
- 初回のみ生成
- 新メソッド追加時はメソッド単位で追記
- 既存の実装は保護
Usecase生成例
// Code scaffolded by layergen. Edit as needed.
package usecase
type orderUsecase struct {
svc service.OrderService
}
func NewOrderUsecase(svc service.OrderService) OrderUsecase {
return &orderUsecase{svc: svc}
}
func (u *orderUsecase) Create(ctx context.Context, userID string, amount int64) (*dto.CreateOrder, error) {
// TODO: implement me
return nil, nil
}
Wire(DI)の自動設定
各層のNew関数を自動検出し、Wireの設定ファイルを生成する。
// Code generated by layergen. DO NOT EDIT.
func NewHandlers() (*Handlers, error) {
wire.Build(
persistence.NewOrderRepository,
service.NewOrderService,
usecase.NewOrderUsecase,
handler.NewOrderHandler,
wire.Struct(new(Handlers), "*"),
)
return nil, nil
}
Before / After
Before:手書きの世界
1. proto/order/v1/order.proto を作成
2. buf generate を実行
3. handler/order_handler.go を作成(約50行)
4. usecase/order_usecase.go を作成(約40行)
5. usecase/dto/order.go を作成(約20行)
6. domain/service/order_service.go を作成(約50行)
7. domain/repository/order_repository.go を作成(約20行)
8. infrastructure/persistence/order_repository.go を作成(約60行)
9. di/wire.go を更新(約10行)
合計:約250行のボイラープレート
After:go generateの世界
1. proto/order/v1/order.proto を作成
2. domain/entity/order.go を作成(+gen:repositoryアノテーション付き)
3. make generate を実行
4. 生成されたTODOを実装(約30行)
動かしてみる
1. セットアップ
git clone https://github.com/huavcjj/go-generate-playground.git
cd go-generate-playground
2. ツールのインストール
make setup
実行されるコマンド
go install github.com/bufbuild/buf/cmd/buf@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest
3. コード生成
make generate
実行されるコマンド
# 1. protoからGoコード生成
buf generate
# 2. レイヤーコード生成(内部でgoimportsも実行)
go run ./cmd/layergen/
# 3. DIコード生成
go run github.com/google/wire/cmd/wire@latest ./internal/di/
4. サーバー起動
make run
5. APIテスト
新しいエンティティを追加する手順
Step 1: protoファイルを作成
// proto/product/v1/product.proto
syntax = "proto3";
package product.v1;
option go_package = "github.com/huavcjj/go-generate-playground/gen/product/v1;productv1";
service ProductService {
rpc Create(CreateRequest) returns (CreateResponse);
rpc Get(GetRequest) returns (GetResponse);
}
message CreateRequest {
string name = 1;
int64 price = 2;
}
message CreateResponse {
string product_id = 1;
}
message GetRequest {
string product_id = 1;
}
message GetResponse {
string product_id = 1;
string name = 2;
int64 price = 3;
}
Step 2: entityを作成
// internal/domain/entity/product.go
package entity
// +gen:repository Create,GetByID
type Product struct {
ID string
Name string
Price int64
}
func NewProduct(name string, price int64) *Product {
return &Product{
ID: uuid.New().String(),
Name: name,
Price: price,
}
}
Step 3: 生成を実行
make generate
Step 4: TODOを実装
生成されたスキャフォールドファイルの// TODO: implement meを実装する。
生成結果
Handler(自動生成)
// Code generated by layergen. DO NOT EDIT.
package handler
import (
"context"
"net/http"
"connectrpc.com/connect"
productv1 "github.com/huavcjj/go-generate-playground/gen/product/v1"
"github.com/huavcjj/go-generate-playground/gen/product/v1/productv1connect"
"github.com/huavcjj/go-generate-playground/internal/usecase"
)
type ProductHandler struct {
uc usecase.ProductUsecase
}
func NewProductHandler(uc usecase.ProductUsecase) *ProductHandler {
return &ProductHandler{uc: uc}
}
func (h *ProductHandler) Handler() (string, http.Handler) {
return productv1connect.NewProductServiceHandler(h)
}
func (h *ProductHandler) Create(
ctx context.Context,
req *connect.Request[productv1.CreateRequest],
) (*connect.Response[productv1.CreateResponse], error) {
out, err := h.uc.Create(ctx, req.Msg.Name, req.Msg.Price)
if err != nil {
return nil, err
}
return connect.NewResponse(&productv1.CreateResponse{
ProductId: out.ProductID,
}), nil
}
func (h *ProductHandler) Get(
ctx context.Context,
req *connect.Request[productv1.GetRequest],
) (*connect.Response[productv1.GetResponse], error) {
out, err := h.uc.Get(ctx, req.Msg.ProductId)
if err != nil {
return nil, err
}
return connect.NewResponse(&productv1.GetResponse{
ProductId: out.ProductID,
Name: out.Name,
Price: out.Price,
}), nil
}
var _ productv1connect.ProductServiceHandler = (*ProductHandler)(nil)
Usecase Interface(自動生成)
// Code generated by layergen. DO NOT EDIT.
package usecase
import (
"context"
"github.com/huavcjj/go-generate-playground/internal/usecase/dto"
)
type ProductUsecase interface {
Create(ctx context.Context, name string, price int64) (*dto.CreateProduct, error)
Get(ctx context.Context, productID string) (*dto.GetProduct, error)
}
Usecase 実装(スキャフォールド)
// Code scaffolded by layergen. Edit as needed.
package usecase
import (
"context"
"github.com/huavcjj/go-generate-playground/internal/domain/service"
"github.com/huavcjj/go-generate-playground/internal/usecase/dto"
)
type productUsecase struct {
svc service.ProductService
}
func NewProductUsecase(svc service.ProductService) ProductUsecase {
return &productUsecase{svc: svc}
}
var _ ProductUsecase = (*productUsecase)(nil)
func (u *productUsecase) Create(ctx context.Context, name string, price int64) (*dto.CreateProduct, error) {
// TODO: implement me
return nil, nil
}
func (u *productUsecase) Get(ctx context.Context, productID string) (*dto.GetProduct, error) {
// TODO: implement me
return nil, nil
}
DTO(自動生成)
// Code generated by layergen. DO NOT EDIT.
package dto
type CreateProduct struct {
ProductID string
}
type GetProduct struct {
ProductID string
Name string
Price int64
}
Repository Interface(自動生成)
// Code generated by layergen. DO NOT EDIT.
package repository
import (
"context"
"github.com/huavcjj/go-generate-playground/internal/domain/entity"
)
type ProductRepository interface {
Create(ctx context.Context, e *entity.Product) error
GetByID(ctx context.Context, id string) (*entity.Product, error)
}
Repository 実装(スキャフォールド)
// Code scaffolded by layergen. Edit as needed.
package persistence
import (
"context"
"github.com/huavcjj/go-generate-playground/internal/domain/entity"
"github.com/huavcjj/go-generate-playground/internal/domain/repository"
)
type productRepository struct {
// TODO: add DB connection or other dependencies
}
func NewProductRepository() repository.ProductRepository {
return &productRepository{}
}
var _ repository.ProductRepository = (*productRepository)(nil)
func (r *productRepository) Create(ctx context.Context, e *entity.Product) error {
// TODO: implement me
return nil
}
func (r *productRepository) GetByID(ctx context.Context, id string) (*entity.Product, error) {
// TODO: implement me
return nil, nil
}
Service(スキャフォールド)
// Code scaffolded by layergen. Edit as needed.
package service
import (
"context"
"github.com/huavcjj/go-generate-playground/internal/domain/entity"
"github.com/huavcjj/go-generate-playground/internal/domain/repository"
)
type ProductService interface {
CreateProduct(ctx context.Context, userID string, amount int64) (*entity.Product, error)
GetProduct(ctx context.Context, id string) (*entity.Product, error)
}
type productService struct {
repo repository.ProductRepository
}
func NewProductService(repo repository.ProductRepository) ProductService {
return &productService{repo: repo}
}
var _ ProductService = (*productService)(nil)
func (s *productService) CreateProduct(ctx context.Context, userID string, amount int64) (*entity.Product, error) {
// TODO: implement me
return nil, nil
}
func (s *productService) GetProduct(ctx context.Context, id string) (*entity.Product, error) {
// TODO: implement me
return nil, nil
}
Handlers構造体(自動生成)
// Code generated by layergen. DO NOT EDIT.
package di
import (
"net/http"
"github.com/huavcjj/go-generate-playground/internal/handler"
)
type Handlers struct {
OrderHandler *handler.OrderHandler
ProductHandler *handler.ProductHandler
}
func (h *Handlers) Register(mux *http.ServeMux) {
mux.Handle(h.OrderHandler.Handler())
mux.Handle(h.ProductHandler.Handler())
}
Wire DI設定(自動生成)
//go:build wireinject
// Code generated by layergen. DO NOT EDIT.
package di
import (
"github.com/google/wire"
"github.com/huavcjj/go-generate-playground/internal/domain/service"
"github.com/huavcjj/go-generate-playground/internal/handler"
"github.com/huavcjj/go-generate-playground/internal/infrastructure/persistence"
"github.com/huavcjj/go-generate-playground/internal/usecase"
)
func NewHandlers() (*Handlers, error) {
wire.Build(
persistence.NewOrderRepository,
persistence.NewProductRepository,
service.NewOrderService,
service.NewProductService,
usecase.NewOrderUsecase,
usecase.NewProductUsecase,
handler.NewOrderHandler,
handler.NewProductHandler,
wire.Struct(new(Handlers), "*"),
)
return nil, nil
}
layergenの仕組み
コード生成ツール cmd/layergen/main.go の処理フローを簡単に説明する。
func main() {
// 1. 設定読み込み
cfg := loadConfig("layergen.yaml")
// 2. Proto起点:protoファイルをスキャン
services := scanProtos(cfg.Paths["proto"])
// 3. Entity起点:entityファイルをスキャン
entities := scanEntities(cfg.Paths["entity"])
// 4. Proto起点の生成
for _, svc := range services {
generateDTO(cfg, svc) // DTO生成
generateHandler(cfg, svc) // Handler生成
generateUsecaseInterface(cfg, svc) // Usecase IF生成
generateUsecaseImpl(cfg, svc) // Usecase実装生成
}
// 5. Entity起点の生成
for _, entity := range entities {
generateRepositoryInterface(cfg, entity) // Repository IF生成
generateRepositoryImpl(cfg, entity) // Repository実装生成
generateServiceImpl(cfg, entity) // Service生成
}
// 6. DI設定の生成
generateHandlers(cfg, services) // Handlers構造体
funcsMap := scanNewFuncs(...) // New関数を検出
generateWire(cfg, funcsMap) // wire.go生成
}
プロジェクト構成
.
├── proto/ # Proto定義(API契約)
│ └── order/v1/order.proto
│
├── cmd/
│ ├── layergen/ # コード生成ツール
│ │ ├── main.go
│ │ ├── config.go
│ │ ├── generator.go
│ │ └── scanner.go
│ └── server/
│ └── main.go # エントリポイント
│
├── gen/ # protoc生成コード
│ └── order/v1/
│ ├── order.pb.go # [buf generate]
│ └── orderv1connect/
│ └── order.connect.go # [buf generate]
│
├── internal/
│ ├── di/ # DI設定
│ │ ├── handlers_gen.go # [自動生成]
│ │ ├── wire.go # [自動生成]
│ │ └── wire_gen.go # [Wire生成]
│ │
│ ├── domain/
│ │ ├── entity/
│ │ │ └── order.go # [手書き] +gen:repository
│ │ ├── repository/
│ │ │ └── order_repository_gen.go # [自動生成]
│ │ └── service/
│ │ ├── order_service.go # [スキャフォールド]
│ │ └── provider_gen.go # [自動生成]
│ │
│ ├── handler/
│ │ ├── order_service_gen.go # [自動生成]
│ │ └── provider_gen.go # [自動生成]
│ │
│ ├── infrastructure/
│ │ └── persistence/
│ │ ├── order_repository.go # [スキャフォールド]
│ │ └── provider_gen.go # [自動生成]
│ │
│ └── usecase/
│ ├── dto/
│ │ └── order_gen.go # [自動生成]
│ ├── order_usecase.go # [スキャフォールド]
│ ├── order_usecase_gen.go # [自動生成]
│ └── provider_gen.go # [自動生成]
│
├── layergen.yaml # 生成設定
└── tools.go # ツール依存管理
まとめ
Go言語の「シンプルさ」は美徳だが、クリーンアーキテクチャとの組み合わせではボイラープレートの問題が生じる。
go generateは、この問題に対するGoらしい解決策だ。マクロやリフレクションのような「魔法」を使わず、明示的なコード生成で対処する。
生成されるコードは人間が読める普通のGoコードだ。何が生成されているか、いつでも確認できる。
おわりに
今回紹介したアプローチは、あくまで実験的な試みだ。
実際にチームで導入するには、いくつかの問いに向き合う必要がある:
- どこまで自動生成するか?
- 生成ルールをどう決めるか?
- スキャフォールドと完全自動生成の境界線は?
- チームメンバー全員がコード生成の仕組みを理解できるか?
これらは技術的な問題というより、チームの合意の問題だ。
自動生成は便利だが、やりすぎると「魔法」になってしまう。Goの思想に反して、何が起きているかわからなくなる。
どこまで生成して、どこから手書きにするか。そのバランスは、プロジェクトの規模やチームのスキルセットによって変わる。

Discussion