kanna

魔法ではない、
生成された Go。

バリデーション、値オブジェクト、データベース、マッピング、メッセージ、依存関係の組み立て、テストデータ。Go のサービスならどこでも繰り返すものを引き受ける、7 つのコードジェネレータです。構造体タグからスキーマまで、すでに書いたものを読み取り、本来手で書くはずの素の Go を生成します。

kanna-validateREADME を読む

user.go自分で書く

//go:generate go tool kanna-validate

//kanna:validate
type CreateUserRequest struct {
	Name string `json:"name" validate:"required,max=50"`
	Age  int    `json:"age"  validate:"gte=18"`
}

validate_gen.gokanna-validate が生成

func (x CreateUserRequest) Validate() error {
	var errs validate.Errors

	if x.Name == "" {
		errs.Add("name", validate.Blank())
	} else {
		if utf8.RuneCountInString(x.Name) > 50 {
			errs.Add("name", validate.TooLong(50))
		}
	}

	if x.Age < 18 {
		errs.Add("age", validate.GreaterThanOrEqualTo(18))
	}

	return errs.Err()
}

kanna-validate は、構造体の validate タグから Validate メソッドを生成し、違反を Rails のエラーコードで、フィールドの JSON 名をキーにして報告します。

README を読む GitHub v0.0.11Go 1.27

go get -tool github.com/go-kanna/validate/cmd/kanna-validate
kanna-valueREADME を読む

model.go自分で書く

//go:generate go tool kanna-value

//kanna:value
type Email struct{ s string }

func (Email) validate(s string) error { return rule.Email(s) }

value_gen.gokanna-value が生成

func NewEmail(v string) (Email, error) {
	if err := (Email{}).validate(v); err != nil {
		return Email{}, err
	}
	return Email{s: v}, nil
}

func (x *Email) UnmarshalJSON(data []byte) error {
	if string(data) == "null" {
		return nil
	}
	var v string
	if err := json.Unmarshal(data, &v); err != nil {
		return err
	}
	y, err := NewEmail(v)
	if err != nil {
		return err
	}
	*x = y
	return nil
}

// MustEmail, String, IsZero, MarshalJSON, Value, and Scan, which checks too.

kanna-value は、値を 1 つ保持する構造体と validate メソッドから値オブジェクトを生成します。コンストラクタ、アクセサ、JSON と SQL のメソッドがそろい、値がどこから来てもルールを守らせます。定数を持つ string 型に //kanna:enum を付ければ、定数の値だけを読み込む enum になります。

README を読む GitHub v0.0.8Go 1.26

go get -tool github.com/go-kanna/value/cmd/kanna-value
kanna-ormREADME を読む

repository/order.go自分で書く

//go:generate go tool kanna-orm -destination ../postgres

//kanna:repository
type OrderRepository interface {
	List(ctx context.Context, s ...scopes.Order) ([]model.Order, error)
	Insert(ctx context.Context, o model.Order) (model.Order, error)
}

postgres/orm_gen.gokanna-orm が生成

func (r OrderRepository) Insert(ctx context.Context, o model.Order) (model.Order, error) {
	const q = `INSERT INTO orders (id, customer_id, status, placed_at, note)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, customer_id, status, placed_at, note`
	var row model.Order
	err := r.db.QueryRow(ctx, q, o.ID, o.CustomerID, o.Status, o.PlacedAt, o.Memo).
		Scan(&row.ID, &row.CustomerID, &row.Status, &row.PlacedAt, &row.Memo)
	if err != nil {
		return model.Order{}, fmt.Errorf("insert into orders: %w", err)
	}
	return row, nil
}

scopes/orm_gen.gokanna-orm が生成

// StatusNe narrows to the rows whose status is not v.
func (s Order) StatusNe(v model.OrderStatus) Order {
	return Order{s.q.Where(`status <> `, v)}
}

// ...a method for each column and relation, and Limit, After, and Cursor.

handler.go自分で呼ぶ

