魔法ではない、
生成された Go。
バリデーション、値オブジェクト、データベース、マッピング、メッセージ、依存関係の組み立て、テストデータ。Go のサービスならどこでも繰り返すものを引き受ける、7 つのコードジェネレータです。構造体タグからスキーマまで、すでに書いたものを読み取り、本来手で書くはずの素の Go を生成します。
それぞれが独立したモジュールです。1 つだけでも、showcase のように 7 つ全部でも使えます。全部使うときは kanna gen が順に実行します。
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 名をキーにして報告します。
タグの書き方は go-playground/validator と同じです。ただし呼び出しのたびにリフレクションで読むのではなく、go generate のときに 1 度だけ読みます。
go get -tool github.com/go-kanna/validate/cmd/kanna-validate
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 になります。
値オブジェクトごとに書くはずのコンストラクタと 4 つのマーシャリングメソッドを、ルールを 1 箇所にまとめて生成します。各レイヤで繰り返すはずの enum の定数チェックも不要になります。
go get -tool github.com/go-kanna/value/cmd/kanna-value
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 をリテラルのまま含めて生成します。テーブルごとのスコープ型も生成し、そのメソッドで絞り込み、並べ替え、ページング、リレーションの読み込みができます。
スコープは gorm のようにつなげられ、クエリは sqlc のようにスキーマと照合され、生成コードにリフレクションはありません。PostgreSQL(pgx または database/sql 経由)と MySQL 8.4 以降に対応します。
go get -tool github.com/go-kanna/orm/cmd/kanna-orm
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、直和型も扱います。
宣言のしかたは goverter に近いものです。Go が自力で変換できないものは、変換する関数を uuid.Parse や timestamppb.New のまま並べます。
go get -tool github.com/go-kanna/mapper/cmd/kanna-mapper
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 を備えています。
ロケールファイルは go-i18n と同様ですが、メッセージは実行時に ID で引くものではなく、コンパイラがチェックする関数です。
go get -tool github.com/go-kanna/i18n/cmd/kanna-i18n
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 つのクリーンアップで閉じます。
Wire と同じくコンパイル前に生成しますが、管理するプロバイダセットはありません。依存は型の隣にある関数から得られ、インターフェースはタグ 1 つでバインドします。
go get -tool github.com/go-kanna/di/cmd/kanna-di
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 のテンプレートから作ります。
どのテストパッケージでも増えていく newTestUser ヘルパーを、すべての構造体について生成し、構造体の変更にも追従します。フィクスチャは作った時点で有効なので、1 つのルールのテストでは 1 つのフィールドを変えるだけです。
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 行だけです。
-
モジュールにインストールする
go get -tool github.com/go-kanna/validate/cmd/kanna-validateバージョンは
go.modに記録されるので、プロジェクトの全員が同じものを実行します。 -
読み取る対象に印を付ける
//go:generate go tool kanna-validate //kanna:validate type CreateUserRequest struct {対象の構造体、インターフェース、ディレクトリの隣にコメントを書きます。kanna がなくても、コードの読み方は変わりません。
-
生成する
go generate ./...各ツールは
_gen.goファイルを書き出し、成功したら何も出力しません。間違いがあれば止まり、ファイルと行、おそらく意図した内容を示します。 -
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 gen
make genの裏で全部を実行し、コミットされた内容を CI でチェックします
すべてのツール
それぞれが独立したモジュールなので、必要なものだけをインストールできます。各ページの内容は、そのツールの README です。
-
構造体の
validateタグからValidateメソッドを生成し、違反を Rails のエラーコードで、フィールドの JSON 名をキーにして報告します。リクエストをデコードし、問題をまとめて一度に報告します。go get -tool github.com/go-kanna/validate/cmd/kanna-validatev0.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-valuev0.0.8 Go 1.26
-
リポジトリのインターフェースの実装を、SQL をリテラルのまま含めて生成します。テーブルごとのスコープ型も生成し、そのメソッドで絞り込み、並べ替え、ページング、リレーションの読み込みができます。マイグレーション済みのデータベースのスナップショットからスキーマを読むので、一致しないカラムや型があれば生成が止まります。PostgreSQL(pgx または database/sql 経由)と MySQL 8.4 以降に対応します。
go get -tool github.com/go-kanna/orm/cmd/kanna-ormv0.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-mapperv0.0.13 Go 1.26
-
ロケールファイルのディレクトリをコンパイルし、メッセージごとに型付きのコンストラクタを生成します。翻訳そのものもコードに組み込むので、実行時に読み込みも解析もしません。CLDR の複数形、ロケールに合わせた数値の書式、Accept-Language の値を受け取る
Localizerを備えています。go get -tool github.com/go-kanna/i18n/cmd/kanna-i18nv0.0.10 Go 1.26
-
型の隣やコンテナの隣にある関数から、コンテナ構造体のコンストラクタを生成します。登録は不要で、インターフェースはタグ 1 つで実装にバインドでき、クリーンアップは 1 つにまとめて返され、実行時にインポートするものはありません。
go get -tool github.com/go-kanna/di/cmd/kanna-div0.0.7 Go 1.26
-
パッケージのすべての構造体について、全フィールドが埋まったコンストラクタを生成します。値は衝突しないよう連番にし、
validateタグのルールの範囲で埋めるか、gofakeit のテンプレートから作ります。テンプレートは生成時にチェックされます。go get -tool github.com/go-kanna/fixture/cmd/kanna-fixturev0.0.10 Go 1.26
-
モジュールのジェネレータを、各ツールが読むものと書くものから求めた順序で実行します。設定は増えません。実行するのは、すでにある
//go:generateの行です。順序の理由の表示と、CI でのチェックもできます。go get -tool github.com/go-kanna/kannav0.3.0 Go 1.26
なぜ鉋なのか
鉋(kanna)は、板の上を手前に引いて削る日本の道具です。良い鉋は向こうが透けるほど薄い削り屑を出し、それ以上手を加える必要のない面を残します。
ジェネレータに求める仕事も同じです。自分で書くはずのコードだけを、読めるほど薄く削り出し、プログラムの他の部分には手を付けません。削り屑はこのロゴになり、その木の色は、このサイトでジェネレータが書いたものすべての色になっています。