Successful Go Program Design, 6 Years On
Development environment
Some Go developers uses a two-entry GOPATH, e.g.
$HOME/go/external:$HOME/go/internal
go get will fetch into the first path, so it can be useful if you need strict separation of third-party vs. internal code.
Top Tip: put $GOPATH/bin in your $PATH, so installed binaries are easily accessible
Repository structure
The basic idea is to have two top-level directories, pkg and cmd. Underneath pkg, create directories for each of your libraries. Underneath cmd, create directories for each of your binaries. All of your Go code should live exclusively in one of these locations.
github.com/peterbourgon/foo/
circle.yml
Dockerfile
cmd/
foosrv/
main.go
foocli/
main.go
pkg/
fs/
fs.go
fs_test.go
mock.go
mock_test.go
merge/
merge.go
merge_test.go
api/
api.go
api_test.go
All of your artifacts remain go gettable.
Top Tip: put library code under a pkg/ subdirectory. put binaries under a cmd/ subdirectory.
Top Tip: always use fully-qualified import paths. never use relative imports.
Formatting and style
use gofmt, goimports
The go vet tool produces(almost!) no false positives, so you might consider making it part of your precommit hook.
Configuration
12-factor apps encourage you to use environment vars for configuration.
define and parse your flags in func main. Only func main has the right to decide the flags that will be available to the user.
Program design
Good
foo, err := newFoo(
*fooKey,
bar,
100 * time.Millisecond,
nil,
)
if err != nil {
log.Fatal(err)
}
defer foo.close()
Bad
// Don't do this.
cfg := fooConfig{}
cfg.Bar = bar
cfg.Period = 100 * time.Millisecond
cfg.Output = nil
foo, err := newFoo(*fooKey, cfg)
if err != nil {
log.Fatal(err)
}
defer foo.close()
Better
// This is better.
cfg := fooConfig{
Bar: bar,
Period: 100 * time.Millisecond,
Output: nil,
}
foo, err := newFoo(*fooKey, cfg)
if err != nil {
log.Fatal(err)
}
defer foo.close()
Even better
// This is even better.
foo, err := newFoo(*fooKey, fooConfig{
Bar: bar,
Period: 100 * time.Millisecond,
Output: nil,
})
if err != nil {
log.Fatal(err)
}
defer foo.close()
Top Tip: use struct literal initialization to avoid invalid intermediate state. inline struct declarations where possible.
Top Tip: make the zero value useful, especially in config objects
Top Tip: make dependencies explicit.
Top Tip: loggers are dependencies, just like references to other components, database handlers, commandline flags, etc.
Logging and instrumentation
- log only actionable information, which will be read by a human or a machine.
- avoid fine-grained log levels - info and debug are probably enough
- use structured logging, like go-kit/log
- loggers are dependencies
Let’s use loggers and metrics to pivot and address global state more directly. Here are some facts about Go:
- log.Print uses a fixed, global log.Logger
- http.Get uses a fixed, global http.Client
- http.Server, by default, uses a fixed, global log.Logger
- database/sql uses a fixed, global driver registry
- func init exists only to have side effects on package-global state
Testing
Top Tip: use many small interfaces to model dependencies
Top Tip: tests only need to test the thing being tested
Dependency management
- FiloSottile/gvt
- Masterminds/glide
- kardianos/govendor
- constabulary/gb
Build and deploy
Top Tip: perfer go install to go build
Conclusion
Top tips:
- Put $GOPATH/bin in your $PATH, so installed binaries are easily accessible.
- Put library code under a pkg/ subdirectory. Put binaries under a cmd/ subdirectory.
- Always use fully-qualified import paths. Never use relative imports.
- Defer to Andrew Gerrand’s naming conventions.
- Only func main has the right to decide which flags are available to the user.
- Use struct literal initialization to avoid invalid intermediate state.
- Avoid nil checks via default no-op implementations.
- Make the zero value useful, especially in config objects.
- Make dependencies explicit!
- Loggers are dependencies, just like references to other components, database handles, commandline flags, etc.
- Use many small interfaces to model dependencies.
- Tests only need to test the thing being tested.
- Use a top tool to vendor dependencies for your binary.
- Libraries should never vendor their dependencies.
- Prefer go install to go build.