orders, err := repo.List(ctx, scopes.Orders().
	StatusNe(model.OrderCanceled).
	PlacedAtDesc().
	Limit(20))

kanna-orm は、リポジトリのインターフェースの実装を、SQL をリテラルのまま含めて生成します。テーブルごとのスコープ型も生成し、そのメソッドで絞り込み、並べ替え、ページング、リレーションの読み込みができます。

README を読む GitHub v0.0.7Go 1.26

go get -tool github.com/go-kanna/orm/cmd/kanna-orm
kanna-mapperREADME を読む

mapper.go自分で書く

//go:generate go tool kanna-mapper

//kanna:mapper
type _ interface {
	EmployeeToProto(model.Employee) *employeev1.Employee
	EmployeeFromProto(*employeev1.Employee) (model.Employee, error)
}

//kanna:converter
var _ = []any{
	uuid.UUID.String,
	uuid.Parse,
	timestamppb.New,
	(*timestamppb.Timestamp).AsTime,
}

mapper_gen.gokanna-mapper が生成

func EmployeeFromProto(src *employeev1.Employee) (model.Employee, error) {
	if src == nil {
		return model.Employee{}, nil
	}
	v, err := uuid.Parse(src.GetId())
	if err != nil {
		return model.Employee{}, fmt.Errorf("map model.Employee.ID: %w", err)
	}
	return model.Employee{
		ID:       v,
		FullName: src.GetName(),
		HiredAt:  src.GetHiredAt().AsTime(),
		Address:  addressToModel(src.GetAddress()),
	}, nil
}

// EmployeeToProto, and addressToModel and addressToEmployeev1, which they call.

kanna-mapper は、ドメイン型と、自分では管理しないワイヤ型(protobuf などの通信用の型)の間の関数を生成します。シグネチャを宣言すれば、本体をフィールドごとに生成し、protobuf の oneof、enum、直和型も扱います。

README を読む GitHub v0.0.13Go 1.26

go get -tool github.com/go-kanna/mapper/cmd/kanna-mapper
kanna-i18nREADME を読む

locales/自分で書く

# en.yaml
hello: "Hello, {name}!"
items_count:
  plural:
    one: "You have {count} item."
    other: "You have {count} items."

# ja.yaml
hello: "こんにちは、{name}さん!"
items_count:
  plural:
    other: "アイテムが{count}個あります。"

messages/i18n_gen.gokanna-i18n が生成

// Hello returns the message "hello":
//
//	Hello, {name}!
func Hello(name string) i18n.Message {
	return i18n.Message{Key: "hello", Args: []i18n.Arg{
		{Name: "name", Value: name},
	}}
}

// ItemsCount returns the message "items_count", in the form count selects:
//
//	one:   You have {count} item.
//	other: You have {count} items.
func ItemsCount(count int) i18n.Message {
	return i18n.Message{Key: "items_count", Args: []i18n.Arg{
		{Name: "count", Value: count},
	}}
}

handler.go自分で呼ぶ

loc := messages.Localizer(r.Header.Get("Accept-Language"))

// You have 1 item. or アイテムが1個あります。
fmt.Fprintln(w, loc.Localize(messages.ItemsCount(1)))

kanna-i18n は、ロケールファイルのディレクトリをコンパイルし、メッセージごとに型付きのコンストラクタを生成します。翻訳もコードに組み込まれます。CLDR の複数形、ロケールに合わせた数値の書式、Accept-Language の値を受け取る Localizer を備えています。

README を読む GitHub v0.0.10Go 1.26

go get -tool github.com/go-kanna/i18n/cmd/kanna-i18n
kanna-diREADME を読む

server.go自分で書く

//go:generate go tool kanna-di

//kanna:container
type server struct {
	_ user.Repository `di:"with=postgres.NewUserRepository"`

	users *handler.User
}

func openDB(cfg config.Config) (*sql.DB, func(), error)
func newLogger() *slog.Logger

di_gen.gokanna-di が生成

