|
| 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