COGS - COmputable Golang Style
The goal of this document is to create consistently styled Go code as followed by Computable.
These are general guidelines, for further in-depth resources please see the following:
Vscode users may get annoying linting issues] via the default linter. An improvement over the default linter is using GolangCI-Lint
Can be installed with the following command:
go get -u github.com/go-delve/delve/cmd/dlv
Please follow the repo instructions to setup correctly.
For further detailed resources see the following:
- How to structure your Go apps by Kat Zien (video)
- Best practices for Industrial Programming by Peter Bourgon (video)
- Golang project layout
/cmd
- Main application for this project
- Don't put a lot of code in the application directory. If the code can be imported and used in other projects, it should live in
/pkg
. - It is common to have small
main()
functions. e.g.
func main() {
if err := run(); err != nil {
fmt.Printf(os.Stderr, "%v", err)
os.Exit(1)
}
}
/pkg
- Code that's okay to be used by external applications.
- Specifically we will be grouping packages by context as related by Domain Driven Design.
/internal
- Private applications and library code.
- Code you don't want others importing.
- This layout pattern is enfored by the Go compiler.
- You can have more than one internal directory at any level in the tree.
/vendor
- Application dependencies
/configs
- Configuration files
The goal of DDD is to create a ubiquitous language allowing easy communication between cross-functional groups. It creates clarity by standardizing the use of words with pre-agreed meanings.
It also informs us how to structure our projects. This is known as "grouping by context." Our project structure will follow Domain Driven Design as best described by Kat Zien here.
Every repository with be accompanied with a D3.md
featuring Ubiquitous Language that should be enforced by the package structure and actual code itself.
Prefer explicit idiomatic file names as opposed to default DDD naming. e.g.
Bad
/pkg/adding
--- service.go
Good
/pkg/adding
--- beer_adder.go
Bad | Good |
---|---|
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64 |
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
) |
Do not group unrelated declarations.
Bad | Good |
---|---|
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
ENV_VAR = "MY_ENV"
) |
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const ENV_VAR = "MY_ENV" |
There should be two import groups:
- Standard library
- Everything else
Bad | Good |
---|---|
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
Code should reduce nesting where possible. This can be done by handling special cases/errors early.
Bad | Good |
---|---|
for _, v := range data {
if v.F1 == 1 {
v = process(v)
if err := v.Call(); err == nil {
v.Send()
} else {
return err
}
} else {
log.Printf("Invalid v: %v", v)
}
} |
for _, v := range data {
if v.F1 != 1 {
log.Printf("Invalid v: %v", v)
continue
}
v = process(v)
if err := v.Call(); err != nil {
return err
}
v.Send()
} |
And avoiding unnecessary else's.
Bad | Good |
---|---|
if ok {
err := something()
if err != nil {
return errors.Wrap(err, "something")
}
// do things
} else {
return errors.New("not ok")
} |
if !ok {
return errors.New("not ok")
}
err := something()
if err != nil {
return errors.Wrap(err, "something")
}
// do things |
If errors occur, the function must return an error and allow the caller to decide how to handle.
Bad | Good |
---|---|
func foo(bar string) {
if len(bar) == 0 {
panic("bar must not be empty")
}
// ...
}
func main() {
if len(os.Args) != 2 {
fmt.Println("USAGE: foo <bar>")
os.Exit(1)
}
foo(os.Args[1])
} |
func foo(bar string) error {
if len(bar) == 0 {
return errors.New("bar must not be empty")
}
// ...
return nil
}
func main() {
if len(os.Args) != 2 {
fmt.Println("USAGE: foo <bar>")
os.Exit(1)
}
if err := foo(os.Args[1]); err != nil {
panic(err)
}
} |
Error strings should not be capitalized (unless beginning with proper nouns or acronyms) or end with punctuation. Errors are usually usually printed following other context.
E.g. use fmt.Errorf("something bad")
not fmt.Errorf("Something bad")
,
so that log.Printf("Reading %s: %v", filename, err)
formats without a spurious capital letter mid-message.
Explicit should be favored over implicit.
Bad | Good |
---|---|
k := User{"John", "Doe", true} |
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
} |
Reduce variable scope where possible. Do not reduce scope if it conflicts with reducing nesting.
Bad | Good |
---|---|
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
} |
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
} |
Go supports raw string literals, which can span multiple lines and include quotes. Use these to avoid hand-escaped strings which are much harder to read.
Bad | Good |
---|---|
wantError := "unknown name:\"test\"" |
wantError := `unknown error:"test"` |
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar"
sval := T{Name: "foo"}
sptr := &T{Name: "bar"}
Bad | Good |
---|---|
t := []string{} |
var t []string |
Prefer using only as narrow an interface as needed.
Bad | Good |
---|---|
type File interface {
io.Closer
io.Reader
io.ReaderAt
io.Seeker
}
func ReadIn(f File) {
b := []byte{}
n, err := f.Read(b)
} |
func ReadIn(r Reader) {
b := []byte{}
n, err := r.Read(b)
} |
We needn't create an entire File
to use ReadIn()
when we're only interested in using the Reader
.
Bad | Good |
---|---|
func getConfig() (*config, error) {
var cfg config
configFile, openErr := os.Open(SMART_CONTRACT_CONFIG_PATH)
defer configFile.Close()
if openErr != nil {
return &cfg, openErr
}
// ...
} |
func getConfig() (*config, error) {
var cfg config
configFile, openErr := os.Open(SMART_CONTRACT_CONFIG_PATH)
if openErr != nil {
return &cfg, openErr
}
defer configFile.Close()
// ...
} |
Method signatures should be as minimal as possible.
When a single letter isn't available, use the shortest sensible two letter variable name.
Bad | Good |
---|---|
func AddListingHandlers(router Routable, list list.Listor, client projectorhttp.HttpClient, loggingChannel chan log.Message) Routable {
// ...
} |
func AddListingHandlers(r Routable, l list.Listor, c projectorhttp.HttpClient, lc chan log.Message) Routable {
// ...
} |
Our constants are in SCREAMING_SNAKE_CASE
, in lieu of the commonly recommended camelCase
.
This was done for readability.
They should never be imported.
Bad | Good |
---|---|
const userId = "userId"
const slug = "slug"
const xCorrelationId = "X-Correlation-ID" |
const USER_ID = "userId"
const SLUG = "slug"
const X_CORRELATION_ID = "X-Correlation-ID" |
-
Our architecture is separated into
Left
,Center
,Right
. -
Left
- Application code
- HTTP and REST for our API
- Is free to import center
- Must implement interfaces dictated by center
- Drives the center
- By instantiating aggregates/entities
- By calling methods on center domain object,
-able
's
-
Center
- Where we define aggregates/entities
- Where we define center domain objects,
-able
's - Where we define our infrastructure interfaces,
-or
's-or
's are interfaces that define the center's ports- Right creates an adapter by fulfilling interfaces defined by these ports
- Right and center meet where ports and adapters connect
- Is importable by all
- Drives right
- Imports 3rd party libraries when context encapsulates 3rd party library
- Is sectioned into Bounded Contexts
- Contexts don't import each other
- Does not import left or right
- Prevents circular imports
- Allows repository mocking
-
Right
- Is free to import center
- Can be
- Infastructure
- a Service (e.g. Stripe)
- a Repository (e.g. Postgres, Dynamo)
- Fulfills the
-or
interface as dictated by the center
Example
func registration(able register.Registerable, or register.Registeror) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
loggor := getChannelLoggor(r)
correlationId(r)
if err := decodeBody(r, able); err != nil {
logging.LogError(loggor, err)
respondHTTPErr(w, http.StatusBadRequest)
return
}
# left drives center by calling method on the center domain object -able
if err := able.Register(or, loggor); err != nil {
logging.LogError(loggor, err)
respondHTTPErr(w, http.StatusInternalServerError)
return
}
respond(w, http.StatusOK, &able)
}
}