func newServer() (*server, func(), error) {
	cfg, err := config.Load()
	if err != nil {
		return nil, nil, err
	}
	db, cleanup, err := openDB(cfg)
	if err != nil {
		return nil, nil, err
	}
	repo := postgres.NewUserRepository(db)
	log := newLogger()
	registerUser := usecase.NewRegisterUser(repo, log)
	getUser := usecase.NewGetUser(repo)
	users := handler.NewUser(registerUser, getUser)

	return &server{
		users: users,
	}, cleanup, nil
}

kanna-di は、型の隣にある関数から、依存する順に呼び出すコンテナ構造体のコンストラクタを生成します。プロバイダが開いたものは、コンストラクタが返す 1 つのクリーンアップで閉じます。

README を読む GitHub v0.0.7Go 1.26

go get -tool github.com/go-kanna/di/cmd/kanna-di
kanna-fixtureREADME を読む

model.go自分で書く

//go:generate go tool kanna-fixture

type User struct {
	ID        int64
	Name      string `fake:"{firstname} {lastname}"`
	Email     string `validate:"required,email"`
	Age       int    `validate:"gte=18,lte=130"`
	Role      Role
	CreatedAt time.Time
}

fixture/fixture_gen.gokanna-fixture が生成

func User(setters ...func(*model.User)) model.User {
	n := seqUser.Add(1)
	f := gofakeit.NewFaker(rand.NewPCG(uint64(n), 0x5f2c8e0a9d4b7136), false)
	x := model.User{
		ID:        n,
		Name:      mustGenerate(f, "{firstname} {lastname}"),
		Email:     fmt.Sprintf("email-%d@example.com", n),
		Age:       int(18 + (n-1)%113),
		Role:      model.RoleAdmin,
		CreatedAt: time.Date(2000, time.January, 1, 0, 0, int(n), 0, time.UTC),
	}
	for _, set := range setters {
		set(&x)
	}
	return x
}

user_test.go自分で呼ぶ

u := fixture.User(func(u *model.User) { u.Age = 17 })
// Validating u reports age, and nothing else.

kanna-fixture は、パッケージのすべての構造体について、全フィールドが埋まったコンストラクタを生成します。値は衝突しないよう連番にし、validate タグのルールの範囲で埋め、指定すれば gofakeit のテンプレートから作ります。

README を読む GitHub v0.0.10Go 1.26

go get -tool github.com/go-kanna/fixture/cmd/kanna-fixture

それぞれが生成するもの

DO NOT EDIT のヘッダが付いたファイルに書き出される、素の Go の宣言です。各リポジトリのサンプルで生成されるものを、go doc の形式で並べました。

kanna-ormscopes/orm_gen.go, postgres/orm_gen.go

type Order struct{ q orm.Of[model.Order] }
func Orders() Order
func (s Order) StatusEq(v model.OrderStatus) Order
func (s Order) PlacedAtDesc() Order
func (s Order) WithLines(sub ...OrderLine) Order
func (s Order) After(row model.Order) Order
func (s Order) AfterCursor(cursor string) (Order, error)
// ...79 methods on Order in all, for its columns, its relations, and paging

type OrderRepository struct{ db DBTX }
func NewOrderRepository(db DBTX) OrderRepository
func (r OrderRepository) Get(ctx context.Context, id model.OrderID, s ...scopes.Order) (model.Order, error)
func (r OrderRepository) List(ctx context.Context, s ...scopes.Order) ([]model.Order, error)
func (r OrderRepository) Insert(ctx context.Context, o model.Order) (model.Order, error)
// ...one for each method of the interface

kanna-validatevalidate_gen.go

func (x CreateUserRequest) Validate() error
func (x Address) Validate() error

kanna-valuevalue_gen.go

func NewEmail(v string) (Email, error)
func MustEmail(v string) Email
func (x Email) String() string
func (x Email) IsZero() bool
func (x Email) MarshalJSON() ([]byte, error)
func (x *Email) UnmarshalJSON(data []byte) error
func (x Email) Value() (driver.Value, error)
func (x *Email) Scan(src any) error

