snapshot of https://github.com/uber-go/guide.git (master, 2026-09-15; blobs > 131072B omitted)
This commit is contained in:
11
.github/dependabot.yml
vendored
Normal file
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/tools"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
40
.github/workflows/ci.yml
vendored
Normal file
40
.github/workflows/ci.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ '*' ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
stitchmd:
|
||||||
|
name: Check or update style.md
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Needed to give the job permission
|
||||||
|
# to push to branches.
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Check or update style.md
|
||||||
|
uses: abhinav/stitchmd-action@v1
|
||||||
|
with:
|
||||||
|
# For pull requests, run in 'write' mode so that edits made
|
||||||
|
# directly in the GitHub UI get propagated to style.md
|
||||||
|
# without a local checkout.
|
||||||
|
#
|
||||||
|
# Otherwise, run in 'check' mode to fail CI if style.md is out-of-date.
|
||||||
|
mode: ${{ github.event_name == 'pull_request' && 'write' || 'check' }}
|
||||||
|
summary: src/SUMMARY.md
|
||||||
|
preface: src/preface.txt
|
||||||
|
output: style.md
|
||||||
|
|
||||||
|
- uses: stefanzweifel/git-auto-commit-action@v5
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
with:
|
||||||
|
file_pattern: style.md
|
||||||
|
commit_message: 'Auto-update style.md'
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/bin
|
||||||
19
.golangci.yml
Normal file
19
.golangci.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Refer to golangci-lint's example config file for more options and information:
|
||||||
|
# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
|
||||||
|
|
||||||
|
run:
|
||||||
|
timeout: 5m
|
||||||
|
modules-download-mode: readonly
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- errcheck
|
||||||
|
- goimports
|
||||||
|
- golint
|
||||||
|
- govet
|
||||||
|
- staticcheck
|
||||||
|
|
||||||
|
issues:
|
||||||
|
exclude-use-default: false
|
||||||
|
max-issues-per-linter: 0
|
||||||
|
max-same-issues: 0
|
||||||
80
CHANGELOG.md
Normal file
80
CHANGELOG.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# 2023-05-09
|
||||||
|
|
||||||
|
- Test tables: Discourage tables with complex, branching test bodies.
|
||||||
|
|
||||||
|
# 2023-04-13
|
||||||
|
|
||||||
|
- Errors: Add guidance on handling errors only once.
|
||||||
|
|
||||||
|
# 2023-03-03
|
||||||
|
|
||||||
|
- Receivers and Interfaces: Clarify subtlety with pointer receivers and values.
|
||||||
|
|
||||||
|
# 2022-10-18
|
||||||
|
|
||||||
|
- Add guidance on managing goroutine lifecycles.
|
||||||
|
|
||||||
|
# 2022-03-30
|
||||||
|
|
||||||
|
- Add guidance on using field tags in marshaled structs.
|
||||||
|
|
||||||
|
# 2021-11-16
|
||||||
|
|
||||||
|
- Add guidance on use of `%w` vs `%v` with `fmt.Errorf`, and where to use
|
||||||
|
`errors.New` or custom error types.
|
||||||
|
|
||||||
|
# 2021-11-12
|
||||||
|
|
||||||
|
- Add soft line length limit of 99 characters.
|
||||||
|
|
||||||
|
# 2021-04-19
|
||||||
|
|
||||||
|
- Programs should exit only in `main()`, preferably at most once.
|
||||||
|
|
||||||
|
# 2021-03-15
|
||||||
|
|
||||||
|
- Add guidance on omitting zero-value fields during struct initialization.
|
||||||
|
- Add guidance on `Foo{}` versus `var` form for initialization of empty
|
||||||
|
structs.
|
||||||
|
- Add new section for Initializing Structs, moving relevant guidances into
|
||||||
|
subsections of it.
|
||||||
|
|
||||||
|
# 2020-06-10
|
||||||
|
|
||||||
|
- Add guidance on avoiding `init()`.
|
||||||
|
- Add guidance to avoid using built-in names.
|
||||||
|
- Add reminder that nil slices are not always the same as empty slices.
|
||||||
|
|
||||||
|
# 2020-02-24
|
||||||
|
|
||||||
|
- Add guidance on verifying interface compliance with compile-time checks.
|
||||||
|
|
||||||
|
# 2020-01-30
|
||||||
|
|
||||||
|
- Recommend using the `time` package when dealing with time.
|
||||||
|
|
||||||
|
# 2020-01-25
|
||||||
|
|
||||||
|
- Add guidance against embedding types in public structs.
|
||||||
|
|
||||||
|
# 2019-12-17
|
||||||
|
|
||||||
|
- Functional Options: Recommend struct implementations of `Option` interface
|
||||||
|
instead of capturing values with a closure.
|
||||||
|
|
||||||
|
# 2019-11-26
|
||||||
|
|
||||||
|
- Add guidance against mutating global variables.
|
||||||
|
|
||||||
|
# 2019-10-21
|
||||||
|
|
||||||
|
- Add section on remaining consistent with existing practices.
|
||||||
|
- Add guidance on map initialization and size hints.
|
||||||
|
|
||||||
|
# 2019-10-11
|
||||||
|
|
||||||
|
- Suggest succinct context for error messages.
|
||||||
|
|
||||||
|
# 2019-10-10
|
||||||
|
|
||||||
|
- Initial release.
|
||||||
76
CODE_OF_CONDUCT.md
Normal file
76
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as
|
||||||
|
contributors and maintainers pledge to making participation in our project and
|
||||||
|
our community a harassment-free experience for everyone, regardless of age, body
|
||||||
|
size, disability, ethnicity, sex characteristics, gender identity and expression,
|
||||||
|
level of experience, education, socio-economic status, nationality, personal
|
||||||
|
appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment
|
||||||
|
include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or
|
||||||
|
advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic
|
||||||
|
address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a
|
||||||
|
professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable
|
||||||
|
behavior and are expected to take appropriate and fair corrective action in
|
||||||
|
response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or
|
||||||
|
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||||
|
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||||
|
permanently any contributor for other behaviors that they deem inappropriate,
|
||||||
|
threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces
|
||||||
|
when an individual is representing the project or its community. Examples of
|
||||||
|
representing a project or community include using an official project e-mail
|
||||||
|
address, posting via an official social media account, or acting as an appointed
|
||||||
|
representative at an online or offline event. Representation of a project may be
|
||||||
|
further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||||
|
reported by contacting the project team at oss-conduct@uber.com. All
|
||||||
|
complaints will be reviewed and investigated and will result in a response that
|
||||||
|
is deemed necessary and appropriate to the circumstances. The project team is
|
||||||
|
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||||
|
Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||||
|
faith may face temporary or permanent repercussions as determined by other
|
||||||
|
members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
||||||
|
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
||||||
|
|
||||||
|
[homepage]: https://www.contributor-covenant.org
|
||||||
|
|
||||||
|
For answers to common questions about this code of conduct, see
|
||||||
|
https://www.contributor-covenant.org/faq
|
||||||
86
CONTRIBUTING.md
Normal file
86
CONTRIBUTING.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Contributing
|
||||||
|
|
||||||
|
Before making any changes,
|
||||||
|
please discuss your plans on GitHub
|
||||||
|
and get agreement on the general direction of the change.
|
||||||
|
|
||||||
|
## Making changes
|
||||||
|
|
||||||
|
- style.md is generated from the contents of the src/ folder.
|
||||||
|
All changes must be made to files in the src/ folder.
|
||||||
|
- For new entries, create a new file with a short name
|
||||||
|
(see [File names](#file-names)) and add it to [SUMMARY.md](src/SUMMARY.md).
|
||||||
|
The file must have a single level 1 heading and any number of subsections.
|
||||||
|
- Use tables for side-by-side code samples.
|
||||||
|
- Link to other sections with their file names (`[..](foo.md)`).
|
||||||
|
|
||||||
|
## Updating style.md
|
||||||
|
|
||||||
|
`style.md` is generated from the contents of `src/` using [stitchmd](https://github.com/abhinav/stitchmd).
|
||||||
|
If CI fails because `style.md` is out of date, regenerate it by running:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make
|
||||||
|
```
|
||||||
|
|
||||||
|
This will install `stitchmd` if needed and update `style.md`.
|
||||||
|
Commit the result and push it to your branch.
|
||||||
|
|
||||||
|
## Writing style
|
||||||
|
|
||||||
|
### Line breaks
|
||||||
|
|
||||||
|
Use [semantic line breaks](https://sembr.org/) in your writing.
|
||||||
|
This keeps the Markdown files easily reviewable and editable.
|
||||||
|
|
||||||
|
### File names
|
||||||
|
|
||||||
|
Files in src/ follow a rough naming convention of:
|
||||||
|
|
||||||
|
{subject}-{desc}.md
|
||||||
|
|
||||||
|
Where `{subject}` is the **singular form** of subject that the entry is about
|
||||||
|
(e.g `string`, `struct`, `time`, `var`, `error`)
|
||||||
|
and `{desc}` is a short one or two word description of the topic.
|
||||||
|
For subjects where their name is enough, the `-{desc}` may be omitted.
|
||||||
|
|
||||||
|
### Code samples
|
||||||
|
|
||||||
|
Use two spaces to indent code samples.
|
||||||
|
Horizontal space is limited in side-by-side samples.
|
||||||
|
|
||||||
|
### Side-by-side samples
|
||||||
|
|
||||||
|
Create side-by-side code samples with the following template:
|
||||||
|
|
||||||
|
~~~
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
BAD CODE GOES HERE
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
GOOD CODE GOES HERE
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
~~~
|
||||||
|
|
||||||
|
The empty lines between the HTML tags and code samples are necessary.
|
||||||
|
|
||||||
|
If you need to add labels or descriptions below the code samples,
|
||||||
|
add another row before the `</tbody></table>` line.
|
||||||
|
|
||||||
|
~~~
|
||||||
|
<tr>
|
||||||
|
<td>DESCRIBE BAD CODE</td>
|
||||||
|
<td>DESCRIBE GOOD CODE</td>
|
||||||
|
</tr>
|
||||||
|
~~~
|
||||||
176
LICENSE
Normal file
176
LICENSE
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
27
Makefile
Normal file
27
Makefile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
SHELL = /bin/bash
|
||||||
|
|
||||||
|
# Setting GOBIN makes 'go install' put the binary in the bin/ directory.
|
||||||
|
export GOBIN ?= $(dir $(abspath $(lastword $(MAKEFILE_LIST))))/bin
|
||||||
|
|
||||||
|
STITCHMD = $(GOBIN)/stitchmd
|
||||||
|
|
||||||
|
# Keep these options in-sync with .github/workflows/ci.yml.
|
||||||
|
STITCHMD_ARGS = -o style.md -preface src/preface.txt src/SUMMARY.md
|
||||||
|
|
||||||
|
.PHONY: all
|
||||||
|
all: style.md
|
||||||
|
|
||||||
|
.PHONY: lint
|
||||||
|
lint: $(STITCHMD)
|
||||||
|
@DIFF=$$($(STITCHMD) -d $(STITCHMD_ARGS)); \
|
||||||
|
if [[ -n "$$DIFF" ]]; then \
|
||||||
|
echo "style.md is out of date:"; \
|
||||||
|
echo "$$DIFF"; \
|
||||||
|
false; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
style.md: $(STITCHMD) $(wildcard src/*)
|
||||||
|
$(STITCHMD) $(STITCHMD_ARGS)
|
||||||
|
|
||||||
|
$(STITCHMD):
|
||||||
|
go install go.abhg.dev/stitchmd@latest
|
||||||
30
README.md
Normal file
30
README.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
This repository holds the [Uber Go Style Guide](style.md), which documents
|
||||||
|
patterns and conventions used in Go code at Uber.
|
||||||
|
|
||||||
|
## Style Guide
|
||||||
|
|
||||||
|
See [Uber Go Style Guide](style.md) for the style guide.
|
||||||
|
|
||||||
|
## Translations
|
||||||
|
|
||||||
|
We are aware of the following translations of this guide by the Go community.
|
||||||
|
|
||||||
|
- **中文翻译** (Chinese): [xxjwxc/uber_go_guide_cn](https://github.com/xxjwxc/uber_go_guide_cn)
|
||||||
|
- **繁體中文** (Traditional Chinese):[ianchen0119/uber_go_guide_tw](https://github.com/ianchen0119/uber_go_guide_tw)
|
||||||
|
- **한국어 번역** (Korean): [TangoEnSkai/uber-go-style-guide-kr](https://github.com/TangoEnSkai/uber-go-style-guide-kr)
|
||||||
|
- **日本語訳** (Japanese): [knsh14/uber-style-guide-ja](https://github.com/knsh14/uber-style-guide-ja)
|
||||||
|
- **Traducción al Español** (Spanish): [friendsofgo/uber-go-guide-es](https://github.com/friendsofgo/uber-go-guide-es)
|
||||||
|
- **แปลภาษาไทย** (Thai): [pallat/uber-go-style-guide-th](https://github.com/pallat/uber-go-style-guide-th)
|
||||||
|
- **Tradução em português** (Portuguese): [lucassscaravelli/uber-go-guide-pt](https://github.com/lucassscaravelli/uber-go-guide-pt)
|
||||||
|
- **Tradução em português** (Portuguese BR): [alcir-junior-caju/uber-go-style-guide-pt-br](https://github.com/alcir-junior-caju/uber-go-style-guide-pt-br)
|
||||||
|
- **Tłumaczenie polskie** (Polish): [DamianSkrzypczak/uber-go-guide-pl](https://github.com/DamianSkrzypczak/uber-go-guide-pl)
|
||||||
|
- **Русский перевод** (Russian): [alekarah/uber-go-guide-ru](https://github.com/alekarah/uber-go-guide-ru)
|
||||||
|
- **Français** (French): [rm3l/uber-go-style-guide-fr](https://github.com/rm3l/uber-go-style-guide-fr)
|
||||||
|
- **Türkçe** (Turkish): [ksckaan1/uber-go-style-guide-tr](https://github.com/ksckaan1/uber-go-style-guide-tr)
|
||||||
|
- **Український переклад** (Ukrainian): [vorobeyme/uber-go-style-guide-uk](https://github.com/vorobeyme/uber-go-style-guide-uk)
|
||||||
|
- **ترجمه فارسی** (Persian): [jamalkaksouri/uber-go-guide-ir](https://github.com/jamalkaksouri/uber-go-guide-ir)
|
||||||
|
- **Tiếng việt** (Vietnamese): [nc-minh/uber-go-guide-vi](https://github.com/nc-minh/uber-go-guide-vi)
|
||||||
|
- **العربية** (Arabic): [anqorithm/uber-go-guide-ar](https://github.com/anqorithm/uber-go-guide-ar)
|
||||||
|
- **Bahasa Indonesia** (Indonesian): [stanleydv12/uber-go-guide-id](https://github.com/stanleydv12/uber-go-guide-id)
|
||||||
|
|
||||||
|
If you have a translation, feel free to submit a PR adding it to the list.
|
||||||
2
src/README.md
Normal file
2
src/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
The contents of this directory are used to generate the top-level style.md.
|
||||||
|
The layout is controlled by SUMMARY.md.
|
||||||
66
src/SUMMARY.md
Normal file
66
src/SUMMARY.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
# Uber Go Style Guide
|
||||||
|
|
||||||
|
- [Introduction](intro.md)
|
||||||
|
- Guidelines
|
||||||
|
- [Pointers to Interfaces](interface-pointer.md)
|
||||||
|
- [Verify Interface Compliance](interface-compliance.md)
|
||||||
|
- [Receivers and Interfaces](interface-receiver.md)
|
||||||
|
- [Zero-value Mutexes are Valid](mutex-zero-value.md)
|
||||||
|
- [Copy Slices and Maps at Boundaries](container-copy.md)
|
||||||
|
- [Defer to Clean Up](defer-clean.md)
|
||||||
|
- [Channel Size is One or None](channel-size.md)
|
||||||
|
- [Start Enums at One](enum-start.md)
|
||||||
|
- [Use `"time"` to handle time](time.md)
|
||||||
|
- Errors
|
||||||
|
- [Error Types](error-type.md)
|
||||||
|
- [Error Wrapping](error-wrap.md)
|
||||||
|
- [Error Naming](error-name.md)
|
||||||
|
- [Handle Errors Once](error-once.md)
|
||||||
|
- [Handle Type Assertion Failures](type-assert.md)
|
||||||
|
- [Don't Panic](panic.md)
|
||||||
|
- [Use go.uber.org/atomic](atomic.md)
|
||||||
|
- [Avoid Mutable Globals](global-mut.md)
|
||||||
|
- [Avoid Embedding Types in Public Structs](embed-public.md)
|
||||||
|
- [Avoid Using Built-In Names](builtin-name.md)
|
||||||
|
- [Avoid `init()`](init.md)
|
||||||
|
- [Exit in Main](exit-main.md)
|
||||||
|
- [Exit Once](exit-once.md)
|
||||||
|
- [Use field tags in marshaled structs](struct-tag.md)
|
||||||
|
- [Don't fire-and-forget goroutines](goroutine-forget.md)
|
||||||
|
- [Wait for goroutines to exit](goroutine-exit.md)
|
||||||
|
- [No goroutines in `init()`](goroutine-init.md)
|
||||||
|
- [Performance](performance.md)
|
||||||
|
- [Prefer strconv over fmt](strconv.md)
|
||||||
|
- [Avoid repeated string-to-byte conversions](string-byte-slice.md)
|
||||||
|
- [Prefer Specifying Container Capacity](container-capacity.md)
|
||||||
|
- Style
|
||||||
|
- [Avoid overly long lines](line-length.md)
|
||||||
|
- [Be Consistent](consistency.md)
|
||||||
|
- [Group Similar Declarations](decl-group.md)
|
||||||
|
- [Import Group Ordering](import-group.md)
|
||||||
|
- [Package Names](package-name.md)
|
||||||
|
- [Function Names](function-name.md)
|
||||||
|
- [Import Aliasing](import-alias.md)
|
||||||
|
- [Function Grouping and Ordering](function-order.md)
|
||||||
|
- [Reduce Nesting](nest-less.md)
|
||||||
|
- [Unnecessary Else](else-unnecessary.md)
|
||||||
|
- [Top-level Variable Declarations](global-decl.md)
|
||||||
|
- [Prefix Unexported Globals with _](global-name.md)
|
||||||
|
- [Embedding in Structs](struct-embed.md)
|
||||||
|
- [Local Variable Declarations](var-decl.md)
|
||||||
|
- [nil is a valid slice](slice-nil.md)
|
||||||
|
- [Reduce Scope of Variables](var-scope.md)
|
||||||
|
- [Avoid Naked Parameters](param-naked.md)
|
||||||
|
- [Use Raw String Literals to Avoid Escaping](string-escape.md)
|
||||||
|
- Initializing Structs
|
||||||
|
- [Use Field Names to Initialize Structs](struct-field-key.md)
|
||||||
|
- [Omit Zero Value Fields in Structs](struct-field-zero.md)
|
||||||
|
- [Use `var` for Zero Value Structs](struct-zero.md)
|
||||||
|
- [Initializing Struct References](struct-pointer.md)
|
||||||
|
- [Initializing Maps](map-init.md)
|
||||||
|
- [Format Strings outside Printf](printf-const.md)
|
||||||
|
- [Naming Printf-style Functions](printf-name.md)
|
||||||
|
- Patterns
|
||||||
|
- [Test Tables](test-table.md)
|
||||||
|
- [Functional Options](functional-option.md)
|
||||||
|
- [Linting](lint.md)
|
||||||
57
src/atomic.md
Normal file
57
src/atomic.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Use go.uber.org/atomic
|
||||||
|
|
||||||
|
Atomic operations with the [sync/atomic] package operate on the raw types
|
||||||
|
(`int32`, `int64`, etc.) so it is easy to forget to use the atomic operation to
|
||||||
|
read or modify the variables.
|
||||||
|
|
||||||
|
[go.uber.org/atomic] adds type safety to these operations by hiding the
|
||||||
|
underlying type. Additionally, it includes a convenient `atomic.Bool` type.
|
||||||
|
|
||||||
|
[go.uber.org/atomic]: https://pkg.go.dev/go.uber.org/atomic
|
||||||
|
[sync/atomic]: https://pkg.go.dev/sync/atomic
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type foo struct {
|
||||||
|
running int32 // atomic
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *foo) start() {
|
||||||
|
if atomic.SwapInt32(&f.running, 1) == 1 {
|
||||||
|
// already running…
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// start the Foo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *foo) isRunning() bool {
|
||||||
|
return f.running == 1 // race!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type foo struct {
|
||||||
|
running atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *foo) start() {
|
||||||
|
if f.running.Swap(true) {
|
||||||
|
// already running…
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// start the Foo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *foo) isRunning() bool {
|
||||||
|
return f.running.Load()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
93
src/builtin-name.md
Normal file
93
src/builtin-name.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Avoid Using Built-In Names
|
||||||
|
|
||||||
|
The Go [language specification] outlines several built-in,
|
||||||
|
[predeclared identifiers] that should not be used as names within Go programs.
|
||||||
|
|
||||||
|
Depending on context, reusing these identifiers as names will either shadow
|
||||||
|
the original within the current lexical scope (and any nested scopes) or make
|
||||||
|
affected code confusing. In the best case, the compiler will complain; in the
|
||||||
|
worst case, such code may introduce latent, hard-to-grep bugs.
|
||||||
|
|
||||||
|
[language specification]: https://go.dev/ref/spec
|
||||||
|
[predeclared identifiers]: https://go.dev/ref/spec#Predeclared_identifiers
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var error string
|
||||||
|
// `error` shadows the builtin
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
|
func handleErrorMessage(error string) {
|
||||||
|
// `error` shadows the builtin
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var errorMessage string
|
||||||
|
// `error` refers to the builtin
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
|
func handleErrorMessage(msg string) {
|
||||||
|
// `error` refers to the builtin
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Foo struct {
|
||||||
|
// While these fields technically don't
|
||||||
|
// constitute shadowing, grepping for
|
||||||
|
// `error` or `string` strings is now
|
||||||
|
// ambiguous.
|
||||||
|
error error
|
||||||
|
string string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Foo) Error() error {
|
||||||
|
// `error` and `f.error` are
|
||||||
|
// visually similar
|
||||||
|
return f.error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Foo) String() string {
|
||||||
|
// `string` and `f.string` are
|
||||||
|
// visually similar
|
||||||
|
return f.string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Foo struct {
|
||||||
|
// `error` and `string` strings are
|
||||||
|
// now unambiguous.
|
||||||
|
err error
|
||||||
|
str string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Foo) Error() error {
|
||||||
|
return f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Foo) String() string {
|
||||||
|
return f.str
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Note that the compiler will not generate errors when using predeclared
|
||||||
|
identifiers, but tools such as `go vet` should correctly point out these and
|
||||||
|
other cases of shadowing.
|
||||||
29
src/channel-size.md
Normal file
29
src/channel-size.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Channel Size is One or None
|
||||||
|
|
||||||
|
Channels should usually have a size of one or be unbuffered. By default,
|
||||||
|
channels are unbuffered and have a size of zero. Any other size
|
||||||
|
must be subject to a high level of scrutiny. Consider how the size is
|
||||||
|
determined, what prevents the channel from filling up under load and blocking
|
||||||
|
writers, and what happens when this occurs.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Ought to be enough for anybody!
|
||||||
|
c := make(chan int, 64)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Size of one
|
||||||
|
c := make(chan int, 1) // or
|
||||||
|
// Unbuffered channel, size of zero
|
||||||
|
c := make(chan int)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
19
src/consistency.md
Normal file
19
src/consistency.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Be Consistent
|
||||||
|
|
||||||
|
Some of the guidelines outlined in this document can be evaluated objectively;
|
||||||
|
others are situational, contextual, or subjective.
|
||||||
|
|
||||||
|
Above all else, **be consistent**.
|
||||||
|
|
||||||
|
Consistent code is easier to maintain, is easier to rationalize, requires less
|
||||||
|
cognitive overhead, and is easier to migrate or update as new conventions emerge
|
||||||
|
or classes of bugs are fixed.
|
||||||
|
|
||||||
|
Conversely, having multiple disparate or conflicting styles within a single
|
||||||
|
codebase causes maintenance overhead, uncertainty, and cognitive dissonance,
|
||||||
|
all of which can directly contribute to lower velocity, painful code reviews,
|
||||||
|
and bugs.
|
||||||
|
|
||||||
|
When applying these guidelines to a codebase, it is recommended that changes
|
||||||
|
are made at a package (or larger) level: application at a sub-package level
|
||||||
|
violates the above concern by introducing multiple styles into the same code.
|
||||||
119
src/container-capacity.md
Normal file
119
src/container-capacity.md
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
# Prefer Specifying Container Capacity
|
||||||
|
|
||||||
|
Specify container capacity where possible in order to allocate memory for the
|
||||||
|
container up front. This minimizes subsequent allocations (by copying and
|
||||||
|
resizing of the container) as elements are added.
|
||||||
|
|
||||||
|
## Specifying Map Capacity Hints
|
||||||
|
|
||||||
|
Where possible, provide capacity hints when initializing
|
||||||
|
maps with `make()`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
make(map[T1]T2, hint)
|
||||||
|
```
|
||||||
|
|
||||||
|
Providing a capacity hint to `make()` tries to right-size the
|
||||||
|
map at initialization time, which reduces the need for growing
|
||||||
|
the map and allocations as elements are added to the map.
|
||||||
|
|
||||||
|
Note that, unlike slices, map capacity hints do not guarantee complete,
|
||||||
|
preemptive allocation, but are used to approximate the number of hashmap buckets
|
||||||
|
required. Consequently, allocations may still occur when adding elements to the
|
||||||
|
map, even up to the specified capacity.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
files, _ := os.ReadDir("./files")
|
||||||
|
|
||||||
|
m := make(map[string]os.DirEntry)
|
||||||
|
for _, f := range files {
|
||||||
|
m[f.Name()] = f
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
files, _ := os.ReadDir("./files")
|
||||||
|
|
||||||
|
m := make(map[string]os.DirEntry, len(files))
|
||||||
|
for _, f := range files {
|
||||||
|
m[f.Name()] = f
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
`m` is created without a size hint; the map will resize
|
||||||
|
dynamically, causing multiple allocations as it grows.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
`m` is created with a size hint; there may be fewer
|
||||||
|
allocations at assignment time.
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
## Specifying Slice Capacity
|
||||||
|
|
||||||
|
Where possible, provide capacity hints when initializing slices with `make()`,
|
||||||
|
particularly when appending.
|
||||||
|
|
||||||
|
```go
|
||||||
|
make([]T, length, capacity)
|
||||||
|
```
|
||||||
|
|
||||||
|
Unlike maps, slice capacity is not a hint: the compiler will allocate enough
|
||||||
|
memory for the capacity of the slice as provided to `make()`, which means that
|
||||||
|
subsequent `append()` operations will incur zero allocations (until the length
|
||||||
|
of the slice matches the capacity, after which any appends will require a resize
|
||||||
|
to hold additional elements).
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
data := make([]int, 0)
|
||||||
|
for k := 0; k < size; k++{
|
||||||
|
data = append(data, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
data := make([]int, 0, size)
|
||||||
|
for k := 0; k < size; k++{
|
||||||
|
data = append(data, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
BenchmarkBad-4 100000000 2.48s
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
BenchmarkGood-4 100000000 0.21s
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
106
src/container-copy.md
Normal file
106
src/container-copy.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# Copy Slices and Maps at Boundaries
|
||||||
|
|
||||||
|
Slices and maps contain pointers to the underlying data so be wary of scenarios
|
||||||
|
when they need to be copied.
|
||||||
|
|
||||||
|
## Receiving Slices and Maps
|
||||||
|
|
||||||
|
Keep in mind that users can modify a map or slice you received as an argument
|
||||||
|
if you store a reference to it.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th> <th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (d *Driver) SetTrips(trips []Trip) {
|
||||||
|
d.trips = trips
|
||||||
|
}
|
||||||
|
|
||||||
|
trips := ...
|
||||||
|
d1.SetTrips(trips)
|
||||||
|
|
||||||
|
// Did you mean to modify d1.trips?
|
||||||
|
trips[0] = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (d *Driver) SetTrips(trips []Trip) {
|
||||||
|
d.trips = make([]Trip, len(trips))
|
||||||
|
copy(d.trips, trips)
|
||||||
|
}
|
||||||
|
|
||||||
|
trips := ...
|
||||||
|
d1.SetTrips(trips)
|
||||||
|
|
||||||
|
// We can now modify trips[0] without affecting d1.trips.
|
||||||
|
trips[0] = ...
|
||||||
|
```
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
## Returning Slices and Maps
|
||||||
|
|
||||||
|
Similarly, be wary of user modifications to maps or slices exposing internal
|
||||||
|
state.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Stats struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
counters map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot returns the current stats.
|
||||||
|
func (s *Stats) Snapshot() map[string]int {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
return s.counters
|
||||||
|
}
|
||||||
|
|
||||||
|
// snapshot is no longer protected by the mutex, so any
|
||||||
|
// access to the snapshot is subject to data races.
|
||||||
|
snapshot := stats.Snapshot()
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Stats struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
counters map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stats) Snapshot() map[string]int {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
|
||||||
|
result := make(map[string]int, len(s.counters))
|
||||||
|
for k, v := range s.counters {
|
||||||
|
result[k] = v
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot is now a copy.
|
||||||
|
snapshot := stats.Snapshot()
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
|
||||||
178
src/decl-group.md
Normal file
178
src/decl-group.md
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
# Group Similar Declarations
|
||||||
|
|
||||||
|
Go supports grouping similar declarations.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "a"
|
||||||
|
import "b"
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"a"
|
||||||
|
"b"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
This also applies to constants, variables, and type declarations.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
const a = 1
|
||||||
|
const b = 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var a = 1
|
||||||
|
var b = 2
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
type Area float64
|
||||||
|
type Volume float64
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
a = 1
|
||||||
|
b = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
a = 1
|
||||||
|
b = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Area float64
|
||||||
|
Volume float64
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Only group related declarations. Do not group declarations that are unrelated.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Operation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Add Operation = iota + 1
|
||||||
|
Subtract
|
||||||
|
Multiply
|
||||||
|
EnvVar = "MY_ENV"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Operation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Add Operation = iota + 1
|
||||||
|
Subtract
|
||||||
|
Multiply
|
||||||
|
)
|
||||||
|
|
||||||
|
const EnvVar = "MY_ENV"
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Groups are not limited in where they can be used. For example, you can use them
|
||||||
|
inside of functions.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func f() string {
|
||||||
|
red := color.New(0xff0000)
|
||||||
|
green := color.New(0x00ff00)
|
||||||
|
blue := color.New(0x0000ff)
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func f() string {
|
||||||
|
var (
|
||||||
|
red = color.New(0xff0000)
|
||||||
|
green = color.New(0x00ff00)
|
||||||
|
blue = color.New(0x0000ff)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Exception: Variable declarations, particularly inside functions, should be
|
||||||
|
grouped together if declared adjacent to other variables. Do this for variables
|
||||||
|
declared together even if they are unrelated.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *client) request() {
|
||||||
|
caller := c.name
|
||||||
|
format := "json"
|
||||||
|
timeout := 5*time.Second
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (c *client) request() {
|
||||||
|
var (
|
||||||
|
caller = c.name
|
||||||
|
format = "json"
|
||||||
|
timeout = 5*time.Second
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
49
src/defer-clean.md
Normal file
49
src/defer-clean.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Defer to Clean Up
|
||||||
|
|
||||||
|
Use defer to clean up resources such as files and locks.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
p.Lock()
|
||||||
|
if p.count < 10 {
|
||||||
|
p.Unlock()
|
||||||
|
return p.count
|
||||||
|
}
|
||||||
|
|
||||||
|
p.count++
|
||||||
|
newCount := p.count
|
||||||
|
p.Unlock()
|
||||||
|
|
||||||
|
return newCount
|
||||||
|
|
||||||
|
// easy to miss unlocks due to multiple returns
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
p.Lock()
|
||||||
|
defer p.Unlock()
|
||||||
|
|
||||||
|
if p.count < 10 {
|
||||||
|
return p.count
|
||||||
|
}
|
||||||
|
|
||||||
|
p.count++
|
||||||
|
return p.count
|
||||||
|
|
||||||
|
// more readable
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Defer has an extremely small overhead and should be avoided only if you can
|
||||||
|
prove that your function execution time is in the order of nanoseconds. The
|
||||||
|
readability win of using defers is worth the miniscule cost of using them. This
|
||||||
|
is especially true for larger methods that have more than simple memory
|
||||||
|
accesses, where the other computations are more significant than the `defer`.
|
||||||
30
src/else-unnecessary.md
Normal file
30
src/else-unnecessary.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Unnecessary Else
|
||||||
|
|
||||||
|
If a variable is set in both branches of an if, it can be replaced with a
|
||||||
|
single if.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var a int
|
||||||
|
if b {
|
||||||
|
a = 100
|
||||||
|
} else {
|
||||||
|
a = 10
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
a := 10
|
||||||
|
if b {
|
||||||
|
a = 100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
132
src/embed-public.md
Normal file
132
src/embed-public.md
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
# Avoid Embedding Types in Public Structs
|
||||||
|
|
||||||
|
These embedded types leak implementation details, inhibit type evolution, and
|
||||||
|
obscure documentation.
|
||||||
|
|
||||||
|
Assuming you have implemented a variety of list types using a shared
|
||||||
|
`AbstractList`, avoid embedding the `AbstractList` in your concrete list
|
||||||
|
implementations.
|
||||||
|
Instead, hand-write only the methods to your concrete list that will delegate
|
||||||
|
to the abstract list.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type AbstractList struct {}
|
||||||
|
|
||||||
|
// Add adds an entity to the list.
|
||||||
|
func (l *AbstractList) Add(e Entity) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes an entity from the list.
|
||||||
|
func (l *AbstractList) Remove(e Entity) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ConcreteList is a list of entities.
|
||||||
|
type ConcreteList struct {
|
||||||
|
*AbstractList
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ConcreteList is a list of entities.
|
||||||
|
type ConcreteList struct {
|
||||||
|
list *AbstractList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds an entity to the list.
|
||||||
|
func (l *ConcreteList) Add(e Entity) {
|
||||||
|
l.list.Add(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes an entity from the list.
|
||||||
|
func (l *ConcreteList) Remove(e Entity) {
|
||||||
|
l.list.Remove(e)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Go allows [type embedding] as a compromise between inheritance and composition.
|
||||||
|
The outer type gets implicit copies of the embedded type's methods.
|
||||||
|
These methods, by default, delegate to the same method of the embedded
|
||||||
|
instance.
|
||||||
|
|
||||||
|
[type embedding]: https://go.dev/doc/effective_go#embedding
|
||||||
|
|
||||||
|
The struct also gains a field by the same name as the type.
|
||||||
|
So, if the embedded type is public, the field is public.
|
||||||
|
To maintain backward compatibility, every future version of the outer type must
|
||||||
|
keep the embedded type.
|
||||||
|
|
||||||
|
An embedded type is rarely necessary.
|
||||||
|
It is a convenience that helps you avoid writing tedious delegate methods.
|
||||||
|
|
||||||
|
Even embedding a compatible AbstractList *interface*, instead of the struct,
|
||||||
|
would offer the developer more flexibility to change in the future, but still
|
||||||
|
leak the detail that the concrete lists use an abstract implementation.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// AbstractList is a generalized implementation
|
||||||
|
// for various kinds of lists of entities.
|
||||||
|
type AbstractList interface {
|
||||||
|
Add(Entity)
|
||||||
|
Remove(Entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConcreteList is a list of entities.
|
||||||
|
type ConcreteList struct {
|
||||||
|
AbstractList
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// ConcreteList is a list of entities.
|
||||||
|
type ConcreteList struct {
|
||||||
|
list AbstractList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds an entity to the list.
|
||||||
|
func (l *ConcreteList) Add(e Entity) {
|
||||||
|
l.list.Add(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes an entity from the list.
|
||||||
|
func (l *ConcreteList) Remove(e Entity) {
|
||||||
|
l.list.Remove(e)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Either with an embedded struct or an embedded interface, the embedded type
|
||||||
|
places limits on the evolution of the type.
|
||||||
|
|
||||||
|
- Adding methods to an embedded interface is a breaking change.
|
||||||
|
- Removing methods from an embedded struct is a breaking change.
|
||||||
|
- Removing the embedded type is a breaking change.
|
||||||
|
- Replacing the embedded type, even with an alternative that satisfies the same
|
||||||
|
interface, is a breaking change.
|
||||||
|
|
||||||
|
Although writing these delegate methods is tedious, the additional effort hides
|
||||||
|
an implementation detail, leaves more opportunities for change, and also
|
||||||
|
eliminates indirection for discovering the full List interface in
|
||||||
|
documentation.
|
||||||
56
src/enum-start.md
Normal file
56
src/enum-start.md
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
# Start Enums at One
|
||||||
|
|
||||||
|
The standard way of introducing enumerations in Go is to declare a custom type
|
||||||
|
and a `const` group with `iota`. Since variables have a 0 default value, you
|
||||||
|
should usually start your enums on a non-zero value.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Operation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Add Operation = iota
|
||||||
|
Subtract
|
||||||
|
Multiply
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add=0, Subtract=1, Multiply=2
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Operation int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Add Operation = iota + 1
|
||||||
|
Subtract
|
||||||
|
Multiply
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add=1, Subtract=2, Multiply=3
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
There are cases where using the zero value makes sense, for example when the
|
||||||
|
zero value case is the desirable default behavior.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type LogOutput int
|
||||||
|
|
||||||
|
const (
|
||||||
|
LogToStdout LogOutput = iota
|
||||||
|
LogToFile
|
||||||
|
LogToRemote
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogToStdout=0, LogToFile=1, LogToRemote=2
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- TODO: section on String methods for enums -->
|
||||||
52
src/error-name.md
Normal file
52
src/error-name.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# Error Naming
|
||||||
|
|
||||||
|
For error values stored as global variables,
|
||||||
|
use the prefix `Err` or `err` depending on whether they're exported.
|
||||||
|
This guidance supersedes the [Prefix Unexported Globals with _](global-name.md).
|
||||||
|
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
// The following two errors are exported
|
||||||
|
// so that users of this package can match them
|
||||||
|
// with errors.Is.
|
||||||
|
|
||||||
|
ErrBrokenLink = errors.New("link is broken")
|
||||||
|
ErrCouldNotOpen = errors.New("could not open")
|
||||||
|
|
||||||
|
// This error is not exported because
|
||||||
|
// we don't want to make it part of our public API.
|
||||||
|
// We may still use it inside the package
|
||||||
|
// with errors.Is.
|
||||||
|
|
||||||
|
errNotFound = errors.New("not found")
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
For custom error types, use the suffix `Error` instead.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Similarly, this error is exported
|
||||||
|
// so that users of this package can match it
|
||||||
|
// with errors.As.
|
||||||
|
|
||||||
|
type NotFoundError struct {
|
||||||
|
File string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NotFoundError) Error() string {
|
||||||
|
return fmt.Sprintf("file %q not found", e.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And this error is not exported because
|
||||||
|
// we don't want to make it part of the public API.
|
||||||
|
// We can still use it inside the package
|
||||||
|
// with errors.As.
|
||||||
|
|
||||||
|
type resolveError struct {
|
||||||
|
Path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *resolveError) Error() string {
|
||||||
|
return fmt.Sprintf("resolve %q", e.Path)
|
||||||
|
}
|
||||||
|
```
|
||||||
111
src/error-once.md
Normal file
111
src/error-once.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Handle Errors Once
|
||||||
|
|
||||||
|
When a caller receives an error from a callee,
|
||||||
|
it can handle it in a variety of different ways
|
||||||
|
depending on what it knows about the error.
|
||||||
|
|
||||||
|
These include, but not are limited to:
|
||||||
|
|
||||||
|
- if the callee contract defines specific errors,
|
||||||
|
matching the error with `errors.Is` or `errors.As`
|
||||||
|
and handling the branches differently
|
||||||
|
- if the error is recoverable,
|
||||||
|
logging the error and degrading gracefully
|
||||||
|
- if the error represents a domain-specific failure condition,
|
||||||
|
returning a well-defined error
|
||||||
|
- returning the error, either [wrapped](error-wrap.md) or verbatim
|
||||||
|
|
||||||
|
Regardless of how the caller handles the error,
|
||||||
|
it should typically handle each error only once.
|
||||||
|
The caller should not, for example, log the error and then return it,
|
||||||
|
because *its* callers may handle the error as well.
|
||||||
|
|
||||||
|
For example, consider the following cases:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Description</th><th>Code</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
**Bad**: Log the error and return it
|
||||||
|
|
||||||
|
Callers further up the stack will likely take a similar action with the error.
|
||||||
|
Doing so makes a lot of noise in the application logs for little value.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
u, err := getUser(id)
|
||||||
|
if err != nil {
|
||||||
|
// BAD: See description
|
||||||
|
log.Printf("Could not get user %q: %v", id, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
**Good**: Wrap the error and return it
|
||||||
|
|
||||||
|
Callers further up the stack will handle the error.
|
||||||
|
Use of `%w` ensures they can match the error with `errors.Is` or `errors.As`
|
||||||
|
if relevant.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
u, err := getUser(id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("get user %q: %w", id, err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
**Good**: Log the error and degrade gracefully
|
||||||
|
|
||||||
|
If the operation isn't strictly necessary,
|
||||||
|
we can provide a degraded but unbroken experience
|
||||||
|
by recovering from it.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := emitMetrics(); err != nil {
|
||||||
|
// Failure to write metrics should not
|
||||||
|
// break the application.
|
||||||
|
log.Printf("Could not emit metrics: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
**Good**: Match the error and degrade gracefully
|
||||||
|
|
||||||
|
If the callee defines a specific error in its contract,
|
||||||
|
and the failure is recoverable,
|
||||||
|
match on that error case and degrade gracefully.
|
||||||
|
For all other cases, wrap the error and return it.
|
||||||
|
|
||||||
|
Callers further up the stack will handle other errors.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
tz, err := getUserTimeZone(id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, ErrUserNotFound) {
|
||||||
|
// User doesn't exist. Use UTC.
|
||||||
|
tz = time.UTC
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("get user %q: %w", id, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
137
src/error-type.md
Normal file
137
src/error-type.md
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Error Types
|
||||||
|
|
||||||
|
There are few options for declaring errors.
|
||||||
|
Consider the following before picking the option best suited for your use case.
|
||||||
|
|
||||||
|
- Does the caller need to match the error so that they can handle it?
|
||||||
|
If yes, we must support the [`errors.Is`] or [`errors.As`] functions
|
||||||
|
by declaring a top-level error variable or a custom type.
|
||||||
|
- Is the error message a static string,
|
||||||
|
or is it a dynamic string that requires contextual information?
|
||||||
|
For the former, we can use [`errors.New`], but for the latter we must
|
||||||
|
use [`fmt.Errorf`] or a custom error type.
|
||||||
|
- Are we propagating a new error returned by a downstream function?
|
||||||
|
If so, see the [section on error wrapping](error-wrap.md).
|
||||||
|
|
||||||
|
[`errors.Is`]: https://pkg.go.dev/errors#Is
|
||||||
|
[`errors.As`]: https://pkg.go.dev/errors#As
|
||||||
|
|
||||||
|
| Error matching? | Error Message | Guidance |
|
||||||
|
|-----------------|---------------|-------------------------------------|
|
||||||
|
| No | static | [`errors.New`] |
|
||||||
|
| No | dynamic | [`fmt.Errorf`] |
|
||||||
|
| Yes | static | top-level `var` with [`errors.New`] |
|
||||||
|
| Yes | dynamic | custom `error` type |
|
||||||
|
|
||||||
|
[`errors.New`]: https://pkg.go.dev/errors#New
|
||||||
|
[`fmt.Errorf`]: https://pkg.go.dev/fmt#Errorf
|
||||||
|
|
||||||
|
For example,
|
||||||
|
use [`errors.New`] for an error with a static string.
|
||||||
|
Export this error as a variable to support matching it with `errors.Is`
|
||||||
|
if the caller needs to match and handle this error.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>No error matching</th><th>Error matching</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// package foo
|
||||||
|
|
||||||
|
func Open() error {
|
||||||
|
return errors.New("could not open")
|
||||||
|
}
|
||||||
|
|
||||||
|
// package bar
|
||||||
|
|
||||||
|
if err := foo.Open(); err != nil {
|
||||||
|
// Can't handle the error.
|
||||||
|
panic("unknown error")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// package foo
|
||||||
|
|
||||||
|
var ErrCouldNotOpen = errors.New("could not open")
|
||||||
|
|
||||||
|
func Open() error {
|
||||||
|
return ErrCouldNotOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// package bar
|
||||||
|
|
||||||
|
if err := foo.Open(); err != nil {
|
||||||
|
if errors.Is(err, foo.ErrCouldNotOpen) {
|
||||||
|
// handle the error
|
||||||
|
} else {
|
||||||
|
panic("unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
For an error with a dynamic string,
|
||||||
|
use [`fmt.Errorf`] if the caller does not need to match it,
|
||||||
|
and a custom `error` if the caller does need to match it.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>No error matching</th><th>Error matching</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// package foo
|
||||||
|
|
||||||
|
func Open(file string) error {
|
||||||
|
return fmt.Errorf("file %q not found", file)
|
||||||
|
}
|
||||||
|
|
||||||
|
// package bar
|
||||||
|
|
||||||
|
if err := foo.Open("testfile.txt"); err != nil {
|
||||||
|
// Can't handle the error.
|
||||||
|
panic("unknown error")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// package foo
|
||||||
|
|
||||||
|
type NotFoundError struct {
|
||||||
|
File string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NotFoundError) Error() string {
|
||||||
|
return fmt.Sprintf("file %q not found", e.File)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Open(file string) error {
|
||||||
|
return &NotFoundError{File: file}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// package bar
|
||||||
|
|
||||||
|
if err := foo.Open("testfile.txt"); err != nil {
|
||||||
|
var notFound *NotFoundError
|
||||||
|
if errors.As(err, ¬Found) {
|
||||||
|
// handle the error
|
||||||
|
} else {
|
||||||
|
panic("unknown error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Note that if you export error variables or types from a package,
|
||||||
|
they will become part of the public API of the package.
|
||||||
79
src/error-wrap.md
Normal file
79
src/error-wrap.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Error Wrapping
|
||||||
|
|
||||||
|
There are three main options for propagating errors if a call fails:
|
||||||
|
|
||||||
|
- return the original error as-is
|
||||||
|
- add context with `fmt.Errorf` and the `%w` verb
|
||||||
|
- add context with `fmt.Errorf` and the `%v` verb
|
||||||
|
|
||||||
|
Return the original error as-is if there is no additional context to add.
|
||||||
|
This maintains the original error type and message.
|
||||||
|
This is well suited for cases when the underlying error message
|
||||||
|
has sufficient information to track down where it came from.
|
||||||
|
|
||||||
|
Otherwise, add context to the error message where possible
|
||||||
|
so that instead of a vague error such as "connection refused",
|
||||||
|
you get more useful errors such as "call service foo: connection refused".
|
||||||
|
|
||||||
|
Use `fmt.Errorf` to add context to your errors,
|
||||||
|
picking between the `%w` or `%v` verbs
|
||||||
|
based on whether the caller should be able to
|
||||||
|
match and extract the underlying cause.
|
||||||
|
|
||||||
|
- Use `%w` if the caller should have access to the underlying error.
|
||||||
|
This is a good default for most wrapped errors,
|
||||||
|
but be aware that callers may begin to rely on this behavior.
|
||||||
|
So for cases where the wrapped error is a known `var` or type,
|
||||||
|
document and test it as part of your function's contract.
|
||||||
|
- Use `%v` to obfuscate the underlying error.
|
||||||
|
Callers will be unable to match it,
|
||||||
|
but you can switch to `%w` in the future if needed.
|
||||||
|
|
||||||
|
When adding context to returned errors, keep the context succinct by avoiding
|
||||||
|
phrases like "failed to", which state the obvious and pile up as the error
|
||||||
|
percolates up through the stack:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
s, err := store.New()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"failed to create new store: %w", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
s, err := store.New()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"new store: %w", err)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr><tr><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
failed to x: failed to y: failed to create new store: the error
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
x: y: new store: the error
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
However once the error is sent to another system, it should be clear the
|
||||||
|
message is an error (e.g. an `err` tag or "Failed" prefix in logs).
|
||||||
|
|
||||||
|
See also [Don't just check errors, handle them gracefully].
|
||||||
|
|
||||||
|
[Don't just check errors, handle them gracefully]: https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
|
||||||
76
src/exit-main.md
Normal file
76
src/exit-main.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Exit in Main
|
||||||
|
|
||||||
|
Go programs use [`os.Exit`] or [`log.Fatal*`] to exit immediately. (Panicking
|
||||||
|
is not a good way to exit programs, please [don't panic](panic.md).)
|
||||||
|
|
||||||
|
[`os.Exit`]: https://pkg.go.dev/os#Exit
|
||||||
|
[`log.Fatal*`]: https://pkg.go.dev/log#Fatal
|
||||||
|
|
||||||
|
Call one of `os.Exit` or `log.Fatal*` **only in `main()`**. All other
|
||||||
|
functions should return errors to signal failure.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
body := readFile(path)
|
||||||
|
fmt.Println(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(path string) string {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
body, err := readFile(path)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readFile(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(b), nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Rationale: Programs with multiple functions that exit present a few issues:
|
||||||
|
|
||||||
|
- Non-obvious control flow: Any function can exit the program so it becomes
|
||||||
|
difficult to reason about the control flow.
|
||||||
|
- Difficult to test: A function that exits the program will also exit the test
|
||||||
|
calling it. This makes the function difficult to test and introduces risk of
|
||||||
|
skipping other tests that have not yet been run by `go test`.
|
||||||
|
- Skipped cleanup: When a function exits the program, it skips function calls
|
||||||
|
enqueued with `defer` statements. This adds risk of skipping important
|
||||||
|
cleanup tasks.
|
||||||
117
src/exit-once.md
Normal file
117
src/exit-once.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# Exit Once
|
||||||
|
|
||||||
|
If possible, prefer to call `os.Exit` or `log.Fatal` **at most once** in your
|
||||||
|
`main()`. If there are multiple error scenarios that halt program execution,
|
||||||
|
put that logic under a separate function and return errors from it.
|
||||||
|
|
||||||
|
This has the effect of shortening your `main()` function and putting all key
|
||||||
|
business logic into a separate, testable function.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
args := os.Args[1:]
|
||||||
|
if len(args) != 1 {
|
||||||
|
log.Fatal("missing file")
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// If we call log.Fatal after this line,
|
||||||
|
// f.Close will not be called.
|
||||||
|
|
||||||
|
b, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() error {
|
||||||
|
args := os.Args[1:]
|
||||||
|
if len(args) != 1 {
|
||||||
|
return errors.New("missing file")
|
||||||
|
}
|
||||||
|
name := args[0]
|
||||||
|
|
||||||
|
f, err := os.Open(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
The example above uses `log.Fatal`, but the guidance also applies to
|
||||||
|
`os.Exit` or any library code that calls `os.Exit`.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
if err := run(); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You may alter the signature of `run()` to fit your needs.
|
||||||
|
For example, if your program must exit with specific exit codes for failures,
|
||||||
|
`run()` may return the exit code instead of an error.
|
||||||
|
This allows unit tests to verify this behavior directly as well.
|
||||||
|
|
||||||
|
```go
|
||||||
|
func main() {
|
||||||
|
os.Exit(run(args))
|
||||||
|
}
|
||||||
|
|
||||||
|
func run() (exitCode int) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
More generally, note that the `run()` function used in these examples
|
||||||
|
is not intended to be prescriptive.
|
||||||
|
There's flexibility in the name, signature, and setup of the `run()` function.
|
||||||
|
Among other things, you may:
|
||||||
|
|
||||||
|
- accept unparsed command line arguments (e.g., `run(os.Args[1:])`)
|
||||||
|
- parse command line arguments in `main()` and pass them onto `run`
|
||||||
|
- use a custom error type to carry the exit code back to `main()`
|
||||||
|
- put business logic in a different layer of abstraction from `package main`
|
||||||
|
|
||||||
|
This guidance only requires that there's a single place in your `main()`
|
||||||
|
responsible for actually exiting the process.
|
||||||
8
src/function-name.md
Normal file
8
src/function-name.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Function Names
|
||||||
|
|
||||||
|
We follow the Go community's convention of using [MixedCaps for function
|
||||||
|
names]. An exception is made for test functions, which may contain underscores
|
||||||
|
for the purpose of grouping related test cases, e.g.,
|
||||||
|
`TestMyFunction_WhatIsBeingTested`.
|
||||||
|
|
||||||
|
[MixedCaps for function names]: https://go.dev/doc/effective_go#mixed-caps
|
||||||
55
src/function-order.md
Normal file
55
src/function-order.md
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
# Function Grouping and Ordering
|
||||||
|
|
||||||
|
- Functions should be sorted in rough call order.
|
||||||
|
- Functions in a file should be grouped by receiver.
|
||||||
|
|
||||||
|
Therefore, exported functions should appear first in a file, after
|
||||||
|
`struct`, `const`, `var` definitions.
|
||||||
|
|
||||||
|
A `newXYZ()`/`NewXYZ()` may appear after the type is defined, but before the
|
||||||
|
rest of the methods on the receiver.
|
||||||
|
|
||||||
|
Since functions are grouped by receiver, plain utility functions should appear
|
||||||
|
towards the end of the file.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (s *something) Cost() {
|
||||||
|
return calcCost(s.weights)
|
||||||
|
}
|
||||||
|
|
||||||
|
type something struct{ ... }
|
||||||
|
|
||||||
|
func calcCost(n []int) int {...}
|
||||||
|
|
||||||
|
func (s *something) Stop() {...}
|
||||||
|
|
||||||
|
func newSomething() *something {
|
||||||
|
return &something{}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type something struct{ ... }
|
||||||
|
|
||||||
|
func newSomething() *something {
|
||||||
|
return &something{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *something) Cost() {
|
||||||
|
return calcCost(s.weights)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *something) Stop() {...}
|
||||||
|
|
||||||
|
func calcCost(n []int) int {...}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
157
src/functional-option.md
Normal file
157
src/functional-option.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Functional Options
|
||||||
|
|
||||||
|
Functional options is a pattern in which you declare an opaque `Option` type
|
||||||
|
that records information in some internal struct. You accept a variadic number
|
||||||
|
of these options and act upon the full information recorded by the options on
|
||||||
|
the internal struct.
|
||||||
|
|
||||||
|
Use this pattern for optional arguments in constructors and other public APIs
|
||||||
|
that you foresee needing to expand, especially if you already have three or
|
||||||
|
more arguments on those functions.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// package db
|
||||||
|
|
||||||
|
func Open(
|
||||||
|
addr string,
|
||||||
|
cache bool,
|
||||||
|
logger *zap.Logger
|
||||||
|
) (*Connection, error) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// package db
|
||||||
|
|
||||||
|
type Option interface {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCache(c bool) Option {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(log *zap.Logger) Option {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open creates a connection.
|
||||||
|
func Open(
|
||||||
|
addr string,
|
||||||
|
opts ...Option,
|
||||||
|
) (*Connection, error) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
The cache and logger parameters must always be provided, even if the user
|
||||||
|
wants to use the default.
|
||||||
|
|
||||||
|
```go
|
||||||
|
db.Open(addr, db.DefaultCache, zap.NewNop())
|
||||||
|
db.Open(addr, db.DefaultCache, log)
|
||||||
|
db.Open(addr, false /* cache */, zap.NewNop())
|
||||||
|
db.Open(addr, false /* cache */, log)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
Options are provided only if needed.
|
||||||
|
|
||||||
|
```go
|
||||||
|
db.Open(addr)
|
||||||
|
db.Open(addr, db.WithLogger(log))
|
||||||
|
db.Open(addr, db.WithCache(false))
|
||||||
|
db.Open(
|
||||||
|
addr,
|
||||||
|
db.WithCache(false),
|
||||||
|
db.WithLogger(log),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Our suggested way of implementing this pattern is with an `Option` interface
|
||||||
|
that holds an unexported method, recording options on an unexported `options`
|
||||||
|
struct.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type options struct {
|
||||||
|
cache bool
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option interface {
|
||||||
|
apply(*options)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cacheOption bool
|
||||||
|
|
||||||
|
func (c cacheOption) apply(opts *options) {
|
||||||
|
opts.cache = bool(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithCache(c bool) Option {
|
||||||
|
return cacheOption(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type loggerOption struct {
|
||||||
|
Log *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l loggerOption) apply(opts *options) {
|
||||||
|
opts.logger = l.Log
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogger(log *zap.Logger) Option {
|
||||||
|
return loggerOption{Log: log}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open creates a connection.
|
||||||
|
func Open(
|
||||||
|
addr string,
|
||||||
|
opts ...Option,
|
||||||
|
) (*Connection, error) {
|
||||||
|
options := options{
|
||||||
|
cache: defaultCache,
|
||||||
|
logger: zap.NewNop(),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range opts {
|
||||||
|
o.apply(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that there's a method of implementing this pattern with closures but we
|
||||||
|
believe that the pattern above provides more flexibility for authors and is
|
||||||
|
easier to debug and test for users. In particular, it allows options to be
|
||||||
|
compared against each other in tests and mocks, versus closures where this is
|
||||||
|
impossible. Further, it lets options implement other interfaces, including
|
||||||
|
`fmt.Stringer` which allows for user-readable string representations of the
|
||||||
|
options.
|
||||||
|
|
||||||
|
See also,
|
||||||
|
|
||||||
|
- [Self-referential functions and the design of options]
|
||||||
|
- [Functional options for friendly APIs]
|
||||||
|
|
||||||
|
[Self-referential functions and the design of options]: https://commandcenter.blogspot.com/2014/01/self-referential-functions-and-design.html
|
||||||
|
[Functional options for friendly APIs]: https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis
|
||||||
|
|
||||||
|
<!-- TODO: replace this with parameter structs and functional options, when to
|
||||||
|
use one vs other -->
|
||||||
42
src/global-decl.md
Normal file
42
src/global-decl.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# Top-level Variable Declarations
|
||||||
|
|
||||||
|
At the top level, use the standard `var` keyword. Do not specify the type,
|
||||||
|
unless it is not the same type as the expression.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var _s string = F()
|
||||||
|
|
||||||
|
func F() string { return "A" }
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var _s = F()
|
||||||
|
// Since F already states that it returns a string, we don't need to specify
|
||||||
|
// the type again.
|
||||||
|
|
||||||
|
func F() string { return "A" }
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Specify the type if the type of the expression does not match the desired type
|
||||||
|
exactly.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type myError struct{}
|
||||||
|
|
||||||
|
func (myError) Error() string { return "error" }
|
||||||
|
|
||||||
|
func F() myError { return myError{} }
|
||||||
|
|
||||||
|
var _e error = F()
|
||||||
|
// F returns an object of type myError but we want error.
|
||||||
|
```
|
||||||
76
src/global-mut.md
Normal file
76
src/global-mut.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Avoid Mutable Globals
|
||||||
|
|
||||||
|
Avoid mutating global variables, instead opting for dependency injection.
|
||||||
|
This applies to function pointers as well as other kinds of values.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// sign.go
|
||||||
|
|
||||||
|
var _timeNow = time.Now
|
||||||
|
|
||||||
|
func sign(msg string) string {
|
||||||
|
now := _timeNow()
|
||||||
|
return signWithTime(msg, now)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// sign.go
|
||||||
|
|
||||||
|
type signer struct {
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSigner() *signer {
|
||||||
|
return &signer{
|
||||||
|
now: time.Now,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *signer) Sign(msg string) string {
|
||||||
|
now := s.now()
|
||||||
|
return signWithTime(msg, now)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// sign_test.go
|
||||||
|
|
||||||
|
func TestSign(t *testing.T) {
|
||||||
|
oldTimeNow := _timeNow
|
||||||
|
_timeNow = func() time.Time {
|
||||||
|
return someFixedTime
|
||||||
|
}
|
||||||
|
defer func() { _timeNow = oldTimeNow }()
|
||||||
|
|
||||||
|
assert.Equal(t, want, sign(give))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// sign_test.go
|
||||||
|
|
||||||
|
func TestSigner(t *testing.T) {
|
||||||
|
s := newSigner()
|
||||||
|
s.now = func() time.Time {
|
||||||
|
return someFixedTime
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, want, s.Sign(give))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
50
src/global-name.md
Normal file
50
src/global-name.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# Prefix Unexported Globals with _
|
||||||
|
|
||||||
|
Prefix unexported top-level `var`s and `const`s with `_` to make it clear when
|
||||||
|
they are used that they are global symbols.
|
||||||
|
|
||||||
|
Rationale: Top-level variables and constants have a package scope. Using a
|
||||||
|
generic name makes it easy to accidentally use the wrong value in a different
|
||||||
|
file.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// foo.go
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultPort = 8080
|
||||||
|
defaultUser = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
// bar.go
|
||||||
|
|
||||||
|
func Bar() {
|
||||||
|
defaultPort := 9090
|
||||||
|
...
|
||||||
|
fmt.Println("Default port", defaultPort)
|
||||||
|
|
||||||
|
// We will not see a compile error if the first line of
|
||||||
|
// Bar() is deleted.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// foo.go
|
||||||
|
|
||||||
|
const (
|
||||||
|
_defaultPort = 8080
|
||||||
|
_defaultUser = "user"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
**Exception**: Unexported error values may use the prefix `err` without the underscore.
|
||||||
|
See [Error Naming](error-name.md).
|
||||||
32
src/goroutine-exit.md
Normal file
32
src/goroutine-exit.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Wait for goroutines to exit
|
||||||
|
|
||||||
|
Given a goroutine spawned by the system,
|
||||||
|
there must be a way to wait for the goroutine to exit.
|
||||||
|
There are two popular ways to do this:
|
||||||
|
|
||||||
|
- Use a `sync.WaitGroup` to wait for multiple goroutines to complete.
|
||||||
|
Do this if there are multiple goroutines that you want to wait for.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for i := 0; i < N; i++ {
|
||||||
|
wg.Go(...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// To wait for all to finish:
|
||||||
|
wg.Wait()
|
||||||
|
```
|
||||||
|
|
||||||
|
- Add another `chan struct{}` that the goroutine closes when it's done.
|
||||||
|
Do this if there's only one goroutine.
|
||||||
|
|
||||||
|
```go
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
// ...
|
||||||
|
}()
|
||||||
|
|
||||||
|
// To wait for the goroutine to finish:
|
||||||
|
<-done
|
||||||
|
```
|
||||||
79
src/goroutine-forget.md
Normal file
79
src/goroutine-forget.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Don't fire-and-forget goroutines
|
||||||
|
|
||||||
|
Goroutines are lightweight, but they're not free:
|
||||||
|
at minimum, they cost memory for their stack and CPU to be scheduled.
|
||||||
|
While these costs are small for typical uses of goroutines,
|
||||||
|
they can cause significant performance issues
|
||||||
|
when spawned in large numbers without controlled lifetimes.
|
||||||
|
Goroutines with unmanaged lifetimes can also cause other issues
|
||||||
|
like preventing unused objects from being garbage collected
|
||||||
|
and holding onto resources that are otherwise no longer used.
|
||||||
|
|
||||||
|
Therefore, do not leak goroutines in production code.
|
||||||
|
Use [go.uber.org/goleak](https://pkg.go.dev/go.uber.org/goleak)
|
||||||
|
to test for goroutine leaks inside packages that may spawn goroutines.
|
||||||
|
|
||||||
|
In general, every goroutine:
|
||||||
|
|
||||||
|
- must have a predictable time at which it will stop running; or
|
||||||
|
- there must be a way to signal to the goroutine that it should stop
|
||||||
|
|
||||||
|
In both cases, there must be a way for code to block and wait for the goroutine to
|
||||||
|
finish.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
flush()
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
stop = make(chan struct{}) // tells the goroutine to stop
|
||||||
|
done = make(chan struct{}) // tells us that the goroutine exited
|
||||||
|
)
|
||||||
|
go func() {
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
ticker := time.NewTicker(delay)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
flush()
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Elsewhere...
|
||||||
|
close(stop) // signal the goroutine to stop
|
||||||
|
<-done // and wait for it to exit
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
There's no way to stop this goroutine.
|
||||||
|
This will run until the application exits.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
This goroutine can be stopped with `close(stop)`,
|
||||||
|
and we can wait for it to exit with `<-done`.
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
78
src/goroutine-init.md
Normal file
78
src/goroutine-init.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# No goroutines in `init()`
|
||||||
|
|
||||||
|
`init()` functions should not spawn goroutines.
|
||||||
|
See also [Avoid init()](init.md).
|
||||||
|
|
||||||
|
If a package has need of a background goroutine,
|
||||||
|
it must expose an object that is responsible for managing a goroutine's
|
||||||
|
lifetime.
|
||||||
|
The object must provide a method (`Close`, `Stop`, `Shutdown`, etc)
|
||||||
|
that signals the background goroutine to stop, and waits for it to exit.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func init() {
|
||||||
|
go doWork()
|
||||||
|
}
|
||||||
|
|
||||||
|
func doWork() {
|
||||||
|
for {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Worker struct{ /* ... */ }
|
||||||
|
|
||||||
|
func NewWorker(...) *Worker {
|
||||||
|
w := &Worker{
|
||||||
|
stop: make(chan struct{}),
|
||||||
|
done: make(chan struct{}),
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
go w.doWork()
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Worker) doWork() {
|
||||||
|
defer close(w.done)
|
||||||
|
for {
|
||||||
|
// ...
|
||||||
|
case <-w.stop:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shutdown tells the worker to stop
|
||||||
|
// and waits until it has finished.
|
||||||
|
func (w *Worker) Shutdown() {
|
||||||
|
close(w.stop)
|
||||||
|
<-w.done
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
Spawns a background goroutine unconditionally when the user exports this package.
|
||||||
|
The user has no control over the goroutine or a means of stopping it.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
Spawns the worker only if the user requests it.
|
||||||
|
Provides a means of shutting down the worker so that the user can free up
|
||||||
|
resources used by the worker.
|
||||||
|
|
||||||
|
Note that you should use `WaitGroup`s if the worker manages multiple
|
||||||
|
goroutines.
|
||||||
|
See [Wait for goroutines to exit](goroutine-exit.md).
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
46
src/import-alias.md
Normal file
46
src/import-alias.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Import Aliasing
|
||||||
|
|
||||||
|
Import aliasing must be used if the package name does not match the last
|
||||||
|
element of the import path.
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
client "example.com/client-go"
|
||||||
|
trace "example.com/trace/v2"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
In all other scenarios, import aliases should be avoided unless there is a
|
||||||
|
direct conflict between imports.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
runtimetrace "runtime/trace"
|
||||||
|
|
||||||
|
nettrace "golang.net/x/trace"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime/trace"
|
||||||
|
|
||||||
|
nettrace "golang.net/x/trace"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
37
src/import-group.md
Normal file
37
src/import-group.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Import Group Ordering
|
||||||
|
|
||||||
|
There should be two import groups:
|
||||||
|
|
||||||
|
- Standard library
|
||||||
|
- Everything else
|
||||||
|
|
||||||
|
This is the grouping applied by goimports by default.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"go.uber.org/atomic"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
116
src/init.md
Normal file
116
src/init.md
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
# Avoid `init()`
|
||||||
|
|
||||||
|
Avoid `init()` where possible. When `init()` is unavoidable or desirable, code
|
||||||
|
should attempt to:
|
||||||
|
|
||||||
|
1. Be completely deterministic, regardless of program environment or invocation.
|
||||||
|
2. Avoid depending on the ordering or side-effects of other `init()` functions.
|
||||||
|
While `init()` ordering is well-known, code can change, and thus
|
||||||
|
relationships between `init()` functions can make code brittle and
|
||||||
|
error-prone.
|
||||||
|
3. Avoid accessing or manipulating global or environment state, such as machine
|
||||||
|
information, environment variables, working directory, program
|
||||||
|
arguments/inputs, etc.
|
||||||
|
4. Avoid I/O, including both filesystem, network, and system calls.
|
||||||
|
|
||||||
|
Code that cannot satisfy these requirements likely belongs as a helper to be
|
||||||
|
called as part of `main()` (or elsewhere in a program's lifecycle), or be
|
||||||
|
written as part of `main()` itself. In particular, libraries that are intended
|
||||||
|
to be used by other programs should take special care to be completely
|
||||||
|
deterministic and not perform "init magic".
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Foo struct {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
var _defaultFoo Foo
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
_defaultFoo = Foo{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var _defaultFoo = Foo{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
// or, better, for testability:
|
||||||
|
|
||||||
|
var _defaultFoo = defaultFoo()
|
||||||
|
|
||||||
|
func defaultFoo() Foo {
|
||||||
|
return Foo{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
var _config Config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Bad: based on current directory
|
||||||
|
cwd, _ := os.Getwd()
|
||||||
|
|
||||||
|
// Bad: I/O
|
||||||
|
raw, _ := os.ReadFile(
|
||||||
|
path.Join(cwd, "config", "config.yaml"),
|
||||||
|
)
|
||||||
|
|
||||||
|
yaml.Unmarshal(raw, &_config)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Config struct {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() Config {
|
||||||
|
cwd, err := os.Getwd()
|
||||||
|
// handle err
|
||||||
|
|
||||||
|
raw, err := os.ReadFile(
|
||||||
|
path.Join(cwd, "config", "config.yaml"),
|
||||||
|
)
|
||||||
|
// handle err
|
||||||
|
|
||||||
|
var config Config
|
||||||
|
yaml.Unmarshal(raw, &config)
|
||||||
|
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Considering the above, some situations in which `init()` may be preferable or
|
||||||
|
necessary might include:
|
||||||
|
|
||||||
|
- Complex expressions that cannot be represented as single assignments.
|
||||||
|
- Pluggable hooks, such as `database/sql` dialects, encoding type registries, etc.
|
||||||
|
- Optimizations to [Google Cloud Functions] and other forms of deterministic
|
||||||
|
precomputation.
|
||||||
|
|
||||||
|
[Google Cloud Functions]: https://cloud.google.com/functions/docs/bestpractices/tips#use_global_variables_to_reuse_objects_in_future_invocations
|
||||||
72
src/interface-compliance.md
Normal file
72
src/interface-compliance.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Verify Interface Compliance
|
||||||
|
|
||||||
|
Verify interface compliance at compile time where appropriate. This includes:
|
||||||
|
|
||||||
|
- Exported types that are required to implement specific interfaces as part of
|
||||||
|
their API contract
|
||||||
|
- Exported or unexported types that are part of a collection of types
|
||||||
|
implementing the same interface
|
||||||
|
- Other cases where violating an interface would break users
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Handler struct {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func (h *Handler) ServeHTTP(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
) {
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Handler struct {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.Handler = (*Handler)(nil)
|
||||||
|
|
||||||
|
func (h *Handler) ServeHTTP(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
The statement `var _ http.Handler = (*Handler)(nil)` will fail to compile if
|
||||||
|
`*Handler` ever stops matching the `http.Handler` interface.
|
||||||
|
|
||||||
|
The right hand side of the assignment should be the zero value of the asserted
|
||||||
|
type. This is `nil` for pointer types (like `*Handler`), slices, and maps, and
|
||||||
|
an empty struct for struct types.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type LogHandler struct {
|
||||||
|
h http.Handler
|
||||||
|
log *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ http.Handler = LogHandler{}
|
||||||
|
|
||||||
|
func (h LogHandler) ServeHTTP(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
14
src/interface-pointer.md
Normal file
14
src/interface-pointer.md
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Pointers to Interfaces
|
||||||
|
|
||||||
|
You almost never need a pointer to an interface. You should be passing
|
||||||
|
interfaces as values—the underlying data can still be a pointer.
|
||||||
|
|
||||||
|
An interface is two fields:
|
||||||
|
|
||||||
|
1. A pointer to some type-specific information. You can think of this as
|
||||||
|
"type."
|
||||||
|
2. Data pointer. If the data stored is a pointer, it’s stored directly. If
|
||||||
|
the data stored is a value, then a pointer to the value is stored.
|
||||||
|
|
||||||
|
If you want interface methods to modify the underlying data, you must use a
|
||||||
|
pointer.
|
||||||
78
src/interface-receiver.md
Normal file
78
src/interface-receiver.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# Receivers and Interfaces
|
||||||
|
|
||||||
|
Methods with value receivers can be called on pointers as well as values.
|
||||||
|
Methods with pointer receivers can only be called on pointers or [addressable values].
|
||||||
|
|
||||||
|
[addressable values]: https://go.dev/ref/spec#Method_values
|
||||||
|
|
||||||
|
For example,
|
||||||
|
|
||||||
|
```go
|
||||||
|
type S struct {
|
||||||
|
data string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s S) Read() string {
|
||||||
|
return s.data
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *S) Write(str string) {
|
||||||
|
s.data = str
|
||||||
|
}
|
||||||
|
|
||||||
|
// We cannot get pointers to values stored in maps, because they are not
|
||||||
|
// addressable values.
|
||||||
|
sVals := map[int]S{1: {"A"}}
|
||||||
|
|
||||||
|
// We can call Read on values stored in the map because Read
|
||||||
|
// has a value receiver, which does not require the value to
|
||||||
|
// be addressable.
|
||||||
|
sVals[1].Read()
|
||||||
|
|
||||||
|
// We cannot call Write on values stored in the map because Write
|
||||||
|
// has a pointer receiver, and it's not possible to get a pointer
|
||||||
|
// to a value stored in a map.
|
||||||
|
//
|
||||||
|
// sVals[1].Write("test")
|
||||||
|
|
||||||
|
sPtrs := map[int]*S{1: {"A"}}
|
||||||
|
|
||||||
|
// You can call both Read and Write if the map stores pointers,
|
||||||
|
// because pointers are intrinsically addressable.
|
||||||
|
sPtrs[1].Read()
|
||||||
|
sPtrs[1].Write("test")
|
||||||
|
```
|
||||||
|
|
||||||
|
Similarly, an interface can be satisfied by a pointer, even if the method has a
|
||||||
|
value receiver.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type F interface {
|
||||||
|
f()
|
||||||
|
}
|
||||||
|
|
||||||
|
type S1 struct{}
|
||||||
|
|
||||||
|
func (s S1) f() {}
|
||||||
|
|
||||||
|
type S2 struct{}
|
||||||
|
|
||||||
|
func (s *S2) f() {}
|
||||||
|
|
||||||
|
s1Val := S1{}
|
||||||
|
s1Ptr := &S1{}
|
||||||
|
s2Val := S2{}
|
||||||
|
s2Ptr := &S2{}
|
||||||
|
|
||||||
|
var i F
|
||||||
|
i = s1Val
|
||||||
|
i = s1Ptr
|
||||||
|
i = s2Ptr
|
||||||
|
|
||||||
|
// The following doesn't compile, since s2Val is a value, and there is no value receiver for f.
|
||||||
|
// i = s2Val
|
||||||
|
```
|
||||||
|
|
||||||
|
Effective Go has a good write up on [Pointers vs. Values].
|
||||||
|
|
||||||
|
[Pointers vs. Values]: https://go.dev/doc/effective_go#pointers_vs_values
|
||||||
37
src/intro.md
Normal file
37
src/intro.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
Styles are the conventions that govern our code. The term style is a bit of a
|
||||||
|
misnomer, since these conventions cover far more than just source file
|
||||||
|
formatting—gofmt handles that for us.
|
||||||
|
|
||||||
|
The goal of this guide is to manage this complexity by describing in detail the
|
||||||
|
Dos and Don'ts of writing Go code at Uber. These rules exist to keep the code
|
||||||
|
base manageable while still allowing engineers to use Go language features
|
||||||
|
productively.
|
||||||
|
|
||||||
|
This guide was originally created by [Prashant Varanasi] and [Simon Newton] as
|
||||||
|
a way to bring some colleagues up to speed with using Go. Over the years it has
|
||||||
|
been amended based on feedback from others.
|
||||||
|
|
||||||
|
[Prashant Varanasi]: https://github.com/prashantv
|
||||||
|
[Simon Newton]: https://github.com/nomis52
|
||||||
|
|
||||||
|
This documents idiomatic conventions in Go code that we follow at Uber. A lot
|
||||||
|
of these are general guidelines for Go, while others extend upon external
|
||||||
|
resources:
|
||||||
|
|
||||||
|
1. [Effective Go](https://go.dev/doc/effective_go)
|
||||||
|
2. [Go Common Mistakes](https://go.dev/wiki/CommonMistakes)
|
||||||
|
3. [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments)
|
||||||
|
|
||||||
|
We aim for the code samples to be accurate for the two most recent minor versions
|
||||||
|
of Go [releases](https://go.dev/doc/devel/release).
|
||||||
|
|
||||||
|
All code should be error-free when run through `golint` and `go vet`. We
|
||||||
|
recommend setting up your editor to:
|
||||||
|
|
||||||
|
- Run `goimports` on save
|
||||||
|
- Run `golint` and `go vet` to check for errors
|
||||||
|
|
||||||
|
You can find information in editor support for Go tools here:
|
||||||
|
<https://go.dev/wiki/IDEsAndTextEditorPlugins>
|
||||||
9
src/line-length.md
Normal file
9
src/line-length.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Avoid overly long lines
|
||||||
|
|
||||||
|
Avoid lines of code that require readers to scroll horizontally
|
||||||
|
or turn their heads too much.
|
||||||
|
|
||||||
|
We recommend a soft line length limit of **99 characters**.
|
||||||
|
Authors should aim to wrap lines before hitting this limit,
|
||||||
|
but it is not a hard limit.
|
||||||
|
Code is allowed to exceed this limit.
|
||||||
39
src/lint.md
Normal file
39
src/lint.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Linting
|
||||||
|
|
||||||
|
More importantly than any "blessed" set of linters, lint consistently across a
|
||||||
|
codebase.
|
||||||
|
|
||||||
|
We recommend using the following linters at a minimum, because we feel that they
|
||||||
|
help to catch the most common issues and also establish a high bar for code
|
||||||
|
quality without being unnecessarily prescriptive:
|
||||||
|
|
||||||
|
- [errcheck] to ensure that errors are handled
|
||||||
|
- [goimports] to format code and manage imports
|
||||||
|
- [revive] to point out common style mistakes
|
||||||
|
- [govet] to analyze code for common mistakes
|
||||||
|
- [staticcheck] to do various static analysis checks
|
||||||
|
|
||||||
|
[errcheck]: https://github.com/kisielk/errcheck
|
||||||
|
[goimports]: https://pkg.go.dev/golang.org/x/tools/cmd/goimports
|
||||||
|
[revive]: https://github.com/mgechev/revive
|
||||||
|
[govet]: https://pkg.go.dev/cmd/vet
|
||||||
|
[staticcheck]: https://staticcheck.dev
|
||||||
|
|
||||||
|
> **Note**: [revive] is the modern, faster successor to the now-deprecated [golint].
|
||||||
|
|
||||||
|
## Lint Runners
|
||||||
|
|
||||||
|
We recommend [golangci-lint] as the go-to lint runner for Go code, largely due
|
||||||
|
to its performance in larger codebases and ability to configure and use many
|
||||||
|
canonical linters at once. This repo has an example [.golangci.yml] config file
|
||||||
|
with recommended linters and settings.
|
||||||
|
|
||||||
|
golangci-lint has [various linters] available for use. The above linters are
|
||||||
|
recommended as a base set, and we encourage teams to add any additional linters
|
||||||
|
that make sense for their projects.
|
||||||
|
|
||||||
|
[golangci-lint]: https://github.com/golangci/golangci-lint
|
||||||
|
[.golangci.yml]: https://github.com/uber-go/guide/blob/master/.golangci.yml
|
||||||
|
[various linters]: https://golangci-lint.run/usage/linters/
|
||||||
|
[golint]: https://github.com/golang/lint
|
||||||
|
|
||||||
80
src/map-init.md
Normal file
80
src/map-init.md
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
# Initializing Maps
|
||||||
|
|
||||||
|
Prefer `make(..)` for empty maps, and maps populated
|
||||||
|
programmatically. This makes map initialization visually
|
||||||
|
distinct from declaration, and it makes it easy to add size
|
||||||
|
hints later if available.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
// m1 is safe to read and write;
|
||||||
|
// m2 will panic on writes.
|
||||||
|
m1 = map[T1]T2{}
|
||||||
|
m2 map[T1]T2
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var (
|
||||||
|
// m1 is safe to read and write;
|
||||||
|
// m2 will panic on writes.
|
||||||
|
m1 = make(map[T1]T2)
|
||||||
|
m2 map[T1]T2
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
Declaration and initialization are visually similar.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
Declaration and initialization are visually distinct.
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Where possible, provide capacity hints when initializing
|
||||||
|
maps with `make()`. See
|
||||||
|
[Specifying Map Capacity Hints](container-capacity.md#specifying-map-capacity-hints)
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
On the other hand, if the map holds a fixed list of elements,
|
||||||
|
use map literals to initialize the map.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
m := make(map[T1]T2, 3)
|
||||||
|
m[k1] = v1
|
||||||
|
m[k2] = v2
|
||||||
|
m[k3] = v3
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
m := map[T1]T2{
|
||||||
|
k1: v1,
|
||||||
|
k2: v2,
|
||||||
|
k3: v3,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
The basic rule of thumb is to use map literals when adding a fixed set of
|
||||||
|
elements at initialization time, otherwise use `make` (and specify a size hint
|
||||||
|
if available).
|
||||||
91
src/mutex-zero-value.md
Normal file
91
src/mutex-zero-value.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
# Zero-value Mutexes are Valid
|
||||||
|
|
||||||
|
The zero-value of `sync.Mutex` and `sync.RWMutex` is valid, so you almost
|
||||||
|
never need a pointer to a mutex.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
mu := new(sync.Mutex)
|
||||||
|
mu.Lock()
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var mu sync.Mutex
|
||||||
|
mu.Lock()
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
If you use a struct by pointer, then the mutex should be a non-pointer field on
|
||||||
|
it. Do not embed the mutex on the struct, even if the struct is not exported.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SMap struct {
|
||||||
|
sync.Mutex
|
||||||
|
|
||||||
|
data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSMap() *SMap {
|
||||||
|
return &SMap{
|
||||||
|
data: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SMap) Get(k string) string {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
return m.data[k]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type SMap struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
|
||||||
|
data map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSMap() *SMap {
|
||||||
|
return &SMap{
|
||||||
|
data: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *SMap) Get(k string) string {
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
|
return m.data[k]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
The `Mutex` field, and the `Lock` and `Unlock` methods are unintentionally part
|
||||||
|
of the exported API of `SMap`.
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
The mutex and its methods are implementation details of `SMap` hidden from its
|
||||||
|
callers.
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
45
src/nest-less.md
Normal file
45
src/nest-less.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Reduce Nesting
|
||||||
|
|
||||||
|
Code should reduce nesting where possible by handling error cases/special
|
||||||
|
conditions first and returning early or continuing the loop. Reduce the amount
|
||||||
|
of code that is nested multiple levels.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
15
src/package-name.md
Normal file
15
src/package-name.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Package Names
|
||||||
|
|
||||||
|
When naming packages, choose a name that is:
|
||||||
|
|
||||||
|
- All lower-case. No capitals or underscores.
|
||||||
|
- Does not need to be renamed using named imports at most call sites.
|
||||||
|
- Short and succinct. Remember that the name is identified in full at every call
|
||||||
|
site.
|
||||||
|
- Not plural. For example, `net/url`, not `net/urls`.
|
||||||
|
- Not "common", "util", "shared", or "lib". These are bad, uninformative names.
|
||||||
|
|
||||||
|
See also [Package Names] and [Style guideline for Go packages].
|
||||||
|
|
||||||
|
[Package Names]: https://go.dev/blog/package-names
|
||||||
|
[Style guideline for Go packages]: https://rakyll.org/style-packages/
|
||||||
87
src/panic.md
Normal file
87
src/panic.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# Don't Panic
|
||||||
|
|
||||||
|
Code running in production must avoid panics. Panics are a major source of
|
||||||
|
[cascading failures]. If an error occurs, the function must return an error and
|
||||||
|
allow the caller to decide how to handle it.
|
||||||
|
|
||||||
|
[cascading failures]: https://en.wikipedia.org/wiki/Cascading_failure
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func run(args []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
panic("an argument is required")
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
run(os.Args[1:])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func run(args []string) error {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return errors.New("an argument is required")
|
||||||
|
}
|
||||||
|
// ...
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if err := run(os.Args[1:]); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Panic/recover is not an error handling strategy. A program must panic only when
|
||||||
|
something irrecoverable happens such as a nil dereference. An exception to this is
|
||||||
|
program initialization: bad things at program startup that should abort the
|
||||||
|
program may cause panic.
|
||||||
|
|
||||||
|
```go
|
||||||
|
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
|
||||||
|
```
|
||||||
|
|
||||||
|
Even in tests, prefer `t.Fatal` or `t.FailNow` over panics to ensure that the
|
||||||
|
test is marked as failed.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// func TestFoo(t *testing.T)
|
||||||
|
|
||||||
|
f, err := os.CreateTemp("", "test")
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to set up test")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// func TestFoo(t *testing.T)
|
||||||
|
|
||||||
|
f, err := os.CreateTemp("", "test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("failed to set up test")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
49
src/param-naked.md
Normal file
49
src/param-naked.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# Avoid Naked Parameters
|
||||||
|
|
||||||
|
Naked parameters in function calls can hurt readability. Add C-style comments
|
||||||
|
(`/* ... */`) for parameter names when their meaning is not obvious.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// func printInfo(name string, isLocal, done bool)
|
||||||
|
|
||||||
|
printInfo("foo", true, true)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// func printInfo(name string, isLocal, done bool)
|
||||||
|
|
||||||
|
printInfo("foo", true /* isLocal */, true /* done */)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Better yet, replace naked `bool` types with custom types for more readable and
|
||||||
|
type-safe code. This allows more than just two states (true/false) for that
|
||||||
|
parameter in the future.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Region int
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnknownRegion Region = iota
|
||||||
|
Local
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusReady Status = iota + 1
|
||||||
|
StatusDone
|
||||||
|
// Maybe we will have a StatusInProgress in the future.
|
||||||
|
)
|
||||||
|
|
||||||
|
func printInfo(name string, region Region, status Status)
|
||||||
|
```
|
||||||
3
src/performance.md
Normal file
3
src/performance.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# Performance
|
||||||
|
|
||||||
|
Performance-specific guidelines apply only to the hot path.
|
||||||
7
src/preface.txt
Normal file
7
src/preface.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<!--
|
||||||
|
This file was generated by stitchmd. DO NOT EDIT.
|
||||||
|
To make changes, edit the files in the "src" directory.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- markdownlint-disable MD033 -->
|
||||||
|
|
||||||
26
src/printf-const.md
Normal file
26
src/printf-const.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Format Strings outside Printf
|
||||||
|
|
||||||
|
If you declare format strings for `Printf`-style functions outside a string
|
||||||
|
literal, make them `const` values.
|
||||||
|
|
||||||
|
This helps `go vet` perform static analysis of the format string.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
msg := "unexpected values %v, %v\n"
|
||||||
|
fmt.Printf(msg, 1, 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
const msg = "unexpected values %v, %v\n"
|
||||||
|
fmt.Printf(msg, 1, 2)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
22
src/printf-name.md
Normal file
22
src/printf-name.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Naming Printf-style Functions
|
||||||
|
|
||||||
|
When you declare a `Printf`-style function, make sure that `go vet` can detect
|
||||||
|
it and check the format string.
|
||||||
|
|
||||||
|
This means that you should use predefined `Printf`-style function
|
||||||
|
names if possible. `go vet` will check these by default. See [Printf family]
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
[Printf family]: https://pkg.go.dev/cmd/vet#hdr-Printf_family
|
||||||
|
|
||||||
|
If using the predefined names is not an option, end the name you choose with
|
||||||
|
f: `Wrapf`, not `Wrap`. `go vet` can be asked to check specific `Printf`-style
|
||||||
|
names but they must end with f.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
go vet -printfuncs=wrapf,statusf
|
||||||
|
```
|
||||||
|
|
||||||
|
See also [go vet: Printf family check].
|
||||||
|
|
||||||
|
[go vet: Printf family check]: https://kuzminva.wordpress.com/2017/11/07/go-vet-printf-family-check/
|
||||||
95
src/slice-nil.md
Normal file
95
src/slice-nil.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# nil is a valid slice
|
||||||
|
|
||||||
|
`nil` is a valid slice of length 0. This means that,
|
||||||
|
|
||||||
|
- You should not return a slice of length zero explicitly. Return `nil`
|
||||||
|
instead.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
if x == "" {
|
||||||
|
return []int{}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
if x == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
- To check if a slice is empty, always use `len(s) == 0`. Do not check for
|
||||||
|
`nil`.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func isEmpty(s []string) bool {
|
||||||
|
return s == nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func isEmpty(s []string) bool {
|
||||||
|
return len(s) == 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
- The zero value (a slice declared with `var`) is usable immediately without
|
||||||
|
`make()`.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
nums := []int{}
|
||||||
|
// or, nums := make([]int)
|
||||||
|
|
||||||
|
if add1 {
|
||||||
|
nums = append(nums, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if add2 {
|
||||||
|
nums = append(nums, 2)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var nums []int
|
||||||
|
|
||||||
|
if add1 {
|
||||||
|
nums = append(nums, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if add2 {
|
||||||
|
nums = append(nums, 2)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Remember that, while it is a valid slice, a nil slice is not equivalent to an
|
||||||
|
allocated slice of length 0 - one is nil and the other is not - and the two may
|
||||||
|
be treated differently in different situations (such as serialization).
|
||||||
39
src/strconv.md
Normal file
39
src/strconv.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# Prefer strconv over fmt
|
||||||
|
|
||||||
|
When converting primitives to/from strings, `strconv` is faster than
|
||||||
|
`fmt`.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s := fmt.Sprint(rand.Int())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
s := strconv.Itoa(rand.Int())
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
40
src/string-byte-slice.md
Normal file
40
src/string-byte-slice.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# Avoid repeated string-to-byte conversions
|
||||||
|
|
||||||
|
Do not create byte slices from a fixed string repeatedly. Instead, perform the
|
||||||
|
conversion once and capture the result.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
w.Write([]byte("Hello world"))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
data := []byte("Hello world")
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
w.Write(data)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
BenchmarkBad-4 50000000 22.2 ns/op
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```plain
|
||||||
|
BenchmarkGood-4 500000000 3.25 ns/op
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
23
src/string-escape.md
Normal file
23
src/string-escape.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Use Raw String Literals to Avoid Escaping
|
||||||
|
|
||||||
|
Go supports [raw string literals](https://go.dev/ref/spec#raw_string_lit),
|
||||||
|
which can span multiple lines and include quotes. Use these to avoid
|
||||||
|
hand-escaped strings which are much harder to read.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
wantError := "unknown name:\"test\""
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
wantError := `unknown error:"test"`
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
156
src/struct-embed.md
Normal file
156
src/struct-embed.md
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# Embedding in Structs
|
||||||
|
|
||||||
|
Embedded types should be at the top of the field list of a
|
||||||
|
struct, and there must be an empty line separating embedded fields from regular
|
||||||
|
fields.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Client struct {
|
||||||
|
version int
|
||||||
|
http.Client
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Client struct {
|
||||||
|
http.Client
|
||||||
|
|
||||||
|
version int
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Embedding should provide tangible benefit, like adding or augmenting
|
||||||
|
functionality in a semantically-appropriate way. It should do this with zero
|
||||||
|
adverse user-facing effects (see also: [Avoid Embedding Types in Public Structs](embed-public.md)).
|
||||||
|
|
||||||
|
Exception: Mutexes should not be embedded, even on unexported types. See also: [Zero-value Mutexes are Valid](mutex-zero-value.md).
|
||||||
|
|
||||||
|
Embedding **should not**:
|
||||||
|
|
||||||
|
- Be purely cosmetic or convenience-oriented.
|
||||||
|
- Make outer types more difficult to construct or use.
|
||||||
|
- Affect outer types' zero values. If the outer type has a useful zero value, it
|
||||||
|
should still have a useful zero value after embedding the inner type.
|
||||||
|
- Expose unrelated functions or fields from the outer type as a side-effect of
|
||||||
|
embedding the inner type.
|
||||||
|
- Expose unexported types.
|
||||||
|
- Affect outer types' copy semantics.
|
||||||
|
- Change the outer type's API or type semantics.
|
||||||
|
- Embed a non-canonical form of the inner type.
|
||||||
|
- Expose implementation details of the outer type.
|
||||||
|
- Allow users to observe or control type internals.
|
||||||
|
- Change the general behavior of inner functions through wrapping in a way that
|
||||||
|
would reasonably surprise users.
|
||||||
|
|
||||||
|
Simply put, embed consciously and intentionally. A good litmus test is, "would
|
||||||
|
all of these exported inner methods/fields be added directly to the outer type";
|
||||||
|
if the answer is "some" or "no", don't embed the inner type - use a field
|
||||||
|
instead.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type A struct {
|
||||||
|
// Bad: A.Lock() and A.Unlock() are
|
||||||
|
// now available, provide no
|
||||||
|
// functional benefit, and allow
|
||||||
|
// users to control details about
|
||||||
|
// the internals of A.
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type countingWriteCloser struct {
|
||||||
|
// Good: Write() is provided at this
|
||||||
|
// outer layer for a specific
|
||||||
|
// purpose, and delegates work
|
||||||
|
// to the inner type's Write().
|
||||||
|
io.WriteCloser
|
||||||
|
|
||||||
|
count int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *countingWriteCloser) Write(bs []byte) (int, error) {
|
||||||
|
w.count += len(bs)
|
||||||
|
return w.WriteCloser.Write(bs)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Book struct {
|
||||||
|
// Bad: pointer changes zero value usefulness
|
||||||
|
io.ReadWriter
|
||||||
|
|
||||||
|
// other fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// later
|
||||||
|
|
||||||
|
var b Book
|
||||||
|
b.Read(...) // panic: nil pointer
|
||||||
|
b.String() // panic: nil pointer
|
||||||
|
b.Write(...) // panic: nil pointer
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Book struct {
|
||||||
|
// Good: has useful zero value
|
||||||
|
bytes.Buffer
|
||||||
|
|
||||||
|
// other fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// later
|
||||||
|
|
||||||
|
var b Book
|
||||||
|
b.Read(...) // ok
|
||||||
|
b.String() // ok
|
||||||
|
b.Write(...) // ok
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Client struct {
|
||||||
|
sync.Mutex
|
||||||
|
sync.WaitGroup
|
||||||
|
bytes.Buffer
|
||||||
|
url.URL
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Client struct {
|
||||||
|
mtx sync.Mutex
|
||||||
|
wg sync.WaitGroup
|
||||||
|
buf bytes.Buffer
|
||||||
|
url url.URL
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
41
src/struct-field-key.md
Normal file
41
src/struct-field-key.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# Use Field Names to Initialize Structs
|
||||||
|
|
||||||
|
You should almost always specify field names when initializing structs. This is
|
||||||
|
now enforced by [`go vet`].
|
||||||
|
|
||||||
|
[`go vet`]: https://pkg.go.dev/cmd/vet
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
k := User{"John", "Doe", true}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
k := User{
|
||||||
|
FirstName: "John",
|
||||||
|
LastName: "Doe",
|
||||||
|
Admin: true,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Exception: Field names *may* be omitted in test tables when there are 3 or
|
||||||
|
fewer fields.
|
||||||
|
|
||||||
|
```go
|
||||||
|
tests := []struct{
|
||||||
|
op Operation
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{Add, "add"},
|
||||||
|
{Subtract, "subtract"},
|
||||||
|
}
|
||||||
|
```
|
||||||
48
src/struct-field-zero.md
Normal file
48
src/struct-field-zero.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# Omit Zero Value Fields in Structs
|
||||||
|
|
||||||
|
When initializing structs with field names, omit fields that have zero values
|
||||||
|
unless they provide meaningful context. Otherwise, let Go set these to zero
|
||||||
|
values automatically.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
user := User{
|
||||||
|
FirstName: "John",
|
||||||
|
LastName: "Doe",
|
||||||
|
MiddleName: "",
|
||||||
|
Admin: false,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
user := User{
|
||||||
|
FirstName: "John",
|
||||||
|
LastName: "Doe",
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
This helps reduce noise for readers by omitting values that are default in
|
||||||
|
that context. Only meaningful values are specified.
|
||||||
|
|
||||||
|
Include zero values where field names provide meaningful context. For example,
|
||||||
|
test cases in [Test Tables](test-table.md) can benefit from names of fields
|
||||||
|
even when they are zero-valued.
|
||||||
|
|
||||||
|
```go
|
||||||
|
tests := []struct{
|
||||||
|
give string
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{give: "0", want: 0},
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
28
src/struct-pointer.md
Normal file
28
src/struct-pointer.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Initializing Struct References
|
||||||
|
|
||||||
|
Use `&T{}` instead of `new(T)` when initializing struct references so that it
|
||||||
|
is consistent with the struct initialization.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
sval := T{Name: "foo"}
|
||||||
|
|
||||||
|
// inconsistent
|
||||||
|
sptr := new(T)
|
||||||
|
sptr.Name = "bar"
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
sval := T{Name: "foo"}
|
||||||
|
|
||||||
|
sptr := &T{Name: "bar"}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
47
src/struct-tag.md
Normal file
47
src/struct-tag.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Use field tags in marshaled structs
|
||||||
|
|
||||||
|
Any struct field that is marshaled into JSON, YAML,
|
||||||
|
or other formats that support tag-based field naming
|
||||||
|
should be annotated with the relevant tag.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Stock struct {
|
||||||
|
Price int
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := json.Marshal(Stock{
|
||||||
|
Price: 137,
|
||||||
|
Name: "UBER",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Stock struct {
|
||||||
|
Price int `json:"price"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
// Safe to rename Name to Symbol.
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes, err := json.Marshal(Stock{
|
||||||
|
Price: 137,
|
||||||
|
Name: "UBER",
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Rationale:
|
||||||
|
The serialized form of the structure is a contract between different systems.
|
||||||
|
Changes to the structure of the serialized form--including field names--break
|
||||||
|
this contract. Specifying field names inside tags makes the contract explicit,
|
||||||
|
and it guards against accidentally breaking the contract by refactoring or
|
||||||
|
renaming fields.
|
||||||
28
src/struct-zero.md
Normal file
28
src/struct-zero.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Use `var` for Zero Value Structs
|
||||||
|
|
||||||
|
When all the fields of a struct are omitted in a declaration, use the `var`
|
||||||
|
form to declare the struct.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
user := User{}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var user User
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
This differentiates zero valued structs from those with non-zero fields
|
||||||
|
similar to the distinction created for [map initialization](map-init.md), and matches how
|
||||||
|
we prefer to [declare empty slices].
|
||||||
|
|
||||||
|
[declare empty slices]: https://go.dev/wiki/CodeReviewComments#declaring-empty-slices
|
||||||
267
src/test-table.md
Normal file
267
src/test-table.md
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
# Test Tables
|
||||||
|
|
||||||
|
Table-driven tests with [subtests] can be a helpful pattern for writing tests
|
||||||
|
to avoid duplicating code when the core test logic is repetitive.
|
||||||
|
|
||||||
|
If a system under test needs to be tested against _multiple conditions_ where
|
||||||
|
certain parts of the inputs and outputs change, a table-driven test should
|
||||||
|
be used to reduce redundancy and improve readability.
|
||||||
|
|
||||||
|
[subtests]: https://go.dev/blog/subtests
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// func TestSplitHostPort(t *testing.T)
|
||||||
|
|
||||||
|
host, port, err := net.SplitHostPort("192.0.2.0:8000")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "192.0.2.0", host)
|
||||||
|
assert.Equal(t, "8000", port)
|
||||||
|
|
||||||
|
host, port, err = net.SplitHostPort("192.0.2.0:http")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "192.0.2.0", host)
|
||||||
|
assert.Equal(t, "http", port)
|
||||||
|
|
||||||
|
host, port, err = net.SplitHostPort(":8000")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "", host)
|
||||||
|
assert.Equal(t, "8000", port)
|
||||||
|
|
||||||
|
host, port, err = net.SplitHostPort("1:8")
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "1", host)
|
||||||
|
assert.Equal(t, "8", port)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// func TestSplitHostPort(t *testing.T)
|
||||||
|
|
||||||
|
tests := []struct{
|
||||||
|
give string
|
||||||
|
wantHost string
|
||||||
|
wantPort string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
give: "192.0.2.0:8000",
|
||||||
|
wantHost: "192.0.2.0",
|
||||||
|
wantPort: "8000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
give: "192.0.2.0:http",
|
||||||
|
wantHost: "192.0.2.0",
|
||||||
|
wantPort: "http",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
give: ":8000",
|
||||||
|
wantHost: "",
|
||||||
|
wantPort: "8000",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
give: "1:8",
|
||||||
|
wantHost: "1",
|
||||||
|
wantPort: "8",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.give, func(t *testing.T) {
|
||||||
|
host, port, err := net.SplitHostPort(tt.give)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantHost, host)
|
||||||
|
assert.Equal(t, tt.wantPort, port)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Test tables make it easier to add context to error messages, reduce duplicate
|
||||||
|
logic, and add new test cases.
|
||||||
|
|
||||||
|
We follow the convention that the slice of structs is referred to as `tests`
|
||||||
|
and each test case `tt`. Further, we encourage explicating the input and output
|
||||||
|
values for each test case with `give` and `want` prefixes.
|
||||||
|
|
||||||
|
```go
|
||||||
|
tests := []struct{
|
||||||
|
give string
|
||||||
|
wantHost string
|
||||||
|
wantPort string
|
||||||
|
}{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Avoid Unnecessary Complexity in Table Tests
|
||||||
|
|
||||||
|
Table tests can be difficult to read and maintain if the subtests contain conditional
|
||||||
|
assertions or other branching logic. Table tests should **NOT** be used whenever
|
||||||
|
there needs to be complex or conditional logic inside subtests (i.e. complex logic inside the `for` loop).
|
||||||
|
|
||||||
|
Large, complex table tests harm readability and maintainability because test readers may
|
||||||
|
have difficulty debugging test failures that occur.
|
||||||
|
|
||||||
|
Table tests like this should be split into either multiple test tables or multiple
|
||||||
|
individual `Test...` functions.
|
||||||
|
|
||||||
|
Some ideals to aim for are:
|
||||||
|
|
||||||
|
* Focus on the narrowest unit of behavior
|
||||||
|
* Minimize "test depth", and avoid conditional assertions (see below)
|
||||||
|
* Ensure that all table fields are used in all tests
|
||||||
|
* Ensure that all test logic runs for all table cases
|
||||||
|
|
||||||
|
In this context, "test depth" means "within a given test, the number of
|
||||||
|
successive assertions that require previous assertions to hold" (similar
|
||||||
|
to cyclomatic complexity).
|
||||||
|
Having "shallower" tests means that there are fewer relationships between
|
||||||
|
assertions and, more importantly, that those assertions are less likely
|
||||||
|
to be conditional by default.
|
||||||
|
|
||||||
|
Concretely, table tests can become confusing and difficult to read if they use multiple branching
|
||||||
|
pathways (e.g. `shouldError`, `expectCall`, etc.), use many `if` statements for
|
||||||
|
specific mock expectations (e.g. `shouldCallFoo`), or place functions inside the
|
||||||
|
table (e.g. `setupMocks func(*FooMock)`).
|
||||||
|
|
||||||
|
However, when testing behavior that only
|
||||||
|
changes based on changed input, it may be preferable to group similar cases
|
||||||
|
together in a table test to better illustrate how behavior changes across all inputs,
|
||||||
|
rather than splitting otherwise comparable units into separate tests
|
||||||
|
and making them harder to compare and contrast.
|
||||||
|
|
||||||
|
If the test body is short and straightforward,
|
||||||
|
it's acceptable to have a single branching pathway for success versus failure cases
|
||||||
|
with a table field like `shouldErr` to specify error expectations.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestComplicatedTable(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
give string
|
||||||
|
want string
|
||||||
|
wantErr error
|
||||||
|
shouldCallX bool
|
||||||
|
shouldCallY bool
|
||||||
|
giveXResponse string
|
||||||
|
giveXErr error
|
||||||
|
giveYResponse string
|
||||||
|
giveYErr error
|
||||||
|
}{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.give, func(t *testing.T) {
|
||||||
|
// setup mocks
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
xMock := xmock.NewMockX(ctrl)
|
||||||
|
if tt.shouldCallX {
|
||||||
|
xMock.EXPECT().Call().Return(
|
||||||
|
tt.giveXResponse, tt.giveXErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
yMock := ymock.NewMockY(ctrl)
|
||||||
|
if tt.shouldCallY {
|
||||||
|
yMock.EXPECT().Call().Return(
|
||||||
|
tt.giveYResponse, tt.giveYErr,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := DoComplexThing(tt.give, xMock, yMock)
|
||||||
|
|
||||||
|
// verify results
|
||||||
|
if tt.wantErr != nil {
|
||||||
|
require.EqualError(t, err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestShouldCallX(t *testing.T) {
|
||||||
|
// setup mocks
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
xMock := xmock.NewMockX(ctrl)
|
||||||
|
xMock.EXPECT().Call().Return("XResponse", nil)
|
||||||
|
|
||||||
|
yMock := ymock.NewMockY(ctrl)
|
||||||
|
|
||||||
|
got, err := DoComplexThing("inputX", xMock, yMock)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "want", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldCallYAndFail(t *testing.T) {
|
||||||
|
// setup mocks
|
||||||
|
ctrl := gomock.NewController(t)
|
||||||
|
xMock := xmock.NewMockX(ctrl)
|
||||||
|
|
||||||
|
yMock := ymock.NewMockY(ctrl)
|
||||||
|
yMock.EXPECT().Call().Return("YResponse", nil)
|
||||||
|
|
||||||
|
_, err := DoComplexThing("inputY", xMock, yMock)
|
||||||
|
assert.EqualError(t, err, "Y failed")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
This complexity makes it more difficult to change, understand, and prove the
|
||||||
|
correctness of the test.
|
||||||
|
|
||||||
|
While there are no strict guidelines, readability and maintainability should
|
||||||
|
always be top-of-mind when deciding between Table Tests versus separate tests
|
||||||
|
for multiple inputs/outputs to a system.
|
||||||
|
|
||||||
|
## Parallel Tests
|
||||||
|
|
||||||
|
Parallel tests, like some specialized loops (for example, those that spawn
|
||||||
|
goroutines or capture references as part of the loop body),
|
||||||
|
must take care to explicitly assign loop variables within the loop's scope to
|
||||||
|
ensure that they hold the expected values.
|
||||||
|
|
||||||
|
```go
|
||||||
|
tests := []struct{
|
||||||
|
give string
|
||||||
|
// ...
|
||||||
|
}{
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.give, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// ...
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In the example above, we must declare a `tt` variable scoped to the loop
|
||||||
|
iteration because of the use of `t.Parallel()` below.
|
||||||
|
If we do not do that, most or all tests will receive an unexpected value for
|
||||||
|
`tt`, or a value that changes as they're running.
|
||||||
|
|
||||||
|
<!-- TODO: Explain how to use _test packages. -->
|
||||||
168
src/time.md
Normal file
168
src/time.md
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
# Use `"time"` to handle time
|
||||||
|
|
||||||
|
Time is complicated. Incorrect assumptions often made about time include the
|
||||||
|
following.
|
||||||
|
|
||||||
|
1. A day has 24 hours
|
||||||
|
2. An hour has 60 minutes
|
||||||
|
3. A week has 7 days
|
||||||
|
4. A year has 365 days
|
||||||
|
5. [And a lot more](https://infiniteundo.com/post/25326999628/falsehoods-programmers-believe-about-time)
|
||||||
|
|
||||||
|
For example, *1* means that adding 24 hours to a time instant will not always
|
||||||
|
yield a new calendar day.
|
||||||
|
|
||||||
|
Therefore, always use the [`"time"`] package when dealing with time because it
|
||||||
|
helps deal with these incorrect assumptions in a safer, more accurate manner.
|
||||||
|
|
||||||
|
[`"time"`]: https://pkg.go.dev/time
|
||||||
|
|
||||||
|
## Use `time.Time` for instants of time
|
||||||
|
|
||||||
|
Use [`time.Time`] when dealing with instants of time, and the methods on
|
||||||
|
`time.Time` when comparing, adding, or subtracting time.
|
||||||
|
|
||||||
|
[`time.Time`]: https://pkg.go.dev/time#Time
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func isActive(now, start, stop int) bool {
|
||||||
|
return start <= now && now < stop
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func isActive(now, start, stop time.Time) bool {
|
||||||
|
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
## Use `time.Duration` for periods of time
|
||||||
|
|
||||||
|
Use [`time.Duration`] when dealing with periods of time.
|
||||||
|
|
||||||
|
[`time.Duration`]: https://pkg.go.dev/time#Duration
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func poll(delay int) {
|
||||||
|
for {
|
||||||
|
// ...
|
||||||
|
time.Sleep(time.Duration(delay) * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll(10) // was it seconds or milliseconds?
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func poll(delay time.Duration) {
|
||||||
|
for {
|
||||||
|
// ...
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
poll(10*time.Second)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Going back to the example of adding 24 hours to a time instant, the method we
|
||||||
|
use to add time depends on intent. If we want the same time of the day, but on
|
||||||
|
the next calendar day, we should use [`Time.AddDate`]. However, if we want an
|
||||||
|
instant of time guaranteed to be 24 hours after the previous time, we should
|
||||||
|
use [`Time.Add`].
|
||||||
|
|
||||||
|
[`Time.AddDate`]: https://pkg.go.dev/time#Time.AddDate
|
||||||
|
[`Time.Add`]: https://pkg.go.dev/time#Time.Add
|
||||||
|
|
||||||
|
```go
|
||||||
|
newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
|
||||||
|
maybeNewDay := t.Add(24 * time.Hour)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use `time.Time` and `time.Duration` with external systems
|
||||||
|
|
||||||
|
Use `time.Duration` and `time.Time` in interactions with external systems when
|
||||||
|
possible. For example:
|
||||||
|
|
||||||
|
- Command-line flags: [`flag`] supports `time.Duration` via
|
||||||
|
[`time.ParseDuration`]
|
||||||
|
- JSON: [`encoding/json`] supports encoding `time.Time` as an [RFC 3339]
|
||||||
|
string via its [`UnmarshalJSON` method]
|
||||||
|
- SQL: [`database/sql`] supports converting `DATETIME` or `TIMESTAMP` columns
|
||||||
|
into `time.Time` and back if the underlying driver supports it
|
||||||
|
- YAML: [`gopkg.in/yaml.v2`] supports `time.Time` as an [RFC 3339] string, and
|
||||||
|
`time.Duration` via [`time.ParseDuration`].
|
||||||
|
|
||||||
|
[`flag`]: https://pkg.go.dev/flag
|
||||||
|
[`time.ParseDuration`]: https://pkg.go.dev/time#ParseDuration
|
||||||
|
[`encoding/json`]: https://pkg.go.dev/encoding/json
|
||||||
|
[RFC 3339]: https://tools.ietf.org/html/rfc3339
|
||||||
|
[`UnmarshalJSON` method]: https://pkg.go.dev/time#Time.UnmarshalJSON
|
||||||
|
[`database/sql`]: https://pkg.go.dev/database/sql
|
||||||
|
[`gopkg.in/yaml.v2`]: https://pkg.go.dev/gopkg.in/yaml.v2
|
||||||
|
|
||||||
|
When it is not possible to use `time.Duration` in these interactions, use
|
||||||
|
`int` or `float64` and include the unit in the name of the field.
|
||||||
|
|
||||||
|
For example, since `encoding/json` does not support `time.Duration`, the unit
|
||||||
|
is included in the name of the field.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// {"interval": 2}
|
||||||
|
type Config struct {
|
||||||
|
Interval int `json:"interval"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
// {"intervalMillis": 2000}
|
||||||
|
type Config struct {
|
||||||
|
IntervalMillis int `json:"intervalMillis"`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
When it is not possible to use `time.Time` in these interactions, unless an
|
||||||
|
alternative is agreed upon, use `string` and format timestamps as defined in
|
||||||
|
[RFC 3339]. This format is used by default by [`Time.UnmarshalText`] and is
|
||||||
|
available for use in `Time.Format` and `time.Parse` via [`time.RFC3339`].
|
||||||
|
|
||||||
|
[`Time.UnmarshalText`]: https://pkg.go.dev/time#Time.UnmarshalText
|
||||||
|
[`time.RFC3339`]: https://pkg.go.dev/time#RFC3339
|
||||||
|
|
||||||
|
Although this tends to not be a problem in practice, keep in mind that the
|
||||||
|
`"time"` package does not support parsing timestamps with leap seconds
|
||||||
|
([8728]), nor does it account for leap seconds in calculations ([15190]). If
|
||||||
|
you compare two instants of time, the difference will not include the leap
|
||||||
|
seconds that may have occurred between those two instants.
|
||||||
|
|
||||||
|
[8728]: https://github.com/golang/go/issues/8728
|
||||||
|
[15190]: https://github.com/golang/go/issues/15190
|
||||||
30
src/type-assert.md
Normal file
30
src/type-assert.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Handle Type Assertion Failures
|
||||||
|
|
||||||
|
The single return value form of a [type assertion] will panic on an incorrect
|
||||||
|
type. Therefore, always use the "comma ok" idiom.
|
||||||
|
|
||||||
|
[type assertion]: https://go.dev/ref/spec#Type_assertions
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
t := i.(string)
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
t, ok := i.(string)
|
||||||
|
if !ok {
|
||||||
|
// handle the error gracefully
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
<!-- TODO: There are a few situations where the single assignment form is
|
||||||
|
fine. -->
|
||||||
59
src/var-decl.md
Normal file
59
src/var-decl.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Local Variable Declarations
|
||||||
|
|
||||||
|
Short variable declarations (`:=`) should be used if a variable is being set to
|
||||||
|
some value explicitly.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
var s = "foo"
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
s := "foo"
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
However, there are cases where the default value is clearer when the `var`
|
||||||
|
keyword is used. [Declaring Empty Slices], for example.
|
||||||
|
|
||||||
|
[Declaring Empty Slices]: https://go.dev/wiki/CodeReviewComments#declaring-empty-slices
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func f(list []int) {
|
||||||
|
filtered := []int{}
|
||||||
|
for _, v := range list {
|
||||||
|
if v > 10 {
|
||||||
|
filtered = append(filtered, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func f(list []int) {
|
||||||
|
var filtered []int
|
||||||
|
for _, v := range list {
|
||||||
|
if v > 10 {
|
||||||
|
filtered = append(filtered, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
102
src/var-scope.md
Normal file
102
src/var-scope.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Reduce Scope of Variables
|
||||||
|
|
||||||
|
Where possible, reduce scope of variables and constants. Do not reduce the scope if it
|
||||||
|
conflicts with [Reduce Nesting](nest-less.md).
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
err := os.WriteFile(name, data, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := os.WriteFile(name, data, 0644); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
If you need a result of a function call outside of the if, then you should not
|
||||||
|
try to reduce the scope.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
if data, err := os.ReadFile(name); err == nil {
|
||||||
|
err = cfg.Decode(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(cfg)
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
data, err := os.ReadFile(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.Decode(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(cfg)
|
||||||
|
return nil
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
|
|
||||||
|
Constants do not need to be global unless they are used in multiple functions or files
|
||||||
|
or are part of an external contract of the package.
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>Bad</th><th>Good</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
const (
|
||||||
|
_defaultPort = 8080
|
||||||
|
_defaultUser = "user"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Bar() {
|
||||||
|
fmt.Println("Default port", _defaultPort)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td><td>
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Bar() {
|
||||||
|
const (
|
||||||
|
defaultPort = 8080
|
||||||
|
defaultUser = "user"
|
||||||
|
)
|
||||||
|
fmt.Println("Default port", defaultPort)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</td></tr>
|
||||||
|
</tbody></table>
|
||||||
Reference in New Issue
Block a user