├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── src ├── README.md ├── SUMMARY.md ├── atomic.md ├── builtin-name.md ├── channel-size.md ├── consistency.md ├── container-capacity.md ├── container-copy.md ├── decl-group.md ├── defer-clean.md ├── else-unnecessary.md ├── embed-public.md ├── enum-start.md ├── error-name.md ├── error-once.md ├── error-type.md ├── error-wrap.md ├── exit-main.md ├── exit-once.md ├── function-name.md ├── function-order.md ├── functional-option.md ├── global-decl.md ├── global-mut.md ├── global-name.md ├── goroutine-exit.md ├── goroutine-forget.md ├── goroutine-init.md ├── import-alias.md ├── import-group.md ├── init.md ├── interface-compliance.md ├── interface-pointer.md ├── interface-receiver.md ├── intro.md ├── line-length.md ├── lint.md ├── map-init.md ├── mutex-zero-value.md ├── nest-less.md ├── package-name.md ├── panic.md ├── param-naked.md ├── performance.md ├── preface.txt ├── printf-const.md ├── printf-name.md ├── slice-nil.md ├── strconv.md ├── string-byte-slice.md ├── string-escape.md ├── struct-embed.md ├── struct-field-key.md ├── struct-field-zero.md ├── struct-pointer.md ├── struct-tag.md ├── struct-zero.md ├── test-table.md ├── time.md ├── type-assert.md ├── var-decl.md └── var-scope.md └── style.md /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/tools" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request_target: 7 | branches: [ '*' ] 8 | 9 | jobs: 10 | stitchmd: 11 | name: Check or update style.md 12 | runs-on: ubuntu-latest 13 | 14 | # Needed to give the job permission 15 | # to push to branches. 16 | permissions: 17 | contents: write 18 | 19 | steps: 20 | - name: Check out repository 21 | uses: actions/checkout@v4 22 | with: 23 | # Check out the pull request repository and branch. 24 | # If the PR is made from a fork, this will check out the fork. 25 | # This is necessary for git-auto-commit-action to update PRs made by forks. 26 | # See 27 | # https://github.com/stefanzweifel/git-auto-commit-action#use-in-forks-from-public-repositories 28 | repository: ${{ github.event.pull_request.head.repo.full_name }} 29 | ref: ${{ github.head_ref }} 30 | 31 | - name: Check or update style.md 32 | uses: abhinav/stitchmd-action@v1 33 | with: 34 | # For pull requests, run in 'write' mode so that edits made 35 | # directly in the GitHub UI get propagated to style.md 36 | # without a local checkout. 37 | # 38 | # Otherwise, run in 'check' mode to fail CI if style.md is out-of-date. 39 | mode: ${{ github.event_name == 'pull_request_target' && 'write' || 'check' }} 40 | summary: src/SUMMARY.md 41 | preface: src/preface.txt 42 | output: style.md 43 | 44 | - uses: stefanzweifel/git-auto-commit-action@v5 45 | if: ${{ github.event_name == 'pull_request_target' }} 46 | with: 47 | file_pattern: style.md 48 | commit_message: 'Auto-update style.md' 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /bin 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # Refer to golangci-lint's example config file for more options and information: 2 | # https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml 3 | 4 | run: 5 | timeout: 5m 6 | modules-download-mode: readonly 7 | 8 | linters: 9 | enable: 10 | - errcheck 11 | - goimports 12 | - golint 13 | - govet 14 | - staticcheck 15 | 16 | issues: 17 | exclude-use-default: false 18 | max-issues-per-linter: 0 19 | max-same-issues: 0 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2023-05-09 2 | 3 | - Test tables: Discourage tables with complex, branching test bodies. 4 | 5 | # 2023-04-13 6 | 7 | - Errors: Add guidance on handling errors only once. 8 | 9 | # 2023-03-03 10 | 11 | - Receivers and Interfaces: Clarify subtlety with pointer receivers and values. 12 | 13 | # 2022-10-18 14 | 15 | - Add guidance on managing goroutine lifecycles. 16 | 17 | # 2022-03-30 18 | 19 | - Add guidance on using field tags in marshaled structs. 20 | 21 | # 2021-11-16 22 | 23 | - Add guidance on use of `%w` vs `%v` with `fmt.Errorf`, and where to use 24 | `errors.New` or custom error types. 25 | 26 | # 2021-11-12 27 | 28 | - Add soft line length limit of 99 characters. 29 | 30 | # 2021-04-19 31 | 32 | - Programs should exit only in `main()`, preferably at most once. 33 | 34 | # 2021-03-15 35 | 36 | - Add guidance on omitting zero-value fields during struct initialization. 37 | - Add guidance on `Foo{}` versus `var` form for initialization of empty 38 | structs. 39 | - Add new section for Initializing Structs, moving relevant guidances into 40 | subsections of it. 41 | 42 | # 2020-06-10 43 | 44 | - Add guidance on avoiding `init()`. 45 | - Add guidance to avoid using built-in names. 46 | - Add reminder that nil slices are not always the same as empty slices. 47 | 48 | # 2020-02-24 49 | 50 | - Add guidance on verifying interface compliance with compile-time checks. 51 | 52 | # 2020-01-30 53 | 54 | - Recommend using the `time` package when dealing with time. 55 | 56 | # 2020-01-25 57 | 58 | - Add guidance against embedding types in public structs. 59 | 60 | # 2019-12-17 61 | 62 | - Functional Options: Recommend struct implementations of `Option` interface 63 | instead of capturing values with a closure. 64 | 65 | # 2019-11-26 66 | 67 | - Add guidance against mutating global variables. 68 | 69 | # 2019-10-21 70 | 71 | - Add section on remaining consistent with existing practices. 72 | - Add guidance on map initialization and size hints. 73 | 74 | # 2019-10-11 75 | 76 | - Suggest succinct context for error messages. 77 | 78 | # 2019-10-10 79 | 80 | - Initial release. 81 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at oss-conduct@uber.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Before making any changes, 4 | please discuss your plans on GitHub 5 | and get agreement on the general direction of the change. 6 | 7 | ## Making changes 8 | 9 | - style.md is generated from the contents of the src/ folder. 10 | All changes must be made to files in the src/ folder. 11 | - For new entries, create a new file with a short name 12 | (see [File names](#file-names)) and add it to [SUMMARY.md](src/SUMMARY.md). 13 | The file must have a single level 1 heading and any number of subsections. 14 | - Use tables for side-by-side code samples. 15 | - Link to other sections with their file names (`[..](foo.md)`). 16 | 17 | ## Writing style 18 | 19 | ### Line breaks 20 | 21 | Use [semantic line breaks](https://sembr.org/) in your writing. 22 | This keeps the Markdown files easily reviewable and editable. 23 | 24 | ### File names 25 | 26 | Files in src/ follow a rough naming convention of: 27 | 28 | {subject}-{desc}.md 29 | 30 | Where `{subject}` is the **singular form** of subject that the entry is about 31 | (e.g `string`, `struct`, `time`, `var`, `error`) 32 | and `{desc}` is a short one or two word description of the topic. 33 | For subjects where their name is enough, the `-{desc}` may be omitted. 34 | 35 | ### Code samples 36 | 37 | Use two spaces to indent code samples. 38 | Horizontal space is limited in side-by-side samples. 39 | 40 | ### Side-by-side samples 41 | 42 | Create side-by-side code samples with the following template: 43 | 44 | ~~~ 45 |
Bad | Good |
---|---|
49 | 50 | ```go 51 | BAD CODE GOES HERE 52 | ``` 53 | 54 | | 55 | 56 | ```go 57 | GOOD CODE GOES HERE 58 | ``` 59 | 60 | |
Bad | Good |
---|---|
17 | 18 | ```go 19 | type foo struct { 20 | running int32 // atomic 21 | } 22 | 23 | func (f* foo) start() { 24 | if atomic.SwapInt32(&f.running, 1) == 1 { 25 | // already running… 26 | return 27 | } 28 | // start the Foo 29 | } 30 | 31 | func (f *foo) isRunning() bool { 32 | return f.running == 1 // race! 33 | } 34 | ``` 35 | 36 | | 37 | 38 | ```go 39 | type foo struct { 40 | running atomic.Bool 41 | } 42 | 43 | func (f *foo) start() { 44 | if f.running.Swap(true) { 45 | // already running… 46 | return 47 | } 48 | // start the Foo 49 | } 50 | 51 | func (f *foo) isRunning() bool { 52 | return f.running.Load() 53 | } 54 | ``` 55 | 56 | |
Bad | Good |
---|---|
18 | 19 | ```go 20 | var error string 21 | // `error` shadows the builtin 22 | 23 | // or 24 | 25 | func handleErrorMessage(error string) { 26 | // `error` shadows the builtin 27 | } 28 | ``` 29 | 30 | | 31 | 32 | ```go 33 | var errorMessage string 34 | // `error` refers to the builtin 35 | 36 | // or 37 | 38 | func handleErrorMessage(msg string) { 39 | // `error` refers to the builtin 40 | } 41 | ``` 42 | 43 | |
45 | 46 | ```go 47 | type Foo struct { 48 | // While these fields technically don't 49 | // constitute shadowing, grepping for 50 | // `error` or `string` strings is now 51 | // ambiguous. 52 | error error 53 | string string 54 | } 55 | 56 | func (f Foo) Error() error { 57 | // `error` and `f.error` are 58 | // visually similar 59 | return f.error 60 | } 61 | 62 | func (f Foo) String() string { 63 | // `string` and `f.string` are 64 | // visually similar 65 | return f.string 66 | } 67 | ``` 68 | 69 | | 70 | 71 | ```go 72 | type Foo struct { 73 | // `error` and `string` strings are 74 | // now unambiguous. 75 | err error 76 | str string 77 | } 78 | 79 | func (f Foo) Error() error { 80 | return f.err 81 | } 82 | 83 | func (f Foo) String() string { 84 | return f.str 85 | } 86 | ``` 87 | 88 | |
Bad | Good |
---|---|
13 | 14 | ```go 15 | // Ought to be enough for anybody! 16 | c := make(chan int, 64) 17 | ``` 18 | 19 | | 20 | 21 | ```go 22 | // Size of one 23 | c := make(chan int, 1) // or 24 | // Unbuffered channel, size of zero 25 | c := make(chan int) 26 | ``` 27 | 28 | |
Bad | Good |
---|---|
29 | 30 | ```go 31 | m := make(map[string]os.FileInfo) 32 | 33 | files, _ := os.ReadDir("./files") 34 | for _, f := range files { 35 | m[f.Name()] = f 36 | } 37 | ``` 38 | 39 | | 40 | 41 | ```go 42 | 43 | files, _ := os.ReadDir("./files") 44 | 45 | m := make(map[string]os.DirEntry, len(files)) 46 | for _, f := range files { 47 | m[f.Name()] = f 48 | } 49 | ``` 50 | 51 | |
53 | 54 | `m` is created without a size hint; there may be more 55 | allocations at assignment time. 56 | 57 | | 58 | 59 | `m` is created with a size hint; there may be fewer 60 | allocations at assignment time. 61 | 62 | |
Bad | Good |
---|---|
84 | 85 | ```go 86 | for n := 0; n < b.N; n++ { 87 | data := make([]int, 0) 88 | for k := 0; k < size; k++{ 89 | data = append(data, k) 90 | } 91 | } 92 | ``` 93 | 94 | | 95 | 96 | ```go 97 | for n := 0; n < b.N; n++ { 98 | data := make([]int, 0, size) 99 | for k := 0; k < size; k++{ 100 | data = append(data, k) 101 | } 102 | } 103 | ``` 104 | 105 | |
107 | 108 | ```plain 109 | BenchmarkBad-4 100000000 2.48s 110 | ``` 111 | 112 | | 113 | 114 | ```plain 115 | BenchmarkGood-4 100000000 0.21s 116 | ``` 117 | 118 | |
Bad | Good |
---|---|
16 | 17 | ```go 18 | func (d *Driver) SetTrips(trips []Trip) { 19 | d.trips = trips 20 | } 21 | 22 | trips := ... 23 | d1.SetTrips(trips) 24 | 25 | // Did you mean to modify d1.trips? 26 | trips[0] = ... 27 | ``` 28 | 29 | | 30 |31 | 32 | ```go 33 | func (d *Driver) SetTrips(trips []Trip) { 34 | d.trips = make([]Trip, len(trips)) 35 | copy(d.trips, trips) 36 | } 37 | 38 | trips := ... 39 | d1.SetTrips(trips) 40 | 41 | // We can now modify trips[0] without affecting d1.trips. 42 | trips[0] = ... 43 | ``` 44 | 45 | | 46 |
Bad | Good |
---|---|
60 | 61 | ```go 62 | type Stats struct { 63 | mu sync.Mutex 64 | counters map[string]int 65 | } 66 | 67 | // Snapshot returns the current stats. 68 | func (s *Stats) Snapshot() map[string]int { 69 | s.mu.Lock() 70 | defer s.mu.Unlock() 71 | 72 | return s.counters 73 | } 74 | 75 | // snapshot is no longer protected by the mutex, so any 76 | // access to the snapshot is subject to data races. 77 | snapshot := stats.Snapshot() 78 | ``` 79 | 80 | | 81 | 82 | ```go 83 | type Stats struct { 84 | mu sync.Mutex 85 | counters map[string]int 86 | } 87 | 88 | func (s *Stats) Snapshot() map[string]int { 89 | s.mu.Lock() 90 | defer s.mu.Unlock() 91 | 92 | result := make(map[string]int, len(s.counters)) 93 | for k, v := range s.counters { 94 | result[k] = v 95 | } 96 | return result 97 | } 98 | 99 | // Snapshot is now a copy. 100 | snapshot := stats.Snapshot() 101 | ``` 102 | 103 | |
Bad | Good |
---|---|
9 | 10 | ```go 11 | import "a" 12 | import "b" 13 | ``` 14 | 15 | | 16 | 17 | ```go 18 | import ( 19 | "a" 20 | "b" 21 | ) 22 | ``` 23 | 24 | |
Bad | Good |
---|---|
33 | 34 | ```go 35 | 36 | const a = 1 37 | const b = 2 38 | 39 | 40 | 41 | var a = 1 42 | var b = 2 43 | 44 | 45 | 46 | type Area float64 47 | type Volume float64 48 | ``` 49 | 50 | | 51 | 52 | ```go 53 | const ( 54 | a = 1 55 | b = 2 56 | ) 57 | 58 | var ( 59 | a = 1 60 | b = 2 61 | ) 62 | 63 | type ( 64 | Area float64 65 | Volume float64 66 | ) 67 | ``` 68 | 69 | |
Bad | Good |
---|---|
78 | 79 | ```go 80 | type Operation int 81 | 82 | const ( 83 | Add Operation = iota + 1 84 | Subtract 85 | Multiply 86 | EnvVar = "MY_ENV" 87 | ) 88 | ``` 89 | 90 | | 91 | 92 | ```go 93 | type Operation int 94 | 95 | const ( 96 | Add Operation = iota + 1 97 | Subtract 98 | Multiply 99 | ) 100 | 101 | const EnvVar = "MY_ENV" 102 | ``` 103 | 104 | |
Bad | Good |
---|---|
114 | 115 | ```go 116 | func f() string { 117 | red := color.New(0xff0000) 118 | green := color.New(0x00ff00) 119 | blue := color.New(0x0000ff) 120 | 121 | // ... 122 | } 123 | ``` 124 | 125 | | 126 | 127 | ```go 128 | func f() string { 129 | var ( 130 | red = color.New(0xff0000) 131 | green = color.New(0x00ff00) 132 | blue = color.New(0x0000ff) 133 | ) 134 | 135 | // ... 136 | } 137 | ``` 138 | 139 | |
Bad | Good |
---|---|
150 | 151 | ```go 152 | func (c *client) request() { 153 | caller := c.name 154 | format := "json" 155 | timeout := 5*time.Second 156 | var err error 157 | 158 | // ... 159 | } 160 | ``` 161 | 162 | | 163 | 164 | ```go 165 | func (c *client) request() { 166 | var ( 167 | caller = c.name 168 | format = "json" 169 | timeout = 5*time.Second 170 | err error 171 | ) 172 | 173 | // ... 174 | } 175 | ``` 176 | 177 | |
Bad | Good |
---|---|
9 | 10 | ```go 11 | p.Lock() 12 | if p.count < 10 { 13 | p.Unlock() 14 | return p.count 15 | } 16 | 17 | p.count++ 18 | newCount := p.count 19 | p.Unlock() 20 | 21 | return newCount 22 | 23 | // easy to miss unlocks due to multiple returns 24 | ``` 25 | 26 | | 27 | 28 | ```go 29 | p.Lock() 30 | defer p.Unlock() 31 | 32 | if p.count < 10 { 33 | return p.count 34 | } 35 | 36 | p.count++ 37 | return p.count 38 | 39 | // more readable 40 | ``` 41 | 42 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | var a int 13 | if b { 14 | a = 100 15 | } else { 16 | a = 10 17 | } 18 | ``` 19 | 20 | | 21 | 22 | ```go 23 | a := 10 24 | if b { 25 | a = 100 26 | } 27 | ``` 28 | 29 | |
Bad | Good |
---|---|
30 | 31 | ```go 32 | // ConcreteList is a list of entities. 33 | type ConcreteList struct { 34 | *AbstractList 35 | } 36 | ``` 37 | 38 | | 39 | 40 | ```go 41 | // ConcreteList is a list of entities. 42 | type ConcreteList struct { 43 | list *AbstractList 44 | } 45 | 46 | // Add adds an entity to the list. 47 | func (l *ConcreteList) Add(e Entity) { 48 | l.list.Add(e) 49 | } 50 | 51 | // Remove removes an entity from the list. 52 | func (l *ConcreteList) Remove(e Entity) { 53 | l.list.Remove(e) 54 | } 55 | ``` 56 | 57 | |
Bad | Good |
---|---|
83 | 84 | ```go 85 | // AbstractList is a generalized implementation 86 | // for various kinds of lists of entities. 87 | type AbstractList interface { 88 | Add(Entity) 89 | Remove(Entity) 90 | } 91 | 92 | // ConcreteList is a list of entities. 93 | type ConcreteList struct { 94 | AbstractList 95 | } 96 | ``` 97 | 98 | | 99 | 100 | ```go 101 | // ConcreteList is a list of entities. 102 | type ConcreteList struct { 103 | list AbstractList 104 | } 105 | 106 | // Add adds an entity to the list. 107 | func (l *ConcreteList) Add(e Entity) { 108 | l.list.Add(e) 109 | } 110 | 111 | // Remove removes an entity from the list. 112 | func (l *ConcreteList) Remove(e Entity) { 113 | l.list.Remove(e) 114 | } 115 | ``` 116 | 117 | |
Bad | Good |
---|---|
11 | 12 | ```go 13 | type Operation int 14 | 15 | const ( 16 | Add Operation = iota 17 | Subtract 18 | Multiply 19 | ) 20 | 21 | // Add=0, Subtract=1, Multiply=2 22 | ``` 23 | 24 | | 25 | 26 | ```go 27 | type Operation int 28 | 29 | const ( 30 | Add Operation = iota + 1 31 | Subtract 32 | Multiply 33 | ) 34 | 35 | // Add=1, Subtract=2, Multiply=3 36 | ``` 37 | 38 | |
Description | Code |
---|---|
29 | 30 | **Bad**: Log the error and return it 31 | 32 | Callers further up the stack will likely take a similar action with the error. 33 | Doing so causing a lot of noise in the application logs for little value. 34 | 35 | | 36 | 37 | ```go 38 | u, err := getUser(id) 39 | if err != nil { 40 | // BAD: See description 41 | log.Printf("Could not get user %q: %v", id, err) 42 | return err 43 | } 44 | ``` 45 | 46 | |
48 | 49 | **Good**: Wrap the error and return it 50 | 51 | Callers further up the stack will handle the error. 52 | Use of `%w` ensures they can match the error with `errors.Is` or `errors.As` 53 | if relevant. 54 | 55 | | 56 | 57 | ```go 58 | u, err := getUser(id) 59 | if err != nil { 60 | return fmt.Errorf("get user %q: %w", id, err) 61 | } 62 | ``` 63 | 64 | |
66 | 67 | **Good**: Log the error and degrade gracefully 68 | 69 | If the operation isn't strictly necessary, 70 | we can provide a degraded but unbroken experience 71 | by recovering from it. 72 | 73 | | 74 | 75 | ```go 76 | if err := emitMetrics(); err != nil { 77 | // Failure to write metrics should not 78 | // break the application. 79 | log.Printf("Could not emit metrics: %v", err) 80 | } 81 | 82 | ``` 83 | 84 | |
86 | 87 | **Good**: Match the error and degrade gracefully 88 | 89 | If the callee defines a specific error in its contract, 90 | and the failure is recoverable, 91 | match on that error case and degrade gracefully. 92 | For all other cases, wrap the error and return it. 93 | 94 | Callers further up the stack will handle other errors. 95 | 96 | | 97 | 98 | ```go 99 | tz, err := getUserTimeZone(id) 100 | if err != nil { 101 | if errors.Is(err, ErrUserNotFound) { 102 | // User doesn't exist. Use UTC. 103 | tz = time.UTC 104 | } else { 105 | return fmt.Errorf("get user %q: %w", id, err) 106 | } 107 | } 108 | ``` 109 | 110 | |
No error matching | Error matching |
---|---|
38 | 39 | ```go 40 | // package foo 41 | 42 | func Open() error { 43 | return errors.New("could not open") 44 | } 45 | 46 | // package bar 47 | 48 | if err := foo.Open(); err != nil { 49 | // Can't handle the error. 50 | panic("unknown error") 51 | } 52 | ``` 53 | 54 | | 55 | 56 | ```go 57 | // package foo 58 | 59 | var ErrCouldNotOpen = errors.New("could not open") 60 | 61 | func Open() error { 62 | return ErrCouldNotOpen 63 | } 64 | 65 | // package bar 66 | 67 | if err := foo.Open(); err != nil { 68 | if errors.Is(err, foo.ErrCouldNotOpen) { 69 | // handle the error 70 | } else { 71 | panic("unknown error") 72 | } 73 | } 74 | ``` 75 | 76 | |
No error matching | Error matching |
---|---|
87 | 88 | ```go 89 | // package foo 90 | 91 | func Open(file string) error { 92 | return fmt.Errorf("file %q not found", file) 93 | } 94 | 95 | // package bar 96 | 97 | if err := foo.Open("testfile.txt"); err != nil { 98 | // Can't handle the error. 99 | panic("unknown error") 100 | } 101 | ``` 102 | 103 | | 104 | 105 | ```go 106 | // package foo 107 | 108 | type NotFoundError struct { 109 | File string 110 | } 111 | 112 | func (e *NotFoundError) Error() string { 113 | return fmt.Sprintf("file %q not found", e.File) 114 | } 115 | 116 | func Open(file string) error { 117 | return &NotFoundError{File: file} 118 | } 119 | 120 | 121 | // package bar 122 | 123 | if err := foo.Open("testfile.txt"); err != nil { 124 | var notFound *NotFoundError 125 | if errors.As(err, ¬Found) { 126 | // handle the error 127 | } else { 128 | panic("unknown error") 129 | } 130 | } 131 | ``` 132 | 133 | |
Bad | Good |
---|---|
40 | 41 | ```go 42 | s, err := store.New() 43 | if err != nil { 44 | return fmt.Errorf( 45 | "failed to create new store: %w", err) 46 | } 47 | ``` 48 | 49 | | 50 | 51 | ```go 52 | s, err := store.New() 53 | if err != nil { 54 | return fmt.Errorf( 55 | "new store: %w", err) 56 | } 57 | ``` 58 | 59 | |
60 | 61 | ```plain 62 | failed to x: failed to y: failed to create new store: the error 63 | ``` 64 | 65 | | 66 | 67 | ```plain 68 | x: y: new store: the error 69 | ``` 70 | 71 | |
Bad | Good |
---|---|
16 | 17 | ```go 18 | func main() { 19 | body := readFile(path) 20 | fmt.Println(body) 21 | } 22 | 23 | func readFile(path string) string { 24 | f, err := os.Open(path) 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | 29 | b, err := io.ReadAll(f) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | 34 | return string(b) 35 | } 36 | ``` 37 | 38 | | 39 | 40 | ```go 41 | func main() { 42 | body, err := readFile(path) 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | fmt.Println(body) 47 | } 48 | 49 | func readFile(path string) (string, error) { 50 | f, err := os.Open(path) 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | b, err := io.ReadAll(f) 56 | if err != nil { 57 | return "", err 58 | } 59 | 60 | return string(b), nil 61 | } 62 | ``` 63 | 64 | |
Bad | Good |
---|---|
14 | 15 | ```go 16 | package main 17 | 18 | func main() { 19 | args := os.Args[1:] 20 | if len(args) != 1 { 21 | log.Fatal("missing file") 22 | } 23 | name := args[0] 24 | 25 | f, err := os.Open(name) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | defer f.Close() 30 | 31 | // If we call log.Fatal after this line, 32 | // f.Close will not be called. 33 | 34 | b, err := io.ReadAll(f) 35 | if err != nil { 36 | log.Fatal(err) 37 | } 38 | 39 | // ... 40 | } 41 | ``` 42 | 43 | | 44 | 45 | ```go 46 | package main 47 | 48 | func main() { 49 | if err := run(); err != nil { 50 | log.Fatal(err) 51 | } 52 | } 53 | 54 | func run() error { 55 | args := os.Args[1:] 56 | if len(args) != 1 { 57 | return errors.New("missing file") 58 | } 59 | name := args[0] 60 | 61 | f, err := os.Open(name) 62 | if err != nil { 63 | return err 64 | } 65 | defer f.Close() 66 | 67 | b, err := io.ReadAll(f) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | // ... 73 | } 74 | ``` 75 | 76 | |
Bad | Good |
---|---|
19 | 20 | ```go 21 | func (s *something) Cost() { 22 | return calcCost(s.weights) 23 | } 24 | 25 | type something struct{ ... } 26 | 27 | func calcCost(n []int) int {...} 28 | 29 | func (s *something) Stop() {...} 30 | 31 | func newSomething() *something { 32 | return &something{} 33 | } 34 | ``` 35 | 36 | | 37 | 38 | ```go 39 | type something struct{ ... } 40 | 41 | func newSomething() *something { 42 | return &something{} 43 | } 44 | 45 | func (s *something) Cost() { 46 | return calcCost(s.weights) 47 | } 48 | 49 | func (s *something) Stop() {...} 50 | 51 | func calcCost(n []int) int {...} 52 | ``` 53 | 54 | |
Bad | Good |
---|---|
16 | 17 | ```go 18 | // package db 19 | 20 | func Open( 21 | addr string, 22 | cache bool, 23 | logger *zap.Logger 24 | ) (*Connection, error) { 25 | // ... 26 | } 27 | ``` 28 | 29 | | 30 | 31 | ```go 32 | // package db 33 | 34 | type Option interface { 35 | // ... 36 | } 37 | 38 | func WithCache(c bool) Option { 39 | // ... 40 | } 41 | 42 | func WithLogger(log *zap.Logger) Option { 43 | // ... 44 | } 45 | 46 | // Open creates a connection. 47 | func Open( 48 | addr string, 49 | opts ...Option, 50 | ) (*Connection, error) { 51 | // ... 52 | } 53 | ``` 54 | 55 | |
57 | 58 | The cache and logger parameters must always be provided, even if the user 59 | wants to use the default. 60 | 61 | ```go 62 | db.Open(addr, db.DefaultCache, zap.NewNop()) 63 | db.Open(addr, db.DefaultCache, log) 64 | db.Open(addr, false /* cache */, zap.NewNop()) 65 | db.Open(addr, false /* cache */, log) 66 | ``` 67 | 68 | | 69 | 70 | Options are provided only if needed. 71 | 72 | ```go 73 | db.Open(addr) 74 | db.Open(addr, db.WithLogger(log)) 75 | db.Open(addr, db.WithCache(false)) 76 | db.Open( 77 | addr, 78 | db.WithCache(false), 79 | db.WithLogger(log), 80 | ) 81 | ``` 82 | 83 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | var _s string = F() 13 | 14 | func F() string { return "A" } 15 | ``` 16 | 17 | | 18 | 19 | ```go 20 | var _s = F() 21 | // Since F already states that it returns a string, we don't need to specify 22 | // the type again. 23 | 24 | func F() string { return "A" } 25 | ``` 26 | 27 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | // sign.go 13 | 14 | var _timeNow = time.Now 15 | 16 | func sign(msg string) string { 17 | now := _timeNow() 18 | return signWithTime(msg, now) 19 | } 20 | ``` 21 | 22 | | 23 | 24 | ```go 25 | // sign.go 26 | 27 | type signer struct { 28 | now func() time.Time 29 | } 30 | 31 | func newSigner() *signer { 32 | return &signer{ 33 | now: time.Now, 34 | } 35 | } 36 | 37 | func (s *signer) Sign(msg string) string { 38 | now := s.now() 39 | return signWithTime(msg, now) 40 | } 41 | ``` 42 | 43 | |
45 | 46 | ```go 47 | // sign_test.go 48 | 49 | func TestSign(t *testing.T) { 50 | oldTimeNow := _timeNow 51 | _timeNow = func() time.Time { 52 | return someFixedTime 53 | } 54 | defer func() { _timeNow = oldTimeNow }() 55 | 56 | assert.Equal(t, want, sign(give)) 57 | } 58 | ``` 59 | 60 | | 61 | 62 | ```go 63 | // sign_test.go 64 | 65 | func TestSigner(t *testing.T) { 66 | s := newSigner() 67 | s.now = func() time.Time { 68 | return someFixedTime 69 | } 70 | 71 | assert.Equal(t, want, s.Sign(give)) 72 | } 73 | ``` 74 | 75 | |
Bad | Good |
---|---|
14 | 15 | ```go 16 | // foo.go 17 | 18 | const ( 19 | defaultPort = 8080 20 | defaultUser = "user" 21 | ) 22 | 23 | // bar.go 24 | 25 | func Bar() { 26 | defaultPort := 9090 27 | ... 28 | fmt.Println("Default port", defaultPort) 29 | 30 | // We will not see a compile error if the first line of 31 | // Bar() is deleted. 32 | } 33 | ``` 34 | 35 | | 36 | 37 | ```go 38 | // foo.go 39 | 40 | const ( 41 | _defaultPort = 8080 42 | _defaultUser = "user" 43 | ) 44 | ``` 45 | 46 | |
Bad | Good |
---|---|
30 | 31 | ```go 32 | go func() { 33 | for { 34 | flush() 35 | time.Sleep(delay) 36 | } 37 | }() 38 | ``` 39 | 40 | | 41 | 42 | ```go 43 | var ( 44 | stop = make(chan struct{}) // tells the goroutine to stop 45 | done = make(chan struct{}) // tells us that the goroutine exited 46 | ) 47 | go func() { 48 | defer close(done) 49 | 50 | ticker := time.NewTicker(delay) 51 | defer ticker.Stop() 52 | for { 53 | select { 54 | case <-ticker.C: 55 | flush() 56 | case <-stop: 57 | return 58 | } 59 | } 60 | }() 61 | 62 | // Elsewhere... 63 | close(stop) // signal the goroutine to stop 64 | <-done // and wait for it to exit 65 | ``` 66 | 67 | |
69 | 70 | There's no way to stop this goroutine. 71 | This will run until the application exits. 72 | 73 | | 74 | 75 | This goroutine can be stopped with `close(stop)`, 76 | and we can wait for it to exit with `<-done`. 77 | 78 | |
Bad | Good |
---|---|
16 | 17 | ```go 18 | func init() { 19 | go doWork() 20 | } 21 | 22 | func doWork() { 23 | for { 24 | // ... 25 | } 26 | } 27 | ``` 28 | 29 | | 30 | 31 | ```go 32 | type Worker struct{ /* ... */ } 33 | 34 | func NewWorker(...) *Worker { 35 | w := &Worker{ 36 | stop: make(chan struct{}), 37 | done: make(chan struct{}), 38 | // ... 39 | } 40 | go w.doWork() 41 | } 42 | 43 | func (w *Worker) doWork() { 44 | defer close(w.done) 45 | for { 46 | // ... 47 | case <-w.stop: 48 | return 49 | } 50 | } 51 | 52 | // Shutdown tells the worker to stop 53 | // and waits until it has finished. 54 | func (w *Worker) Shutdown() { 55 | close(w.stop) 56 | <-w.done 57 | } 58 | ``` 59 | 60 | |
62 | 63 | Spawns a background goroutine unconditionally when the user exports this package. 64 | The user has no control over the goroutine or a means of stopping it. 65 | 66 | | 67 | 68 | Spawns the worker only if the user requests it. 69 | Provides a means of shutting down the worker so that the user can free up 70 | resources used by the worker. 71 | 72 | Note that you should use `WaitGroup`s if the worker manages multiple 73 | goroutines. 74 | See [Wait for goroutines to exit](goroutine-exit.md). 75 | 76 | |
Bad | Good |
---|---|
22 | 23 | ```go 24 | import ( 25 | "fmt" 26 | "os" 27 | runtimetrace "runtime/trace" 28 | 29 | nettrace "golang.net/x/trace" 30 | ) 31 | ``` 32 | 33 | | 34 | 35 | ```go 36 | import ( 37 | "fmt" 38 | "os" 39 | "runtime/trace" 40 | 41 | nettrace "golang.net/x/trace" 42 | ) 43 | ``` 44 | 45 | |
Bad | Good |
---|---|
14 | 15 | ```go 16 | import ( 17 | "fmt" 18 | "os" 19 | "go.uber.org/atomic" 20 | "golang.org/x/sync/errgroup" 21 | ) 22 | ``` 23 | 24 | | 25 | 26 | ```go 27 | import ( 28 | "fmt" 29 | "os" 30 | 31 | "go.uber.org/atomic" 32 | "golang.org/x/sync/errgroup" 33 | ) 34 | ``` 35 | 36 | |
Bad | Good |
---|---|
26 | 27 | ```go 28 | type Foo struct { 29 | // ... 30 | } 31 | 32 | var _defaultFoo Foo 33 | 34 | func init() { 35 | _defaultFoo = Foo{ 36 | // ... 37 | } 38 | } 39 | ``` 40 | 41 | | 42 | 43 | ```go 44 | var _defaultFoo = Foo{ 45 | // ... 46 | } 47 | 48 | // or, better, for testability: 49 | 50 | var _defaultFoo = defaultFoo() 51 | 52 | func defaultFoo() Foo { 53 | return Foo{ 54 | // ... 55 | } 56 | } 57 | ``` 58 | 59 | |
61 | 62 | ```go 63 | type Config struct { 64 | // ... 65 | } 66 | 67 | var _config Config 68 | 69 | func init() { 70 | // Bad: based on current directory 71 | cwd, _ := os.Getwd() 72 | 73 | // Bad: I/O 74 | raw, _ := os.ReadFile( 75 | path.Join(cwd, "config", "config.yaml"), 76 | ) 77 | 78 | yaml.Unmarshal(raw, &_config) 79 | } 80 | ``` 81 | 82 | | 83 | 84 | ```go 85 | type Config struct { 86 | // ... 87 | } 88 | 89 | func loadConfig() Config { 90 | cwd, err := os.Getwd() 91 | // handle err 92 | 93 | raw, err := os.ReadFile( 94 | path.Join(cwd, "config", "config.yaml"), 95 | ) 96 | // handle err 97 | 98 | var config Config 99 | yaml.Unmarshal(raw, &config) 100 | 101 | return config 102 | } 103 | ``` 104 | 105 | |
Bad | Good |
---|---|
15 | 16 | ```go 17 | type Handler struct { 18 | // ... 19 | } 20 | 21 | 22 | 23 | func (h *Handler) ServeHTTP( 24 | w http.ResponseWriter, 25 | r *http.Request, 26 | ) { 27 | ... 28 | } 29 | ``` 30 | 31 | | 32 | 33 | ```go 34 | type Handler struct { 35 | // ... 36 | } 37 | 38 | var _ http.Handler = (*Handler)(nil) 39 | 40 | func (h *Handler) ServeHTTP( 41 | w http.ResponseWriter, 42 | r *http.Request, 43 | ) { 44 | // ... 45 | } 46 | ``` 47 | 48 | |
Bad | Good |
---|---|
12 | 13 | ```go 14 | var ( 15 | // m1 is safe to read and write; 16 | // m2 will panic on writes. 17 | m1 = map[T1]T2{} 18 | m2 map[T1]T2 19 | ) 20 | ``` 21 | 22 | | 23 | 24 | ```go 25 | var ( 26 | // m1 is safe to read and write; 27 | // m2 will panic on writes. 28 | m1 = make(map[T1]T2) 29 | m2 map[T1]T2 30 | ) 31 | ``` 32 | 33 | |
35 | 36 | Declaration and initialization are visually similar. 37 | 38 | | 39 | 40 | Declaration and initialization are visually distinct. 41 | 42 | |
Bad | Good |
---|---|
57 | 58 | ```go 59 | m := make(map[T1]T2, 3) 60 | m[k1] = v1 61 | m[k2] = v2 62 | m[k3] = v3 63 | ``` 64 | 65 | | 66 | 67 | ```go 68 | m := map[T1]T2{ 69 | k1: v1, 70 | k2: v2, 71 | k3: v3, 72 | } 73 | ``` 74 | 75 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | mu := new(sync.Mutex) 13 | mu.Lock() 14 | ``` 15 | 16 | | 17 | 18 | ```go 19 | var mu sync.Mutex 20 | mu.Lock() 21 | ``` 22 | 23 | |
Bad | Good |
---|---|
33 | 34 | ```go 35 | type SMap struct { 36 | sync.Mutex 37 | 38 | data map[string]string 39 | } 40 | 41 | func NewSMap() *SMap { 42 | return &SMap{ 43 | data: make(map[string]string), 44 | } 45 | } 46 | 47 | func (m *SMap) Get(k string) string { 48 | m.Lock() 49 | defer m.Unlock() 50 | 51 | return m.data[k] 52 | } 53 | ``` 54 | 55 | | 56 | 57 | ```go 58 | type SMap struct { 59 | mu sync.Mutex 60 | 61 | data map[string]string 62 | } 63 | 64 | func NewSMap() *SMap { 65 | return &SMap{ 66 | data: make(map[string]string), 67 | } 68 | } 69 | 70 | func (m *SMap) Get(k string) string { 71 | m.mu.Lock() 72 | defer m.mu.Unlock() 73 | 74 | return m.data[k] 75 | } 76 | ``` 77 | 78 | |
81 | 82 | The `Mutex` field, and the `Lock` and `Unlock` methods are unintentionally part 83 | of the exported API of `SMap`. 84 | 85 | | 86 | 87 | The mutex and its methods are implementation details of `SMap` hidden from its 88 | callers. 89 | 90 | |
Bad | Good |
---|---|
11 | 12 | ```go 13 | for _, v := range data { 14 | if v.F1 == 1 { 15 | v = process(v) 16 | if err := v.Call(); err == nil { 17 | v.Send() 18 | } else { 19 | return err 20 | } 21 | } else { 22 | log.Printf("Invalid v: %v", v) 23 | } 24 | } 25 | ``` 26 | 27 | | 28 | 29 | ```go 30 | for _, v := range data { 31 | if v.F1 != 1 { 32 | log.Printf("Invalid v: %v", v) 33 | continue 34 | } 35 | 36 | v = process(v) 37 | if err := v.Call(); err != nil { 38 | return err 39 | } 40 | v.Send() 41 | } 42 | ``` 43 | 44 | |
Bad | Good |
---|---|
13 | 14 | ```go 15 | func run(args []string) { 16 | if len(args) == 0 { 17 | panic("an argument is required") 18 | } 19 | // ... 20 | } 21 | 22 | func main() { 23 | run(os.Args[1:]) 24 | } 25 | ``` 26 | 27 | | 28 | 29 | ```go 30 | func run(args []string) error { 31 | if len(args) == 0 { 32 | return errors.New("an argument is required") 33 | } 34 | // ... 35 | return nil 36 | } 37 | 38 | func main() { 39 | if err := run(os.Args[1:]); err != nil { 40 | fmt.Fprintln(os.Stderr, err) 41 | os.Exit(1) 42 | } 43 | } 44 | ``` 45 | 46 | |
Bad | Good |
---|---|
65 | 66 | ```go 67 | // func TestFoo(t *testing.T) 68 | 69 | f, err := os.CreateTemp("", "test") 70 | if err != nil { 71 | panic("failed to set up test") 72 | } 73 | ``` 74 | 75 | | 76 | 77 | ```go 78 | // func TestFoo(t *testing.T) 79 | 80 | f, err := os.CreateTemp("", "test") 81 | if err != nil { 82 | t.Fatal("failed to set up test") 83 | } 84 | ``` 85 | 86 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | // func printInfo(name string, isLocal, done bool) 13 | 14 | printInfo("foo", true, true) 15 | ``` 16 | 17 | | 18 | 19 | ```go 20 | // func printInfo(name string, isLocal, done bool) 21 | 22 | printInfo("foo", true /* isLocal */, true /* done */) 23 | ``` 24 | 25 | |
Bad | Good |
---|---|
12 | 13 | ```go 14 | msg := "unexpected values %v, %v\n" 15 | fmt.Printf(msg, 1, 2) 16 | ``` 17 | 18 | | 19 | 20 | ```go 21 | const msg = "unexpected values %v, %v\n" 22 | fmt.Printf(msg, 1, 2) 23 | ``` 24 | 25 | |
Bad | Good |
---|---|
12 | 13 | ```go 14 | if x == "" { 15 | return []int{} 16 | } 17 | ``` 18 | 19 | | 20 | 21 | ```go 22 | if x == "" { 23 | return nil 24 | } 25 | ``` 26 | 27 | |
Bad | Good |
---|---|
37 | 38 | ```go 39 | func isEmpty(s []string) bool { 40 | return s == nil 41 | } 42 | ``` 43 | 44 | | 45 | 46 | ```go 47 | func isEmpty(s []string) bool { 48 | return len(s) == 0 49 | } 50 | ``` 51 | 52 | |
Bad | Good |
---|---|
62 | 63 | ```go 64 | nums := []int{} 65 | // or, nums := make([]int) 66 | 67 | if add1 { 68 | nums = append(nums, 1) 69 | } 70 | 71 | if add2 { 72 | nums = append(nums, 2) 73 | } 74 | ``` 75 | 76 | | 77 | 78 | ```go 79 | var nums []int 80 | 81 | if add1 { 82 | nums = append(nums, 1) 83 | } 84 | 85 | if add2 { 86 | nums = append(nums, 2) 87 | } 88 | ``` 89 | 90 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | for i := 0; i < b.N; i++ { 13 | s := fmt.Sprint(rand.Int()) 14 | } 15 | ``` 16 | 17 | | 18 | 19 | ```go 20 | for i := 0; i < b.N; i++ { 21 | s := strconv.Itoa(rand.Int()) 22 | } 23 | ``` 24 | 25 | |
27 | 28 | ```plain 29 | BenchmarkFmtSprint-4 143 ns/op 2 allocs/op 30 | ``` 31 | 32 | | 33 | 34 | ```plain 35 | BenchmarkStrconv-4 64.2 ns/op 1 allocs/op 36 | ``` 37 | 38 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | for i := 0; i < b.N; i++ { 13 | w.Write([]byte("Hello world")) 14 | } 15 | ``` 16 | 17 | | 18 | 19 | ```go 20 | data := []byte("Hello world") 21 | for i := 0; i < b.N; i++ { 22 | w.Write(data) 23 | } 24 | ``` 25 | 26 | |
28 | 29 | ```plain 30 | BenchmarkBad-4 50000000 22.2 ns/op 31 | ``` 32 | 33 | | 34 | 35 | ```plain 36 | BenchmarkGood-4 500000000 3.25 ns/op 37 | ``` 38 | 39 | |
Bad | Good |
---|---|
11 | 12 | ```go 13 | wantError := "unknown name:\"test\"" 14 | ``` 15 | 16 | | 17 | 18 | ```go 19 | wantError := `unknown error:"test"` 20 | ``` 21 | 22 | |
Bad | Good |
---|---|
11 | 12 | ```go 13 | type Client struct { 14 | version int 15 | http.Client 16 | } 17 | ``` 18 | 19 | | 20 | 21 | ```go 22 | type Client struct { 23 | http.Client 24 | 25 | version int 26 | } 27 | ``` 28 | 29 | |
Bad | Good |
---|---|
64 | 65 | ```go 66 | type A struct { 67 | // Bad: A.Lock() and A.Unlock() are 68 | // now available, provide no 69 | // functional benefit, and allow 70 | // users to control details about 71 | // the internals of A. 72 | sync.Mutex 73 | } 74 | ``` 75 | 76 | | 77 | 78 | ```go 79 | type countingWriteCloser struct { 80 | // Good: Write() is provided at this 81 | // outer layer for a specific 82 | // purpose, and delegates work 83 | // to the inner type's Write(). 84 | io.WriteCloser 85 | 86 | count int 87 | } 88 | 89 | func (w *countingWriteCloser) Write(bs []byte) (int, error) { 90 | w.count += len(bs) 91 | return w.WriteCloser.Write(bs) 92 | } 93 | ``` 94 | 95 | |
97 | 98 | ```go 99 | type Book struct { 100 | // Bad: pointer changes zero value usefulness 101 | io.ReadWriter 102 | 103 | // other fields 104 | } 105 | 106 | // later 107 | 108 | var b Book 109 | b.Read(...) // panic: nil pointer 110 | b.String() // panic: nil pointer 111 | b.Write(...) // panic: nil pointer 112 | ``` 113 | 114 | | 115 | 116 | ```go 117 | type Book struct { 118 | // Good: has useful zero value 119 | bytes.Buffer 120 | 121 | // other fields 122 | } 123 | 124 | // later 125 | 126 | var b Book 127 | b.Read(...) // ok 128 | b.String() // ok 129 | b.Write(...) // ok 130 | ``` 131 | 132 | |
134 | 135 | ```go 136 | type Client struct { 137 | sync.Mutex 138 | sync.WaitGroup 139 | bytes.Buffer 140 | url.URL 141 | } 142 | ``` 143 | 144 | | 145 | 146 | ```go 147 | type Client struct { 148 | mtx sync.Mutex 149 | wg sync.WaitGroup 150 | buf bytes.Buffer 151 | url url.URL 152 | } 153 | ``` 154 | 155 | |
Bad | Good |
---|---|
12 | 13 | ```go 14 | k := User{"John", "Doe", true} 15 | ``` 16 | 17 | | 18 | 19 | ```go 20 | k := User{ 21 | FirstName: "John", 22 | LastName: "Doe", 23 | Admin: true, 24 | } 25 | ``` 26 | 27 | |
Bad | Good |
---|---|
11 | 12 | ```go 13 | user := User{ 14 | FirstName: "John", 15 | LastName: "Doe", 16 | MiddleName: "", 17 | Admin: false, 18 | } 19 | ``` 20 | 21 | | 22 | 23 | ```go 24 | user := User{ 25 | FirstName: "John", 26 | LastName: "Doe", 27 | } 28 | ``` 29 | 30 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | sval := T{Name: "foo"} 13 | 14 | // inconsistent 15 | sptr := new(T) 16 | sptr.Name = "bar" 17 | ``` 18 | 19 | | 20 | 21 | ```go 22 | sval := T{Name: "foo"} 23 | 24 | sptr := &T{Name: "bar"} 25 | ``` 26 | 27 | |
Bad | Good |
---|---|
11 | 12 | ```go 13 | type Stock struct { 14 | Price int 15 | Name string 16 | } 17 | 18 | bytes, err := json.Marshal(Stock{ 19 | Price: 137, 20 | Name: "UBER", 21 | }) 22 | ``` 23 | 24 | | 25 | 26 | ```go 27 | type Stock struct { 28 | Price int `json:"price"` 29 | Name string `json:"name"` 30 | // Safe to rename Name to Symbol. 31 | } 32 | 33 | bytes, err := json.Marshal(Stock{ 34 | Price: 137, 35 | Name: "UBER", 36 | }) 37 | ``` 38 | 39 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | user := User{} 13 | ``` 14 | 15 | | 16 | 17 | ```go 18 | var user User 19 | ``` 20 | 21 | |
Bad | Good |
---|---|
16 | 17 | ```go 18 | // func TestSplitHostPort(t *testing.T) 19 | 20 | host, port, err := net.SplitHostPort("192.0.2.0:8000") 21 | require.NoError(t, err) 22 | assert.Equal(t, "192.0.2.0", host) 23 | assert.Equal(t, "8000", port) 24 | 25 | host, port, err = net.SplitHostPort("192.0.2.0:http") 26 | require.NoError(t, err) 27 | assert.Equal(t, "192.0.2.0", host) 28 | assert.Equal(t, "http", port) 29 | 30 | host, port, err = net.SplitHostPort(":8000") 31 | require.NoError(t, err) 32 | assert.Equal(t, "", host) 33 | assert.Equal(t, "8000", port) 34 | 35 | host, port, err = net.SplitHostPort("1:8") 36 | require.NoError(t, err) 37 | assert.Equal(t, "1", host) 38 | assert.Equal(t, "8", port) 39 | ``` 40 | 41 | | 42 | 43 | ```go 44 | // func TestSplitHostPort(t *testing.T) 45 | 46 | tests := []struct{ 47 | give string 48 | wantHost string 49 | wantPort string 50 | }{ 51 | { 52 | give: "192.0.2.0:8000", 53 | wantHost: "192.0.2.0", 54 | wantPort: "8000", 55 | }, 56 | { 57 | give: "192.0.2.0:http", 58 | wantHost: "192.0.2.0", 59 | wantPort: "http", 60 | }, 61 | { 62 | give: ":8000", 63 | wantHost: "", 64 | wantPort: "8000", 65 | }, 66 | { 67 | give: "1:8", 68 | wantHost: "1", 69 | wantPort: "8", 70 | }, 71 | } 72 | 73 | for _, tt := range tests { 74 | t.Run(tt.give, func(t *testing.T) { 75 | host, port, err := net.SplitHostPort(tt.give) 76 | require.NoError(t, err) 77 | assert.Equal(t, tt.wantHost, host) 78 | assert.Equal(t, tt.wantPort, port) 79 | }) 80 | } 81 | ``` 82 | 83 | |
Bad | Good |
---|---|
152 | 153 | ```go 154 | func TestComplicatedTable(t *testing.T) { 155 | tests := []struct { 156 | give string 157 | want string 158 | wantErr error 159 | shouldCallX bool 160 | shouldCallY bool 161 | giveXResponse string 162 | giveXErr error 163 | giveYResponse string 164 | giveYErr error 165 | }{ 166 | // ... 167 | } 168 | 169 | for _, tt := range tests { 170 | t.Run(tt.give, func(t *testing.T) { 171 | // setup mocks 172 | ctrl := gomock.NewController(t) 173 | xMock := xmock.NewMockX(ctrl) 174 | if tt.shouldCallX { 175 | xMock.EXPECT().Call().Return( 176 | tt.giveXResponse, tt.giveXErr, 177 | ) 178 | } 179 | yMock := ymock.NewMockY(ctrl) 180 | if tt.shouldCallY { 181 | yMock.EXPECT().Call().Return( 182 | tt.giveYResponse, tt.giveYErr, 183 | ) 184 | } 185 | 186 | got, err := DoComplexThing(tt.give, xMock, yMock) 187 | 188 | // verify results 189 | if tt.wantErr != nil { 190 | require.EqualError(t, err, tt.wantErr) 191 | return 192 | } 193 | require.NoError(t, err) 194 | assert.Equal(t, want, got) 195 | }) 196 | } 197 | } 198 | ``` 199 | 200 | | 201 | 202 | ```go 203 | func TestShouldCallX(t *testing.T) { 204 | // setup mocks 205 | ctrl := gomock.NewController(t) 206 | xMock := xmock.NewMockX(ctrl) 207 | xMock.EXPECT().Call().Return("XResponse", nil) 208 | 209 | yMock := ymock.NewMockY(ctrl) 210 | 211 | got, err := DoComplexThing("inputX", xMock, yMock) 212 | 213 | require.NoError(t, err) 214 | assert.Equal(t, "want", got) 215 | } 216 | 217 | func TestShouldCallYAndFail(t *testing.T) { 218 | // setup mocks 219 | ctrl := gomock.NewController(t) 220 | xMock := xmock.NewMockX(ctrl) 221 | 222 | yMock := ymock.NewMockY(ctrl) 223 | yMock.EXPECT().Call().Return("YResponse", nil) 224 | 225 | _, err := DoComplexThing("inputY", xMock, yMock) 226 | assert.EqualError(t, err, "Y failed") 227 | } 228 | ``` 229 | |
Bad | Good |
---|---|
31 | 32 | ```go 33 | func isActive(now, start, stop int) bool { 34 | return start <= now && now < stop 35 | } 36 | ``` 37 | 38 | | 39 | 40 | ```go 41 | func isActive(now, start, stop time.Time) bool { 42 | return (start.Before(now) || start.Equal(now)) && now.Before(stop) 43 | } 44 | ``` 45 | 46 | |
Bad | Good |
---|---|
59 | 60 | ```go 61 | func poll(delay int) { 62 | for { 63 | // ... 64 | time.Sleep(time.Duration(delay) * time.Millisecond) 65 | } 66 | } 67 | 68 | poll(10) // was it seconds or milliseconds? 69 | ``` 70 | 71 | | 72 | 73 | ```go 74 | func poll(delay time.Duration) { 75 | for { 76 | // ... 77 | time.Sleep(delay) 78 | } 79 | } 80 | 81 | poll(10*time.Second) 82 | ``` 83 | 84 | |
Bad | Good |
---|---|
133 | 134 | ```go 135 | // {"interval": 2} 136 | type Config struct { 137 | Interval int `json:"interval"` 138 | } 139 | ``` 140 | 141 | | 142 | 143 | ```go 144 | // {"intervalMillis": 2000} 145 | type Config struct { 146 | IntervalMillis int `json:"intervalMillis"` 147 | } 148 | ``` 149 | 150 | |
Bad | Good |
---|---|
12 | 13 | ```go 14 | t := i.(string) 15 | ``` 16 | 17 | | 18 | 19 | ```go 20 | t, ok := i.(string) 21 | if !ok { 22 | // handle the error gracefully 23 | } 24 | ``` 25 | 26 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | var s = "foo" 13 | ``` 14 | 15 | | 16 | 17 | ```go 18 | s := "foo" 19 | ``` 20 | 21 | |
Bad | Good |
---|---|
33 | 34 | ```go 35 | func f(list []int) { 36 | filtered := []int{} 37 | for _, v := range list { 38 | if v > 10 { 39 | filtered = append(filtered, v) 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | | 46 | 47 | ```go 48 | func f(list []int) { 49 | var filtered []int 50 | for _, v := range list { 51 | if v > 10 { 52 | filtered = append(filtered, v) 53 | } 54 | } 55 | } 56 | ``` 57 | 58 | |
Bad | Good |
---|---|
10 | 11 | ```go 12 | err := os.WriteFile(name, data, 0644) 13 | if err != nil { 14 | return err 15 | } 16 | ``` 17 | 18 | | 19 | 20 | ```go 21 | if err := os.WriteFile(name, data, 0644); err != nil { 22 | return err 23 | } 24 | ``` 25 | 26 | |
Bad | Good |
---|---|
36 | 37 | ```go 38 | if data, err := os.ReadFile(name); err == nil { 39 | err = cfg.Decode(data) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | fmt.Println(cfg) 45 | return nil 46 | } else { 47 | return err 48 | } 49 | ``` 50 | 51 | | 52 | 53 | ```go 54 | data, err := os.ReadFile(name) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if err := cfg.Decode(data); err != nil { 60 | return err 61 | } 62 | 63 | fmt.Println(cfg) 64 | return nil 65 | ``` 66 | 67 | |
Bad | Good |
---|---|
77 | 78 | ```go 79 | const ( 80 | _defaultPort = 8080 81 | _defaultUser = "user" 82 | ) 83 | 84 | func Bar() { 85 | fmt.Println("Default port", _defaultPort) 86 | } 87 | ``` 88 | 89 | | 90 | 91 | ```go 92 | func Bar() { 93 | const ( 94 | defaultPort = 8080 95 | defaultUser = "user" 96 | ) 97 | fmt.Println("Default port", defaultPort) 98 | } 99 | ``` 100 | 101 | |