// and for the enum OrderStatus
func NewOrderStatus(v string) (OrderStatus, error)
func OrderStatusValues() []OrderStatus
func (x *OrderStatus) UnmarshalText(text []byte) error
func (x *OrderStatus) Scan(src any) error

kanna-mappermapper_gen.go

func EmployeeToProto(emp model.Employee) *employeev1.Employee
func EmployeeFromProto(emp *employeev1.Employee) (model.Employee, error)
func addressToEmployeev1(src model.Address) *employeev1.Address
func addressToModel(src *employeev1.Address) model.Address
// ...and the same two for Contact

kanna-i18nmessages/i18n_gen.go

func Hello(name string) i18n.Message
func ItemsCount(count int) i18n.Message
func TotalPrice(price float64) i18n.Message
func UserDeleted(name string) i18n.Message
func Localizer(langs ...string) i18n.Localizer

kanna-didi_gen.go

func newServer(logs io.Writer) (*server, func(), error)
func mustNewServer(logs io.Writer) (*server, func())

kanna-fixturefixture/fixture_gen.go

func Article(setters ...func(*blog.Article)) blog.Article
func Author(setters ...func(*blog.Author)) blog.Author
func SignUp(setters ...func(*blog.SignUp)) blog.SignUp

1 つ覚えれば、7 つとも使える

