Skip to content

Commit 3effddf

Browse files
authored
[APT-1640] Add style guides (#143)
1 parent 2863d47 commit 3effddf

File tree

6 files changed

+1331
-1
lines changed

6 files changed

+1331
-1
lines changed

_docs-sources/guides/index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,9 @@ The Gruntwork IaC library empowers you to construct your own bespoke architectur
116116

117117
</Grid>
118118

119+
## Style Guides
120+
121+
- [Terraform Style Guide](/docs/guides/style/terraform-style-guide)
122+
- [Go Style Guide](/docs/guides/style/golang-style-guide)
123+
119124
</CenterLayout>
Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
---
2+
category: Style Guides
3+
excerpt: The style guide for the Go programming language that is used by all Gruntwork-owned code repositories that contain Go code.
4+
tags: ["go", "golang", "code", "style"]
5+
---
6+
7+
# Go Style Guide
8+
9+
This is Gruntwork’s style guide for the Go programming language. It aims to help us ensure that the code we write is
10+
clear, readable, idiomatic Go code. The conventions detailed in this guide are our preferences and should be thought of
11+
as guidelines rather than hard rules.
12+
13+
## Starting point
14+
15+
We use the excellent [Effective Go](https://golang.org/doc/effective_go.html) guide as the starting point for our style
16+
guide. **Unless explicitly mentioned otherwise, we default to following the conventions outlined in Effective Go.**
17+
18+
Another helpful resource is the [CodeReviewComments](https://github.com/golang/go/wiki/CodeReviewComments) section of the
19+
Go GitHub wiki page.
20+
21+
Below you will find the list of conventions that differ from the above mentioned documents, either because they are
22+
specific to Gruntwork, or because we consciously chose to go against what Effective Go and CodeReviewComments recommend.
23+
24+
## Additional conventions
25+
26+
### General
27+
28+
Before committing any Go code, run `go fmt`. We run this as part of the CI build, so this will prevent your build from failing.
29+
30+
### Comments
31+
32+
Every public function `Foo` should have a `//` comment of the style `Foo <explanation>`,
33+
where explanation says what `Foo` does, and more importantly, why it does that, what assumptions it makes, and other
34+
aspects not obvious from the function name.
35+
36+
### Control structures
37+
38+
When possible, avoid nesting `if`/`else` statements. (Of course, a single, non-nested `if`/`else` is perfectly fine.)
39+
Prefer multiple `return` statements over nested code. This makes the code more readable and avoids complexity.
40+
41+
E.g:
42+
43+
```go
44+
// Do this
45+
if something {
46+
doThis()
47+
return this
48+
}
49+
if orOther {
50+
return that
51+
}
52+
return theOther
53+
```
54+
55+
```go
56+
// Don't do this
57+
if something {
58+
doThis()
59+
} else {
60+
if orOther {
61+
return that
62+
}
63+
}
64+
```
65+
66+
### Error handling
67+
68+
Prefer using the `errors` standard library package for handling single errors. For operations that can produce multiple
69+
errors, leverage the [`MultiError`](https://github.com/gruntwork-io/terragrunt/blob/master/errors/multierror.go)
70+
package by [accumulating
71+
all the errors into a single `MultiError` and returning that](https://github.com/gruntwork-io/terragrunt/blob/cb369119bf5c6f3031e914e8554ffe056dcf9e22/cli/hclfmt.go#L62), rather than returning every error individually as it comes up.
72+
73+
[Don’t panic](https://github.com/golang/go/wiki/CodeReviewComments#dont-panic) (_hat tip, Douglas Adams_). Any method that
74+
can have an error should return that error as its final value. This should be passed up through each layer, which can
75+
decide what to do with it, all the way up to the very entrypoint of the app (or `main`) if necessary.
76+
You should almost NEVER use `panic`.
77+
78+
Use custom error types. Create your own types that implement the `error` interface so that error messages are clear
79+
and have well-defined types you can check against. For some examples of this, see e.g. the custom errors in the
80+
[aws](https://github.com/gruntwork-io/terratest/blob/master/modules/aws/errors.go) package of `terratest`.
81+
82+
Include stack traces. In most of our code, we have to wrap errors with
83+
[`errors.WithStackTrace(e)`](https://github.com/gruntwork-io/gruntwork-cli/blob/master/errors/errors.go#L22) to add the stack trace.
84+
Go annoyingly doesn’t do this by default, but without it, sorting out an error can be very tricky.
85+
86+
### Pointer usage
87+
88+
Prefer using value type over pointers, unless necessary. Generally speaking, there are only a few cases where pointer
89+
usage would be justified:
90+
91+
1. You have very large structs containing lots of data. Instead of copying these around, passing pointers may be more
92+
efficient.
93+
94+
2. You need to mutate a variable you passed to a function. Generally speaking, you should avoid this anyway (see the
95+
section on immutability below), but sometimes it is necessary (think "rename" methods, etc).
96+
97+
3. You need to return `nil` as the default zero value (the default zero value for pointers is `nil`, as opposed to other data
98+
types that have non-nil default zero values).
99+
100+
### Testing
101+
102+
In terms of testing, we don’t necessarily aim for 100% test coverage. Rather, our goal is to have enough testing to give
103+
us confidence that our code works and allow us to update it quickly. When adding tests, keep in mind these factors:
104+
105+
1. The likelihood of bugs: some code is harder to get right than others, and merits correspondingly more tests.
106+
107+
2. The cost of bugs: some bugs are much more costly than others — e.g., bugs in payment systems or security functionality —
108+
so it makes sense to invest more in testing there.
109+
110+
3. The cost of tests: some types of tests are cheap and easy to write and maintain, whereas others are very costly and brittle.
111+
112+
Unless there’s a reason not to, tests should run in parallel. This is done by including a call to `t.Parallel` in the test function.
113+
114+
#### Setup and Teardown pattern
115+
116+
In some cases you will want to write a group of tests that use a common resource, such as a Docker image or VPC.
117+
In this case, you will want to setup the common resource once, run a bunch of tests, and then teardown the resource.
118+
To achieve this, you can follow [the subtest pattern](https://blog.golang.org/subtests) of Go.
119+
120+
Always use [table driven tests](https://dave.cheney.net/2019/05/07/prefer-table-driven-tests) where possible to make the
121+
subtest routines maintainable. Briefly, this means that you group your test cases using a test struct that reflects
122+
the unique parameters of the test cases. Then you can conveniently loop over the test cases in parallel, taking advantage
123+
of uniformity and speed.
124+
125+
Note that the subtest pattern has gotchas when running tests in parallel:
126+
127+
- The main test function will not wait for the subtest to run if it uses `t.Parallel`. To avoid this, you need to wrap
128+
the parallel subtests in a synchronous, blocking subtest. In the example below, the `group` subtest is synchronous
129+
(no call to `t.Parallel`) and thus the main function will wait until that test finishes. The `group` test does not
130+
finish until all the subtests it spawns are finished, even if they are non-blocking and parallel, and thus the
131+
`tearDownVPC` call does not happen until all subtests are done.
132+
133+
- If you are using table driven tests, the range variable will be updated to the next iteration before it is used within
134+
the subtest. That is, in the example below, if we did not have the `testCase := testCase` line in the range block,
135+
the `testCase` reference used in the subtest after the `t.Parallel` call will correspond to the last `testCase` in the
136+
`testCases` list. To avoid this, we create a new variable in the scope of the range block so that it does not get
137+
updated during the loop.
138+
139+
Example:
140+
141+
```go
142+
func TestECS(t *testing.T) {
143+
t.Parallel()
144+
145+
defer tearDownVPC()
146+
deployVPC()
147+
148+
// Wrap the parallel tests in a synchronous test group to
149+
// ensure that the main test function (the one calling
150+
// `tearDownVPC` and `deployVPC`) waits until all the
151+
// subtests are done before running the deferred function.
152+
t.Run("group", func(t *testing.T) {
153+
for _, testCase := range testCases {
154+
// To avoid the range variable from getting updated in the
155+
// parallel tests, we bind a new name that is within the
156+
// scope of the for block.
157+
testCase := testCase
158+
t.Run(testCase.name, func(t *testing.T) {
159+
t.Parallel()
160+
testCase.testCode()
161+
})
162+
}
163+
})
164+
}
165+
```
166+
167+
### Naming things
168+
169+
Prefer descriptive names over short ones. In particular, avoid one-letter variable names, other than in case of very well
170+
known and widely understood conventions, such as `i` for `index` (e.g. in a loop). A more descriptive name helps with
171+
code understanding and maintenance, at very little cost, given the auto-complete feature in most IDEs and editors.
172+
173+
If a name contains an acronym, only capitalize the first letter of the acronym. E.g. use `someEksCluster` rather than
174+
`someEKSCluster`. We go against the [recommendation](https://github.com/golang/go/wiki/CodeReviewComments#initialisms)
175+
here in order to follow the convention already in use by some third party packages we heavily rely on (e.g. `aws-sdk-go`).
176+
177+
#### Constants
178+
179+
Since many languages use `ALL_CAPS` for constants, it is worth calling out explicitly that
180+
[Effective Go](https://golang.org/doc/effective_go.html#mixed-caps) recommends using `MixedCaps` for all names, including constants.
181+
Therefore, `region` or `testRegion` for private constants and `Region` or `TestRegion` for public ones is preferred over
182+
`REGION` or `TEST_REGION`.
183+
184+
### Functional programming practices
185+
186+
#### Immutability
187+
188+
Prefer returning a new value rather than mutating an existing one.
189+
190+
```go
191+
// Don't do this
192+
var result int = 0
193+
194+
func main() {
195+
add(1, 1, &result)
196+
fmt.Println(result)
197+
}
198+
199+
func add(a, b int, result *int) {
200+
*result = a + b
201+
}
202+
```
203+
204+
```go
205+
// Do this instead
206+
func main() {
207+
fmt.Println(add(1, 1))
208+
}
209+
210+
func add(a, b int) int {
211+
return a + b
212+
}
213+
```
214+
215+
#### Pure functions
216+
217+
Prefer functions that take all their inputs as function parameters, instead of reading those inputs via side effects
218+
(e.g., reading from disk or the network or global vars), and whose entire behavior is to return values
219+
(note: errors are values too!), rather than performing side effects (e.g. by writing to disk or the network or global
220+
vars). Of course, you can’t avoid side effects forever if you want your code to do something useful, but try to do as
221+
much of your logic with pure functions as you can, try to pass everything around as explicit parameters and return
222+
everything as explicit values, and centralize the side effecting code to a few isolated places.
223+
224+
#### Composition
225+
226+
Build your code out of small, reusable, named functions, that you compose together.
227+
228+
```go
229+
// Don't do this
230+
func main() {
231+
fmt.Println(mulOfSums(1, 1))
232+
}
233+
234+
func mulOfSums(a, b int) int {
235+
return (a + b) * (a + b)
236+
}
237+
```
238+
239+
```go
240+
// Do this instead
241+
func main() {
242+
fmt.Println(mul(add(1, 1), add(1, 1)))
243+
}
244+
245+
func add(a, b int) int {
246+
return a + b
247+
}
248+
249+
func mul(a, b int) int {
250+
return a * b
251+
}
252+
```
253+
254+
### Repo-specific conventions
255+
256+
#### terratest
257+
258+
Note the existence of methods in terratest which are suffixed with the letter `E`, e.g.
259+
[GetAccountIdE](https://github.com/gruntwork-io/terratest/blob/master/modules/aws/account.go#L23). Methods that have the
260+
suffix `E` return an error as the last return value; methods without `E` mark the test as failed
261+
(e.g., via calling `t.Fail()`) instead of returning an error.

0 commit comments

Comments
 (0)