Getting started with Go Generate

February 3, 2016

Go is a great systems language. It’s simple, doesn’t have too many frills and gets the job done. In addition to that, the tooling around Go has made it a great language for teams. For instance, gofmt formats the code in the codebase in a standard manner because life is too short for inconsistent code. GoVet prints inconsistencies in the code; such as Printf calls whose arguments don’t match with the format string. This ensures that the code produced by all team members has the same look and feel.

 One other tool in the Go kitty is “go generate”. Since Golang is famously infamous for not supporting generics, go:generate seems to be a way to circumvent that limitation. This command allows developers to get the compiler to generate templatized code. In Rob Pike’s words, “the future of computing lies in getting computer programs to write computer programs”. This post is aimed at providing a quick overview on how you can use the generate command and improve your workflow.

One of the first things to understand about “go generate” is that it’s not a part of the “go build” toolchain. It doesn’t perform any dependency analysis and must be called before “go build”. In other words, it’s only for the author of the Go package and not the client.

Let’s get started with an example. Here, we’ll use the generate tool to create model functions from the db schema code directly. This ensures that the schema code and the supported functions are in the same place.

Let’s assume that we have a schema definition file as below:

CREATE TABLE users (
 id PRIMARY KEY,
 name VARCHAR(255)
);

Now, to hold this data in the code, we’ll have a struct:

type User struct{
       ID       int64,
       Name string,
}

Now, using Go’s database/sql package, the code to query the DB would be

func UserByID(db *sql.DB, user *User, id int64) error {
   row := db.QueryRow("SELECT id, name FROM users WHERE id = $1", id)
   return row.Scan(&user.ID, &user.Name)
}

Now, if there a lot of fields and tables, writing this code by hand for all the variants can become tedious and time-consuming. In addition to this, for production ready code, we need the following features:

  • Error Handling
  • Mock implementations for testing
  • An interface for all implementations to comply to
  • Hopefully, write SQL queries outside of the Go code. This would allow us to manage the queries better

Let’s modify our code accordingly to satisfy these requirements.

SQL File:

SELECT id, name FROM users WHERE id = $1

Interface Definition

type SQLQuery interface {
    UserByID(db *sql.DB, user *User, id int64) error
}

Implementation

func (db *DBReader) UserByID(db *sql.DB, user *User, id int64) error {
    row := db.QueryRow("SELECT id, name FROM accounts WHERE id = $1", id)
    return row.Scan(&account.ID, &account.Name
}

Mock Implementation

func (db *MockDBReader) UserByID(db *sql.DB, user *User, id int64) error {
    user = &User{
        ID: 1,
        Name: “Test”,
    }
}
return nil

That’s a lot of code for a simple function! Once the codebase grows and more queries are added, it’ll get harder to maintain and modify packages to ensure that all these conditions are met. Let’s simplify it by generating code that is similar in nature.

Code Generation

For different fields, the function signatures may differ but the basics remain the same. The only difference between such functions is the name and some parameters. The code to query the database and return the result remains the same. We can automate this part by using go generate.

The basic principle behind the generate tool is to pick out commands from the //go:generate comments and then execute those commands. We can parse those commands and generate the required code.

 

A possible modification of the SQL file could be:

-- !selectOne UserByID
-- $1: id int64
SELECT id, name FROM users WHERE id = $1 LIMIT 1;
  • The custom command !selectOne allows the code generator to create a function that reads a single row from the DB and returns that
  • The name of the function is defined as “UserByID”
  • We then define the parameters (along with the type) that are required by the function

Now, all the SQL queries reside in a single location along with the function names and the parameters.

Let’s assume our code generation command is called “codegen”. We can now generate multiple variations of the code by simply adding the comment

To create the interface:

//go:generate codegen -type interface

To create the implementation:

//go:generate codegen -type implementation

To create the mock:

//go:generate codegen -type mock

Voila! A single SQL file can now be used to generate all the required boilerplate code. Infact, as we add more tables and queries, we simply need to modify the SQL,  annotate it and then run the go:generate command. That’s it!

In our production system, we’ve used “go generate” to create a library in Golang that outputs ExoML code. (For those not familiar with the term, the ExoML code is an XML structure that gives you granular control of a phone call made through Exotel). You can check it out here. Do give it a look over. Here, we’ve used go generate to create setters and getters for different structs. It’s a fairly nice starting point for people getting their hands dirty with Golang. This tiny library is a different use-case of the same thought process: 

If code looks boilerplate, then spend your time and energy in generating it and not writing it.

Arpit Mohan
Written By

Try Exotel free for 7 days

Get Rs 1000 worth free call & SMS credits