どのツールも、インストール、実行、CI でのチェックの手順は同じです。設定するのは、実行する 1 行だけです。

  1. モジュールにインストールする

    go get -tool github.com/go-kanna/validate/cmd/kanna-validate

    バージョンは go.mod に記録されるので、プロジェクトの全員が同じものを実行します。

  2. 読み取る対象に印を付ける

    //go:generate go tool kanna-validate
    
    //kanna:validate
    type CreateUserRequest struct {

    対象の構造体、インターフェース、ディレクトリの隣にコメントを書きます。kanna がなくても、コードの読み方は変わりません。

  3. 生成する

    go generate ./...

    各ツールは _gen.go ファイルを書き出し、成功したら何も出力しません。間違いがあれば止まり、ファイルと行、おそらく意図した内容を示します。

  4. CI でチェックする

    KANNA_CHECK=1 go generate ./...

    コミットされたファイルが、ジェネレータが今書き出す内容と違えば失敗します。このとき何も書き出しません。

7 つとも、必要な順に

ツールは他のツールが書いたものを読むので、2 つ以上使うと実行の順序が問題になります。kanna gen は、各ツールが読むものと書くものから順序を求め、//go:generate go tool kanna-* の行を 1 回ずつ実行します。設定は不要で、パッケージの一覧を管理する必要もありません。

コードにある行(go generate が見つける順)あなたが書く

  handler/handler.go:11   //go:generate go tool kanna-di
  mapper/todo.go:5        //go:generate go tool kanna-mapper
  model/todo.go:5         //go:generate go tool kanna-value
  model/todo.go:6         //go:generate go tool kanna-fixture
  usecase/put_todo.go:9   //go:generate go tool kanna-validate
  usecase/put_todo.go:10  //go:generate go tool kanna-di

go tool kanna genkanna が書く

  model/todo.go:5         kanna-value     wrote model/value_gen.go
  model/todo.go:6         kanna-fixture   wrote model/fixture/fixture_gen.go
  usecase/put_todo.go:9   kanna-validate  wrote usecase/validate_gen.go
  usecase/put_todo.go:10  kanna-di        wrote usecase/di_gen.go
  mapper/todo.go:5        kanna-mapper    wrote mapper/mapper_gen.go
  handler/handler.go:11   kanna-di        wrote handler/di_gen.go
6 tasks, 6 files changed
  • 理由を見る

    go tool kanna gen --plan

    実行するものと、その位置にある理由を表示します。たとえば code in usecase calls what kanna-validate generates のように示し、何も実行しません。

  • CI でチェックする

    go tool kanna gen --check

    コミットされたファイルが、生成される内容と違えば失敗します。このとき何も書き出しません。

  • インストールする

    go get -tool github.com/go-kanna/kanna

    実行するのはコードにすでにある行なので、これがなくても各ツールは go generate で今までどおり動きます。README を読む

すべてのジェネレータが守る、Go の 3 つのことわざ

ツールの仕事はそれぞれ違っても、守るルールは同じ 3 つです。

  • Clear is better than clever.

    ジェネレータが書くのは、自分で書くはずのコードです。if 文、関数呼び出し、文字列リテラルの SQL です。デバッガでステップ実行でき、ルールの変更はすべて diff に現れます。

  • Reflection is never clear.

    構造体タグ、ディレクティブ、ロケールファイルは、呼び出しのたびにリフレクションで読むのではなく、go generate のときに 1 度だけ読みます。実行されるのは、生成ファイルに書かれているコードそのものです。

  • A little copying is better than a little dependency.

    生成コードは自分のモジュールの中にある、自分のコードです。kanna-di と kanna-mapper は実行時に何もインポートしません。ランタイムが必要なジェネレータは小さなものを同じモジュールで提供するので、両者がずれることはありません。

Rob Pike の Go Proverbs より。

7 つのツール、1 つのサービス

go-kanna/showcase は、PostgreSQL を使う gRPC の ToDo サービスで、すべてのツールがそれぞれの持ち場で働いています。ジェネレータが生成したものはコミットされているので、各ファイルを生成元と並べて読めます。ここでは木の色で示しています。

showcase/├── config/│   ├── api_config.go│   └── validate_gen.go                 kanna-validate├── internal/│   ├── di/│   │   ├── infra_di.go│   │   └── di_gen.go                   kanna-di│   ├── feature/todo/│   │   ├── model/│   │   │   ├── todo_model.go│   │   │   ├── value_gen.go            kanna-value│   │   │   └── fixture/fixture_gen.go  kanna-fixture│   │   ├── scopes/│   │   │   ├── scopes.go│   │   │   └── orm_gen.go              kanna-orm│   │   ├── repository/│   │   │   └── todo_repository.go│   │   ├── postgres/│   │   │   ├── postgres_todo_repository.go│   │   │   ├── todo_queries.sql│   │   │   └── orm_gen.go              kanna-orm│   │   ├── usecase/│   │   │   ├── *_use_case.go│   │   │   ├── di_gen.go               kanna-di│   │   │   └── validate_gen.go         kanna-validate│   │   ├── mapper/│   │   │   ├── todo_mapper.go│   │   │   └── mapper_gen.go           kanna-mapper│   │   └── handler/│   │       ├── todo_handler.go│   │       └── di_gen.go               kanna-di│   └── messages/│       ├── locales/│       └── i18n_gen.go                 kanna-i18n└── kanna-orm.snapshot.json
  • kanna-diインフラを 1 度だけ組み立て、そこからハンドラを作り、ユースケースに渡すリポジトリを選びます
  • kanna-validateユースケースへの入力と、環境変数から読んだ設定をチェックします
  • kanna-valueToDo の完了日時をどこでも 2000 年から 2100 年の間に保ち、優先度を宣言した 3 つのいずれかに保ちます
  • kanna-ormユースケースが依存するリポジトリと、scopes.Todos().CreatedAtAsc() のように読み取りに使うスコープを実装します
  • kanna-mapperToDo を protobuf メッセージに、その逆に PutTodo の入力にマッピングします。優先度は名前で対応させ、oneof である ListTodo のフィルタは直和型にします
  • kanna-fixtureテストの出発点となる ToDo を作ります
  • kanna-i18nサービスのメッセージとバリデーションエラーの文言を、英語と日本語でコンパイルします
  • kanna genmake gen の裏で全部を実行し、コミットされた内容を CI でチェックします

GitHub でコードを読む

すべてのツール

それぞれが独立したモジュールなので、必要なものだけをインストールできます。各ページの内容は、そのツールの README です。

  • 構造体の validate タグから Validate メソッドを生成し、違反を Rails のエラーコードで、フィールドの JSON 名をキーにして報告します。リクエストをデコードし、問題をまとめて一度に報告します。

    go get -tool github.com/go-kanna/validate/cmd/kanna-validate

    v0.0.11 Go 1.27

  • 値を 1 つ保持する構造体と validate メソッドから値オブジェクトを生成します。コンストラクタ、アクセサ、JSON と SQL のメソッドがそろい、値がどこから来てもルールを守らせます。独自の型なのでメソッドも追加でき、kanna-validate の email、url、uuid タグも同じルールを呼び出します。定数を持つ string 型は、JSON、テキスト、データベースから定数の値だけを読み込む enum になります。

    go get -tool github.com/go-kanna/value/cmd/kanna-value

    v0.0.8 Go 1.26

  • リポジトリのインターフェースの実装を、SQL をリテラルのまま含めて生成します。テーブルごとのスコープ型も生成し、そのメソッドで絞り込み、並べ替え、ページング、リレーションの読み込みができます。マイグレーション済みのデータベースのスナップショットからスキーマを読むので、一致しないカラムや型があれば生成が止まります。PostgreSQL(pgx または database/sql 経由)と MySQL 8.4 以降に対応します。

    go get -tool github.com/go-kanna/orm/cmd/kanna-orm

    v0.0.7 Go 1.26

  • ドメイン型と、自分では管理しないワイヤ型の間の関数を生成します。シグネチャを宣言すれば、本体をフィールドごとに生成し、uuid.Parse や timestamppb.New のように並べた変換関数をそのまま呼び出し、protobuf の oneof も扱います。kanna-value の enum は定数名で protobuf の enum にマッピングされ、ドメインの直和型は oneof にマッピングされます。//kanna:mapper strict を付けると、明示的に指定したフィールドだけを埋めます。

    go get -tool github.com/go-kanna/mapper/cmd/kanna-mapper

    v0.0.13 Go 1.26

  • ロケールファイルのディレクトリをコンパイルし、メッセージごとに型付きのコンストラクタを生成します。翻訳そのものもコードに組み込むので、実行時に読み込みも解析もしません。CLDR の複数形、ロケールに合わせた数値の書式、Accept-Language の値を受け取る Localizer を備えています。

    go get -tool github.com/go-kanna/i18n/cmd/kanna-i18n

    v0.0.10 Go 1.26

  • 型の隣やコンテナの隣にある関数から、コンテナ構造体のコンストラクタを生成します。登録は不要で、インターフェースはタグ 1 つで実装にバインドでき、クリーンアップは 1 つにまとめて返され、実行時にインポートするものはありません。

    go get -tool github.com/go-kanna/di/cmd/kanna-di

    v0.0.7 Go 1.26

  • パッケージのすべての構造体について、全フィールドが埋まったコンストラクタを生成します。値は衝突しないよう連番にし、validate タグのルールの範囲で埋めるか、gofakeit のテンプレートから作ります。テンプレートは生成時にチェックされます。

    go get -tool github.com/go-kanna/fixture/cmd/kanna-fixture

    v0.0.10 Go 1.26

  • モジュールのジェネレータを、各ツールが読むものと書くものから求めた順序で実行します。設定は増えません。実行するのは、すでにある //go:generate の行です。順序の理由の表示と、CI でのチェックもできます。

    go get -tool github.com/go-kanna/kanna

    v0.3.0 Go 1.26

なぜ鉋なのか

鉋(kanna)は、板の上を手前に引いて削る日本の道具です。良い鉋は向こうが透けるほど薄い削り屑を出し、それ以上手を加える必要のない面を残します。

ジェネレータに求める仕事も同じです。自分で書くはずのコードだけを、読めるほど薄く削り出し、プログラムの他の部分には手を付けません。削り屑はこのロゴになり、その木の色は、このサイトでジェネレータが書いたものすべての色になっています。