Go guidelines¶
Imports¶
The imports defined in the imports ( ... )
block of each Go file should be separated into the following sections, in order.
- Standard library packages (e.g.
fmt
,net/http
) - External packages (e.g.
github.com/stretchr/testify/assert
,github.com/DataDog/datadog-agent/pkg/util/log
) - Internal packages (e.g.
github.com/DataDog/datadog-agent/<parent>/internal
)
This is not verified by our static analysis during CI. Instead, we suggest configuring your editor to keep imports properly sorted.
Editor setup
The goimports tool supports a "local packages" section. Use the flag -local github.com/DataDog/datadog-agent
.
Public APIs¶
Go supports the use of private internal
packages to control the public API of a Go module. This prevents packages from being used outside of the parent module, which is useful for decoupling different parts of our codebase.
When adding new code, carefully consider what API is exported by both taking care of what symbols are uppercase and by making judicious use of internal
directories.
- When in doubt, prefer hiding public APIs as it's much easier to refactor code to expose something that was private than doing it the other way around.
- When refactoring a struct into its own package, try moving it into its own
internal
directory if possible (see example below). - If the directory you are editing already has
internal
directories, try to move code to theseinternal
directories instead of creating new ones. - When creating new
internal
directories, try to use the deepest possibleinternal
directory to limit the packages that can import yours. For example, if makinga/b/c/d
internal, consider moving it toa/b/c/internal/d
instead ofa/internal/b/c/d
. With the first path, code froma/b
won't be able to access thed
package, while it could with the second path.
Example
Sometimes one wants to hide private fields of a struct from other code in the same package to enforce a particular code invariant. In this case, the struct should be moved to a different folder within the same package making this folder internal
.
Consider a module named example
where you want to move exampleStruct
from the a
package to a subfolder to hide its private fields from a
's code.
Before the refactor, the code will look like this:
After the refactor, you should move exampleStruct
to an a/internal/b
directory:
and import this package from a/code.go
:
In this way, no new public API is exposed on the a
folder: ExampleStruct
remains private to a
, while we have improved encapsulation as we wanted.
Atomics¶
Avoid atomics!
Atomics are a very low-level concept and full of subtle gotchas and dramatic performance differences across platforms. If you can use something higher-level, such as something from the standard library's sync
package, prefer to do so. Otherwise, if you are in search of performance, be sure to benchmark your work carefully and on multiple platforms.
It's tempting to use an atomic to avoid complaints from the race detector, but this is almost always a mistake -- it is merely hiding your race from the race detector. Consider carefully why the implementation is racing, and try to address that behavior directly.
The exception to this is tests, where atomics can be useful for sensing some value that you would like to assert on that is manipulated in another goroutine. Even here, be wary of race conditions, such as assuming that a background goroutine has executed before the test goroutine makes its assertions.
Always ensure that you:
-
Use
go.uber.org/atomic
instead ofsync/atomic
.Why?
There are two main issues with the standard library's
sync/atomic
package.- It has an alignment bug requiring users to manually ensure alignment. This is frequently forgotten, and only causes issues on less-common platforms, leading to undetected bugs.
- It is very easy to access a raw integer variable using a mix of atomic and non-atomic operations. This mix may be enough to satisfy the race detector, but not sufficient to actually prevent undefined behavior.
-
Declare atomic types using a pointer to ensure proper alignment.
// global variable
var maxFooCount = atomic.NewUint64(42)
// in a struct
type FooTracker struct {
maxCount *atomic.Uint64
}
func NewFooTracker() *FooTracker {
return &FooTracker {
maxCount: atomic.NewUint64(42),
}
}
Use the atomic.Uint64
methods to perform atomic operations on the value. These include some conveniences not available in sync/atomic
, such as Inc
/Dec
and atomic.Bool
.
If the additional pointer allocation poses an undue performance burden, do both of the following.
- Include the value as the first element in the struct (to ensure alignment).
- Add a comment indicating that it must remain in that position and why a pointer was not suitable.
Pointers to atomic types marshal correctly to JSON as their enclosed value. Unmarshaling does the reverse, except that missing values are represented as nil
, rather than an atomic type with zero value.
Types such as expvar.Int
are simple wrappers around an integer, and are accessed using sync/atomic
. Go will properly align variables (whether global or local) but not struct fields, so any expvar types embedded in a struct must use a pointer.
Testing¶
Failing fast¶
The functions in github.com/stretchr/testify/require
automatically abort the test when an assertion fails, whereas github.com/stretchr/testify/assert
does not.
For example, given an error, assert.NoError(t, err)
causes the test to be marked as a failure, but continues to the next statement, possibly leading to a nil
dereference or other such failure. In contrast, require.NoError(t, err)
aborts the test when an error is encountered.
Where a test makes a sequence of independent assertions, assert
is a good choice. When each assertion depends on the previous having been successful, use require
.
Time¶
Tests based on time are a major source of flakes. If you find yourself thinking something like "the ticker should run three times in 500ms", you will be disappointed at how often that is not true in CI. Even if that test is not flaky, it will take at least 500ms to run. Summing such delays over thousands of tests means very long test runs and slower work for everyone.
When the code you are testing requires time, the first strategy is to remove that requirement. For example, if you are testing the functionality of a poller, factor the code such that the tests can call the poll()
method directly, instead of waiting for a Ticker to do so.
Where this is not possible, refactor the code to use a Clock from github.com/benbjohnson/clock
. In production, create a clock.Clock
, and in tests, inject a clock.Mock
. When time should pass in your test execution, call clock.Add(..)
to deterministically advance the clock.
A common pattern for objects that embed a timer is as follows:
func NewThing(arg1, arg2) *Thing {
return newThingWithClock(arg1, arg2, clock.New())
}
func newThingWithClock(arg1, arg2, clock clock.Clock) *Thing {
return &Thing{
...,
clock: clock,
}
}
func TestThingFunctionality(t *testing.T) {
clk := clock.NewMock()
thing := newThingWithClock(..., clk)
// ...
clk.Add(100 * time.Millisecond)
// ...
}