Generated Go,
not magic Go.
Seven code generators for what every Go service repeats: validation, value types, the database, mapping, messages, wiring, and test data. Each reads what you have already written, from struct tags to your schema, and writes the plain Go you would otherwise write by hand.
Each is a module of its own. Use one, or all seven together, as the showcase does, with kanna gen to run them in order.
user.goyou write
//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 writes
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 writes a Validate method from the validate tags of a struct, and reports what breaks under Rails' error codes, keyed by the fields' JSON names.
The tags are written as for go-playground/validator. They are read once, by go generate, instead of by reflection on every call.
go get -tool github.com/go-kanna/validate/cmd/kanna-validate
model.goyou write
//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 writes
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 turns a struct holding one value and a validate method into a value type: constructors, an accessor, and JSON and SQL methods that hold every value to the rule, wherever it comes from. A string type with constants, marked //kanna:enum, becomes an enum that reads in only those.
The constructor and the four marshaling methods you would otherwise write for every value object, with the rule in one place, and the check of an enum's constants that every layer would repeat.
go get -tool github.com/go-kanna/value/cmd/kanna-value
repository/order.goyou write
//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 writes
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 writes
// 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.goyou call
orders, err := repo.List(ctx, scopes.Orders().
StatusNe(model.OrderCanceled).
PlacedAtDesc().
Limit(20))
kanna-orm writes the implementation of your repository interfaces, with the SQL in it as literals, and a scope type for each table whose methods narrow, order, page, and load relations.
Scopes that chain as gorm's do, queries checked against your schema as sqlc checks them, and no reflection in what it writes. On PostgreSQL through pgx or database/sql, and MySQL 8.4 or later.
go get -tool github.com/go-kanna/orm/cmd/kanna-orm
mapper.goyou write
//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 writes
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 writes the functions between your domain types and the wire types you do not control. You declare the signatures; it writes the bodies, field by field, and handles protobuf oneofs, enums, and sum types.
Declared much as with goverter. What Go cannot convert by itself, you list as the functions that do, uuid.Parse and timestamppb.New as they are.
go get -tool github.com/go-kanna/mapper/cmd/kanna-mapper
locales/you write
# 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 writes
// 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.goyou call
loc := messages.Localizer(r.Header.Get("Accept-Language"))
// You have 1 item. or アイテムが1個あります。
fmt.Fprintln(w, loc.Localize(messages.ItemsCount(1)))
kanna-i18n compiles a directory of locale files into a typed constructor for every message, the translations compiled in: CLDR plurals, locale-formatted numbers, and a Localizer that takes an Accept-Language value.
Locale files as with go-i18n, but a message is a function the compiler checks, not an ID looked up at run time.
go get -tool github.com/go-kanna/i18n/cmd/kanna-i18n
server.goyou write
//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 writes
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 writes the constructor of a container struct from the functions next to your types, in the order they need each other. What a provider opens, it closes, through the one cleanup the constructor returns.
Generated before compiling, as with Wire, but with no provider sets to keep: a dependency comes from the function next to its type, and an interface is bound with one tag.
go get -tool github.com/go-kanna/di/cmd/kanna-di
model.goyou write
//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 writes
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.goyou call
u := fixture.User(func(u *model.User) { u.Age = 17 })
// Validating u reports age, and nothing else.
kanna-fixture writes a constructor for every struct of a package, every field filled: numbered so that no two collide, within the rules its validate tags declare, or from a gofakeit template where you ask for one.
The newTestUser helpers every test package grows, written for every struct and kept in step with it. A fixture is valid as made, so a test of one rule changes one field.
go get -tool github.com/go-kanna/fixture/cmd/kanna-fixture
What each one writes
Plain Go declarations, in files headed DO NOT EDIT. Here is what the example in each repository gets, listed as go doc lists it.
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
Learn one, and you know all seven
Every tool is installed, run, and checked in CI the same way. Nothing is set up but the line that runs it.
-
Install it into your module
go get -tool github.com/go-kanna/validate/cmd/kanna-validateThe version is in your
go.mod, so everyone on the project runs the same one. -
Mark what it reads
//go:generate go tool kanna-validate //kanna:validate type CreateUserRequest struct {A comment next to the struct, the interface, or the directory it is about. Without kanna, your code reads the same.
-
Generate
go generate ./...Each tool writes its
_gen.gofiles and prints nothing when it succeeds. A mistake stops it instead, at the file and line, with what you probably meant. -
Check in CI
KANNA_CHECK=1 go generate ./...Fails when a committed file is not what its generator would write now, and writes nothing.
All seven, in the order they need
Tools read what other tools write, so with more than one, which runs first matters. kanna gen works that out from what each one reads and writes, and runs every //go:generate go tool kanna-* line once. There is nothing to configure, and no list of packages to keep.
The lines in your code, as go generate finds themyou write
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 writes
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
-
See why
go tool kanna gen --planPrints what would run and why it comes where it does, such as
code in usecase calls what kanna-validate generates, and runs nothing. -
Check in CI
go tool kanna gen --checkFails when a committed file is not what would be generated, and writes nothing.
-
Install it
go get -tool github.com/go-kanna/kannaIt runs the lines already in your code, so every tool goes on working under
go generatewithout it. Read the README
Three Go proverbs every generator keeps
The tools do different jobs, and hold to the same three rules.
-
Clear is better than clever.
What a generator writes is the code you would have written:
ifstatements, function calls, and SQL in string literals. Step through it in a debugger, and see every change of a rule in a diff. -
Reflection is never clear.
Struct tags, directives, and locale files are read once, by go generate, not by reflection on every call. What runs is what the generated file shows.
-
A little copying is better than a little dependency.
The generated code is yours, in your module. kanna-di and kanna-mapper import nothing at run time; a generator that needs a runtime ships a small one in its own module, so the two never drift apart.
From Rob Pike's Go Proverbs.
Seven tools, one service
go-kanna/showcase is a to-do service, gRPC over PostgreSQL, with every tool in its place. What the generators write is committed, so each file can be read beside what it came from: here, on wood.
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-dibuilds the infrastructure once, and the handler from it, choosing the repository its use cases are given
- kanna-validatechecks what the use cases are given, and the settings read from the environment
- kanna-valueholds a to-do's completion time between 2000 and 2100, wherever it goes, and its priority to the three it declares
- kanna-ormimplements the repository the use cases depend on, and the scopes it reads through, as
scopes.Todos().CreatedAtAsc() - kanna-mappermaps a to-do to its protobuf message, the input of
PutTodoback from one, priorities by name, and the filter ofListTodo, a oneof, into a sum type - kanna-fixturemakes the to-dos the tests start from
- kanna-i18ncompiles the service's messages, and the texts of validation errors, in English and Japanese
- kanna genruns all of them behind
make gen, and checks what is committed in CI
Every tool
Each is a module of its own: install the ones you need. Each page here is the tool's README.
-
Generates a
Validatemethod from thevalidatetags of a struct, and reports what breaks under Rails' error codes, keyed by the fields' JSON names. Decodes a request and reports everything wrong with it at once.go get -tool github.com/go-kanna/validate/cmd/kanna-validatev0.0.11 Go 1.27
-
Turns a struct holding one value and a
validatemethod into a value type, with constructors, an accessor, and JSON and SQL methods that hold every value to the rule wherever it comes from. A type of your own, so it takes your methods; theemail,url, anduuidtags of kanna-validate call the same rules. A string type with constants becomes an enum that reads in only those, from JSON, text, and a database.go get -tool github.com/go-kanna/value/cmd/kanna-valuev0.0.8 Go 1.26
-
Writes the implementation of your repository interfaces, with the SQL in it as literals, and a scope type for each table whose methods narrow, order, page, and load relations. It reads the schema from a snapshot of your migrated database, so a column or a type that does not match stops generation. PostgreSQL through pgx or database/sql, and MySQL 8.4 or later.
go get -tool github.com/go-kanna/orm/cmd/kanna-ormv0.0.7 Go 1.26
-
Writes the functions between your domain types and the wire types you do not control. You declare the signatures; it writes the bodies, field by field, calling the conversions you list,
uuid.Parseandtimestamppb.Newas they are, and handles protobuf oneofs. An enum of kanna-value maps to a protobuf enum by the names of their constants, and a sum type of the domain to a oneof;//kanna:mapper strictfills only the fields named for it.go get -tool github.com/go-kanna/mapper/cmd/kanna-mapperv0.0.13 Go 1.26
-
Compiles a directory of locale files into a typed constructor for every message, with the translations themselves compiled in, so nothing is read or parsed at run time. CLDR plurals, locale-formatted numbers, and a
Localizerthat takes an Accept-Language value.go get -tool github.com/go-kanna/i18n/cmd/kanna-i18nv0.0.10 Go 1.26
-
Writes the constructor of a container struct from the functions next to your types and next to the container: nothing registered, an interface bound to its implementation with one tag, cleanups returned as one, and nothing to import at run time.
go get -tool github.com/go-kanna/di/cmd/kanna-div0.0.7 Go 1.26
-
Writes a constructor for every struct of a package with every field filled: numbered so that no two collide, within the rules its
validatetags declare, or from a gofakeit template, which is checked while generating.go get -tool github.com/go-kanna/fixture/cmd/kanna-fixturev0.0.10 Go 1.26
-
Runs the generators of a module in the order they need, worked out from what each reads and writes. It adds no configuration: what it runs are the
//go:generatelines already there, and it can show why each comes where it does, or check them in CI.go get -tool github.com/go-kanna/kannav0.3.0 Go 1.26
Why a plane
A kanna (鉋) is the Japanese hand plane, pulled toward you across a board. A good one lifts a shaving thin enough to read through, and leaves a surface that needs nothing more.
That is the job we hold a generator to: take off exactly the code you would have written, thin enough to read, and leave the rest of your program alone. The shaving is the mark, and its wood is the color of everything a generator writes on this site.