Generating Code in Go
Different approach to some challenges
(no AI)
Who am I?
Staff Backend Engineer at Pento
Daniel Antos
I used to hate generated code.
What are we talking about today?
- Why?
- Basics of generating code in Go.
- Generate your APIs: gRPC, GraphQL.
- Generate your ORM.
- Generating decorators.
-
Everyday life with generated code.
Why?
- Compile time safety
- To reduce abstraction and complexity
- Computer is better at repetitive task
Error while building:
# github.com/antosdaniel/go-presentation-generate-code/internal/grpc
internal/grpc/payrollServiceWithAuth.go:16:9: ... (missing method GetPayslip)
Basics of generating code in Go
Generators are just CLI tools:
go generate <path>
go generate ./...
Generate your APIs: gRPC, GraphQL
syntax = "proto3";
package payroll.v1;
service PayrollService {
rpc AddPayroll(AddPayrollRequest) returns (AddPayrollResponse);
rpc AddPayslip(AddPayslipRequest) returns (AddPayslipResponse);
rpc GetPayroll(GetPayrollRequest) returns (GetPayrollResponse);
}
message AddPayrollRequest {
string payroll_id = 1;
string tenant_id = 2;
Date payday = 3;
}
// ...
Generate your APIs: gRPC, GraphQL, REST
// PayrollServiceHandler is an implementation of the payroll.v1.PayrollService service.
type PayrollServiceHandler interface {
AddPayroll(context.Context, *connect_go.Request[payrollv1.AddPayrollRequest]) (*connect_go.Response[payrollv1.AddPayrollResponse], error)
AddPayslip(context.Context, *connect_go.Request[payrollv1.AddPayslipRequest]) (*connect_go.Response[payrollv1.AddPayslipResponse], error)
GetPayroll(context.Context, *connect_go.Request[payrollv1.GetPayrollRequest]) (*connect_go.Response[payrollv1.GetPayrollResponse], error)
}
// Now we only have to implement it:
func (s *payrollServiceServer) AddPayroll(
ctx context.Context,
request *connect_go.Request[payrollv1.AddPayrollRequest],
) (*connect_go.Response[payrollv1.AddPayrollResponse], error) {
// TODO: business logic
return &connect_go.Response[payrollv1.AddPayrollResponse]{
Msg: &payrollv1.AddPayrollResponse{ PayrollId: id, },
}, nil
}
Generate your ORM
create table payrolls (
id uuid not null primary key,
tenant_id uuid not null,
payday date not null
);
type PayrollModel struct {
ID string `orm:"id"`
TenantID string `orm:"tenant_id"`
Payday string `orm:"payday"`
}
The ugly side of abstraction
func FindPayroll(ctx context.Context, exec boil.ContextExecutor, iD string, selectCols ...string) (*Payroll, error) {
payrollObj := &Payroll{}
sel := "*"
if len(selectCols) > 0 {
sel = strings.Join(strmangle.IdentQuoteSlice(dialect.LQ, dialect.RQ, selectCols), ",")
}
query := fmt.Sprintf(
"select %s from \"payrolls\" where \"id\"=$1", sel,
)
q := queries.Raw(query, iD)
err := q.Bind(ctx, exec, payrollObj)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, sql.ErrNoRows
}
return nil, errors.Wrap(err, "models: unable to select from payrolls")
}
return payrollObj, nil
}
Abstraction and reflection are costly
Generating decorators
What can we generate?
func (ps PayrollServiceWithTrace) AddPayroll(...) error {
ps.tracer.Start(...)
defer ps.tracer.End(...)
err := ps.base.AddPayroll(...)
if err != nil {
ps.tracer.RaiseError(err)
}
return err
}
func (ps PayrollServiceWithTrace) AddPayslip(...) error {
ps.tracer.Start(...)
defer ps.tracer.End(...)
err := ps.base.AddPayslip(...)
if err != nil {
ps.tracer.RaiseError(err)
}
return err
}
Everyday life with generated code
because you will hit some issues
Commit generated code, and check it in CI
- No need to generate code before build
- Easy to spot unintended changes during code review
- Easily reproducible builds
Makefile is where it begins
.PHONY: install
install:
@printf "\nInstalling sqlboiler...\n"
@go install -mod=readonly github.com/volatiletech/sqlboiler/v4@v4.15.0
@go install -mod=readonly github.com/volatiletech/sqlboiler/v4/drivers/sqlboiler-psql@v4.15.0
@printf "\nInstalling gowrap...\n"
@go install -mod=readonly github.com/hexdigest/gowrap/cmd/gowrap@v1.3.2
# ...
.PHONY: generate
generate:
@printf "Generating protos...\n"
@buf generate --template gen/grpc/buf.gen.yaml
@printf "Generating db models...\n"
@sqlboiler --config db/sqlboiler.toml psql
@printf "go generate...\n"
@go generate ./...
@$(MAKE) format
Live coding!
What did we learn?
- When you have schema, there is no reason to repeat yourself (DRY)
- Compiler can find mistakes for you
- You can drive improvement through automation
Recommended tools
Thanks!
Any questions for me?