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?