📘

DRYを極める:go generateという選択肢

に公開

本記事は CyberAgent 26th Fresh Engineer's Advent Calendar 2025 の20日目の記事です。
https://qiita.com/advent-calendar/2025/ca-26th

はじめに

Goは「シンプルさ」を重視する言語だ。ジェネリクスの導入も慎重に進められ、マクロやメタプログラミングの機能は意図的に排除されている。

しかし、クリーンアーキテクチャを採用したプロジェクトでは、この「シンプルさ」が逆に足かせになることがある。

構造体の定義、インターフェースの宣言、コンストラクタ、エラーハンドリングの定型文。本当に書きたいビジネスロジックは、全体の2割にも満たない。

この記事では、go generateを使ってこの問題を解決する方法を紹介する。
https://github.com/huavcjj/go-generate-playground.git


なぜ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