Blog

The use of "annotations" boosted our productivity in Golang

13 Dec, 2021

Writing REST-services in Go is fairly boring and repetitive. Both with the standard http-library as with frameworks you need to write quite some code to implement a simple service. Since we do like Java’s “JAX-RS” approach (using annotations), we wondered if we can use this same declarative approach in Go. This blog describes this successful experiment.

This approach has matured since and has proven itself in a large healthcare project. The source-code of our home-made tool can be found on Github: https://github.com/MarcGrol/golangAnnotations.

Overview of our solution

Our annotations are packed inside regular comments. Consequently, the AST-library (from the go standard library) is used to parse the go source-code (including our special annotation-comments). Code-generators are then triggered to create boring and predictable source-code. This generated code is just committed with the rest of your code. One advantage of this approach is that the generated-code is not some magical bytecode (like in Java) but easily readable and debuggable source-code.

Step-by-step

JAX-RS-like annotations

It all starts with adding annotations to our own go source-code. We did not re-invent a new syntax for go-annotations. To make the annotations easily recognisable, the annotations are modelled like in Java. One or more annotations can be attached to structs, enums, functions, methods and interfaces.

Example fragment of our golang code:

package tourdefrance
// @RestService( path="/api/tour" )
type TourDeFranceService struct{}
type EtappeResult struct{ ... }
// @RestOperation( method="PUT", path="/{year}/etappe/{etappeUid}" )
func (ts *TourService) addEtappeResults(c context.Context, year int,
                etappeUid string, results EtappeResult) error {
    // ...
    return nil
}

Parsing go code into an intermediate model

When we parse this code, the golang’s AST-library spits out something like this: Example

This hairy AST (=abstract syntax tree) is then transformed into a simplified intermediate representation. Coding this transformation was fairly tedious and is covered by a solid set of unit tests. Currently the following constructs are extracted from the ast:

  • structs with their fields
  • enumerations with their literals
  • functions and methods with their input and output params
  • interfaces with operations with their input- and output params

This is the resulting intermediate representation of the annotated go example above:

{
    packageName: "tourdefrance",
    structs: [
        {
            docLines: ["// @RestService( path=/api/tour)"],
            name: "TourService",
            operations: [
                {
                    docLines: ["// @RestOperation( method=PUT, path=/{year}/etappe/{etappeUid}"],
                    name: "addEtappeResults",
                    inputArgs: [
                        {name: "c", typeName: "context.Context"},
                        {name: "year", typeName: "int"},
                        {name: "etappeUid", typeName: "string"},
                        {name: "results", typeName: "EtappeResult"}
                    ],
                    outputArgs: [
                        {typeName: "error"},
                    ],
                }
            ]
        }
    ]
}

Generation of source-code from the populated intermediate model

Once we have a fully populated intermediate representation, we can feed it into our code-generators. These generators are typically based on go’s “text/template”-library. Based on our rest-annotations the following predictable and repetitive code is generated to handle all the HTTP stuff. 

Conclusion

The go standard library allows you to parse go-code into your own meta model. Based on this intermediate model and comment-like annotations we use code generation to create boring and repeatable parts of our application. This approach is currently heavily used in the healthcare platform Duxxie.

Besides rest-annotations, we have also added more annotations to support the following:

  • type-strong http-clients (to facilitate automated testing)
  • serialisable enums
  • event-sourcing
  • event-handling
  • datastore CRUD
  • documentation.

Now the hard work is done, new annotations and generators can relatively easily be added. Does anybody have suggestions for other situations where this approach might also shine? Feel free to offer pull requests.

 

Explore related posts