DEV Community

Nik L.
Nik L.

Posted on

Streamlining Code with go generate for Reduced Boilerplate

Thought of delving more in Go, after this article:


The Go programming language is renowned for its simplicity, power, exceptional tooling, and widespread adoption. Nevertheless, as is typical with strongly typed languages, developers often find themselves crafting significant amounts of boilerplate code to establish connections and perform routine tasks.

In this article, we'll delve into three key aspects:

  1. Why Code Generation in Go: The rationale behind employing code generation in Go to alleviate boilerplate code.

  2. Foundations of Code Generation in Go: Exploring the fundamental elements and libraries that facilitate code generation.

  3. Learning from Examples: Discovering existing code generation tools and projects to gain insights and inspiration.

Leveraging Code Generation to Reduce Boilerplate

Developers sometimes attempt to diminish boilerplate by using reflection and introducing methods that accept the generic interface{} type. However, this approach sacrifices type safety. When interface{} is employed, the compiler cannot ensure that the correct types are being used, which increases the risk of runtime errors.

Many instances of boilerplate code can be inferred from the existing project codebase. To address this, developers can create tools that analyze the project's source code and generate the necessary code automatically.

Elements of Code Generation in Go

To implement code generation effectively in Go, developers need to master the fundamental elements that facilitate code analysis and generation.

  • Reading Code: The Go standard library provides several packages for reading and parsing code:

    • go/build: Gathers information about Go packages, including directory locations, code and test files, and dependencies.
    • go/scanner and go/parser: These packages read source code and parse it into an Abstract Syntax Tree (AST).
    • go/ast: Declares types used to represent the AST and provides methods for traversing and modifying the tree.
    • go/types: Defines data types and algorithms for type-checking Go packages. This package processes the AST to provide type information.
  • Generating Code: Most projects rely on the text/template package to generate code. It's advisable to begin generated files with a comment indicating that the code is automatically generated, specifying the generating tool and emphasizing that manual edits should be avoided.

Example Comment:

/*
* CODE GENERATED AUTOMATICALLY WITH github.com/ernesto-jimenez/gogen/unmarshalmap
* THIS FILE SHOULD NOT BE EDITED BY HAND
*/
Enter fullscreen mode Exit fullscreen mode

The go/format package, which contains the logic used by go fmt, can be used to format generated code before writing it.

Using go generate

When developing tools for code generation in Go, two questions frequently arise: When should code generation occur during the development process, and how can generated code be kept up-to-date?

Since Go version 1.4, the go tool includes the generate command, enabling developers to run code generation tools through the go tool itself. Special comments within the source code dictate which commands should be executed during code generation.

To invoke code generation, add a comment in the following format:

//go:generate shell command
Enter fullscreen mode Exit fullscreen mode

The important points to remember are:

  • go generate should be initiated by the developer authoring the program or package and is not automatically triggered by go get.
  • Ensure that all tools specified in go generate are installed and configured on your system, and document where to obtain these tools.

If the code generation tool is located within the same repository, using go run from go:generate is recommended. This allows you to execute code generation without manual building and installation each time the tool is modified.

Initiating Your Code Generation Journey

While the standard library packages for code parsing and generation are powerful, comprehending their usage solely through documentation can be challenging. Learning from existing code generation tools is highly beneficial, serving multiple purposes:

  1. Gaining inspiration for the types of tools you can build.
  2. Learning from the source code of existing tools.
  3. Discovering tools that can be valuable on their own.

Projects for Learning

Two practical examples of code generation tools:

  1. Generating Stubs for Interface Implementation

Have you ever encountered the repetitive task of manually creating method stubs when implementing an interface? The 'impl' tool can automatically generate stubs for you. It scans the standard library to identify the interface and produces the required method implementations.

Example:

   $ impl 'f *File' io.ReadWriteCloser
   func (f *File) Read(p []byte) (n int, err error) {
       panic("not implemented")
   }

   func (f *File) Write(p []byte) (n int, err error) {
       panic("not implemented")
   }

   func (f *File) Close() error {
       panic("not implemented")
   }
Enter fullscreen mode Exit fullscreen mode
  1. Automatic Mock Generation with 'mockery'

Automated Mock Generation with 'mockery'

