These guidelines for writing Go code are WIP

Overview

This document covers common coding styles and guidelines for all ForgeRock products.

Copyright notices

Within the FRaaS codebases we are not currently adding licence headers to individual source files.  This practice diverges from the standard practice of doing so across other projects.

The ForgeRock Go coding style

Quick dos/donts

Documentation

Source code layout

In addition to good documentation, having a consistent approach to organising code across directories and within a given source file makes it easier for engineers to move between projects and get up to speed quickly.

Linting rules

Where possible, agreed standards relating to source files should be enforced by linting during continuous integration.  The linting rules currently in use by the FRaaS team are:

linters:
  # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint
  disable-all: true
  enable:
    - deadcode
    - errcheck
    - gofmt
    - goimports
    - gosimple
    - govet
    - ineffassign
    - structcheck
    - typecheck
    - unused
    - varcheck

run:
  tests: true

Logging

Idiomatic Go

In addition to the points raised above, we should endeavour to write idiomatic Go.  Guidance for what these idioms are and how to follow them can be found in:

Mocking

Mocking is done using https://github.com/vektra/mockery which generates mocks using https://github.com/stretchr/testify.

What not to do
Generating mocks

When adding a new interface that will require a mock there are a few simple steps to follow

  1. Follow the instructions to install mockery
  2. Add a line either above the interface you've added or in a package.go file alongside the interface with a 'go generate' comment
  3. Run 'go generate ./...' in the relevant folder - this will run 'mockery' and generate a mock for your interface under the 'mocks' folder

Your interface should look like this:

//go:generate mockery --all

// ThingDoer does a thing
type ThingDoer interface {
	DoThing() (error)
}

If you need to generate a mock for an interface but you also need to use that mock in the same package, this will cause import cycles. To get around this, add '--inpackage' to the list of arguments to 'mockery' in the generate comment.

Using mocks

If you want to use one of these mocks in one of your tests, there is a small utility function in go/common/pkg/testutil/mockhelper.go which can be used to do some common setup and mock assertion. An example of how to use this:


func TestSomething(t *testing.T) {
	// create mock controller
	ctrl := testutil.NewController(t)
	// defer 'finish' - this will check that your mock assertions are satisfied
	defer ctrl.Finish()

	// Create the mock object
	mockThingdoer := &mocks.Thingdoer{}

	// register the mock object with the controller - this will prevent mock assertions from instantly panicking, and will check for assertions at the end of the test
	ctrl.Register(&mockThingdoer.Mock)

	// Set mock assertions
	mockThingDoer.On("DoThing").Return(nil)

	// call your function, do basic checks
	err := useThingdoer(mockThingdoer)
	assert.NoError(t, err)

	// If 'DoThing' was not called, mockery will fail the test when the function ends
}


The testify documentation will have more information about what methods these mocks will have and how to use them.

Assertions in tests

We use https://github.com/stretchr/testify for writing assertions in tests. This has 2 common operations

'require' should be used when it's impossible to continue the test, such as if an unrecoverable error happens during test setup or if a particular object used is not valid:

object, err := GetObject()
require.NoError(t, err)
require.NotNil(t, object)

object.DoThing()

If a test can continue after something fails, use 'assert' instead.

Adding 'options' to a constructor

This is a fairly standard pattern to add extra options to a constructor for a struct. Say you have a struct with a constructor like this

type Recipe struct {
	name       string
	hasSpinach bool
}

func NewRecipe(name string, hasSpinach bool) Recipe {
	return Recipe{
		name:       name,
		hasSpinach: hasSpinach,
	}
}

If you want to extend this struct, the obvious way to do it is to add extra things to the constructor:

type Recipe struct {
	name       string
	hasSpinach bool
    numEggs    int
}

func NewRecipe(name string, hasSpinach bool, numEggs int) Recipe {
	return Recipe{
		name:       name,
		hasSpinach: hasSpinach,
        numEggs:    numEggs,
	}
}

In future people might want to add even more things to this structure, and this approach does not scale:

If you find yourself adding a lot of fields to a struct like this, use 'options' instead.

type Recipe struct {
	name       string
	hasSpinach bool
    numEggs    int
}

type option interface {
	apply(*Recipe)
}

type WithEggs int
func (o WithEggs) Apply(r *Recipe) {
	r.numEggs = o
}

type WithSpinach bool
func (o WithSpinach) Apply(r *Recipe) {
	r.hasSpinach = o
}

func NewRecipe(name string, options ...option) Recipe {
	r := &Recipe{
		name:       name,
	}

	for _, o := range options {
		o.apply(r)
	}

	return r
}

Now anybody who wants to create one of these structs can pass options defining only what they care about rather than every single field in the struct.

recipe1 := NewRecipe("something")
recipe2 := NewRecipe("omelette", WithSpinach(true), WithEggs(6))

Sub-tests

If you are writing a test where there are multiple 'sub tests' (such as checking the behaviour of a certain endpoint with multiple different data inputs), Use t.Run() to separate out these sub-tests.

If you have a test setup like this:

func TestFunction(t *testing.T) {
	tests := []struct {
		name string
		input string
	}{
		{
			...
		},
	}

	for _, test := range tests {
		err := Function(test.input)
		require.NoError(t, err)
	}
}

Then as soon as any test runs into an error, it will implicitly fail all other tests as well. Instead, use sub-tests like this:

	for _, test := range tests {
		t.Run("test.name", func(t *testing.T) {
			err := Function(test.input)
			require.NoError(t, err)
		})
	}

Creating GCP API clients

When creating an API client (for example, a logging client that is used to read logs from the GCP API) and you only ever want to read data, pass a 'WithScope' option to the constructor to limit what the client can do:

crmService, err := cloudresourcemanager.NewService(ctx, option.WithScopes(cloudresourcemanager.CloudPlatformReadOnlyScope))
if err != nil {
	return errors.Wrap(err, "creating service")
}

...


Commonly used libraries