We use Go packages to delimit units of functionality.

We generally like smaller packages with a tight scope, but not so small that components that are always updated together are pulled away from each other by package boundaries.

When to create new packages

Here are example reasons to create new packages:

Where to create new packages and how to name them

In case of doubt, ask for recommendations from the nearest TL.

To name a package, we use the following guidelines:

See also Package Names and Style guideline for Go packages.

How to break cyclical dependencies

Sometimes after choosing a package layout, the Go compiler complains that cyclical dependencies are not allowed.

We can override this in two ways: either using interfaces or using dependency injection. There is also a trick for using helper packages in unit tests.

Interfaces

Suppose you want to define two packages a and b, and define a.Foo() to call b.Bar() and b.Bar() to call a.Foo().

Using interfaces, you would change the code in a as follows:

package a
import "b/bapi"
func Foo(b bapi.B) { b.Bar() }

Then in package b

package b
import "a"
type b struct{}
// implements the bapi.B interface.
func (x b) Bar() { a.Foo(x) }
func Bar() { var x b; x.Bar() }

And sub-directory bapi

package bapi

type B interface {
   Bar()
}

When using this model, ensure that package a has a mock implementation of the other package’s API so that its unit tests can run.

Dependency injection

Suppose you want to define two packages a and b, and define a.Foo() to call b.Bar() and b.Bar() to call a.Foo().

  1. In package a, for example, define var BarFn = func() { ... placeholder ...}

  2. In package b, implement func init() { a.BarFn = Bar }

Note: We are OK with dep injection for the sake of making the code clearer as per the guidelines above, under the following conditions:

Test packages

Often we want to use a helper package in a unit test, but just in the unit test, and that import creates a dependency cycle.

For this, Go already has a native functionality: test files (*_test.go) inside a package named X can declare another package X_test.

X_test can import another package that then recursively depends on X, and this way there is no cycle.

For example, in CockroachDB we often use testserver in tests. testserver depends on pkg/server, which depends on pkg/sql. So all the tests in the sql and server package that want a test server cannot be part of the sql or server packages; at the top of their source file we see package sql_test and package server_test.

Separately licensed packages inside CockroachDB: CCL and BSL

CockroachDB contains code under two separate licenses:

We strongly wish to retain the ability to build cockroach binaries that do not contain CCL-licensed code, for several reasons:

This means that none of the BSL code can import (using the Go import statement) CCL packages. So all the CCL functionality is injected into the BSL code using dependency injection.

(The converse is possible: CCL code can import BSL code, and this happens often.)