The testify package offers a robust mock feature, simplifying the process of mocking dependencies during unit testing. Leveraging the implicit satisfaction of interfaces, we can define our dependencies using interfaces and utilize mock objects in place of actual external dependencies.

Here's a straightforward illustration of how to mock an imaginary 'downcaser' interface:

package main

import (
  "testing"

  "github.com/stretchr/testify/mock"
)

type downcaser interface {
  Downcase(string) (string, error)
}

func TestMock(t *testing.T) {
  m := &mockDowncaser{}
  m.On("Downcase", "FOO").Return("foo", nil)
  m.Downcase("FOO")
  m.AssertNumberOfCalls(t, "Downcase", 1)
}
Enter fullscreen mode Exit fullscreen mode

The implementation of the mock object is quite straightforward:

type mockDowncaser struct {
  mock.Mock
}

func (m *mockDowncaser) Downcase(a0 string) (string, error) {
  ret := m.Called(a0)
  return ret.Get(0).(string), ret.Error(1)
}
Enter fullscreen mode Exit fullscreen mode

Remarkably, the mock object can be generated automatically, thanks to the simplicity of the interface definition. This is precisely what 'mockery' accomplishes:

$ mockery -inpkg -testonly -name=downcaser
Generating mock for: downcaser
Enter fullscreen mode Exit fullscreen mode

I routinely employ 'mockery' in conjunction with 'go generate' to effortlessly produce mock implementations for my interfaces. The process entails adding just one line of code to the existing example to set up a functional mock.

package main

import (
  "testing"
)

type downcaser interface {
  Downcase(string) (string, error)
}

//go:generate mockery -inpkg -testonly -name=downcaser

func TestMock(t *testing.T) {
  m := &mockDowncaser{}
  m.On("Downcase", "FOO").Return("foo", nil)
  m.Downcase("FOO")
  m.AssertNumberOfCalls(t, "Downcase", 1)
}
Enter fullscreen mode Exit fullscreen mode

The setup is complete once 'go generate' is executed. Here's an overview of how the process unfolds:

$ go test
# github.com/ernesto-jimenez/test
./main_test.go:14: undefined: mockDowncaser
FAIL    github.com/ernesto-jimenez/test [build failed]

$ go generate
Generating mock for: downcaser

$ go test
PASS
ok      github.com/ernesto-jimenez/test 0.011s
Enter fullscreen mode Exit fullscreen mode

Any modifications to the interface are seamlessly accommodated by running 'go generate,' ensuring that the corresponding mock remains up to date. 'mockery' played a pivotal role in my journey, leading me to contribute to testify/mock and ultimately assume the role of a maintainer for the project. However, it's worth noting that 'mockery' was developed before the inclusion of 'go/types' in the standard library in Go version 1.5. Consequently, it relies on the lower-level 'go/ast,' which can make the code more complex to follow and may introduce issues when generating mocks for interfaces that employ composition.

Exploring 'gogen' Experiments

The author of this article has open-sourced a code generation package called 'gogen.' It currently includes three tools:

  1. goautomock: Similar to 'mockery' but implemented using 'go/types' rather than 'go/ast,' making it compatible with composed interfaces and standard library interfaces.

  2. gounmarshalmap: Generates an UnmarshalMap(map[string]interface{}) function for a struct, decoding a map into the struct. It serves as an alternative to 'mapstructure,' relying on code generation instead of reflection.

  3. gospecific: A small experiment to generate specific packages from generic ones using interface{}. It analyzes the generic package's source code and generates a new package with specific types, addressing the use of interface{}.

Conclusion

Code generation is a powerful technique that reduces the burden of writing repetitive code while maintaining type safety. It is widely adopted by projects like Slackline and is likely to see increased use in the future.

However, developers should assess the worthiness of creating a code generation tool for their specific use case before embarking on the development journey.


Similar to this, I run a developer-centric community on Slack. Where we discuss these kinds of topics, implementations, integrations, some truth bombs, weird chats, virtual meets, and everything that will help a developer remain sane ;) Afterall, too much knowledge can be dangerous too.

I'm inviting you to join our free community, take part in discussions, and share your freaking experience & expertise. You can fill out this form, and a Slack invite will ring your email in a few days. We have amazing folks from some of the great companies, and you wouldn't wanna miss interacting with them. Invite Form

Top comments (0)