Repository: uber-go/guide Branch: master Commit: e2c8a0ed5723 Files: 73 Total size: 192.1 KB Directory structure: gitextract_i4we_p2v/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── src/ │ ├── README.md │ ├── SUMMARY.md │ ├── atomic.md │ ├── builtin-name.md │ ├── channel-size.md │ ├── consistency.md │ ├── container-capacity.md │ ├── container-copy.md │ ├── decl-group.md │ ├── defer-clean.md │ ├── else-unnecessary.md │ ├── embed-public.md │ ├── enum-start.md │ ├── error-name.md │ ├── error-once.md │ ├── error-type.md │ ├── error-wrap.md │ ├── exit-main.md │ ├── exit-once.md │ ├── function-name.md │ ├── function-order.md │ ├── functional-option.md │ ├── global-decl.md │ ├── global-mut.md │ ├── global-name.md │ ├── goroutine-exit.md │ ├── goroutine-forget.md │ ├── goroutine-init.md │ ├── import-alias.md │ ├── import-group.md │ ├── init.md │ ├── interface-compliance.md │ ├── interface-pointer.md │ ├── interface-receiver.md │ ├── intro.md │ ├── line-length.md │ ├── lint.md │ ├── map-init.md │ ├── mutex-zero-value.md │ ├── nest-less.md │ ├── package-name.md │ ├── panic.md │ ├── param-naked.md │ ├── performance.md │ ├── preface.txt │ ├── printf-const.md │ ├── printf-name.md │ ├── slice-nil.md │ ├── strconv.md │ ├── string-byte-slice.md │ ├── string-escape.md │ ├── struct-embed.md │ ├── struct-field-key.md │ ├── struct-field-zero.md │ ├── struct-pointer.md │ ├── struct-tag.md │ ├── struct-zero.md │ ├── test-table.md │ ├── time.md │ ├── type-assert.md │ ├── var-decl.md │ └── var-scope.md └── style.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "gomod" directory: "/tools" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: [ main ] pull_request_target: 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 with: # Check out the pull request repository and branch. # If the PR is made from a fork, this will check out the fork. # This is necessary for git-auto-commit-action to update PRs made by forks. # See # https://github.com/stefanzweifel/git-auto-commit-action#use-in-forks-from-public-repositories repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.head_ref }} - 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_target' && '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_target' }} with: file_pattern: style.md commit_message: 'Auto-update style.md' ================================================ FILE: .gitignore ================================================ /bin ================================================ FILE: .golangci.yml ================================================ # 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 ================================================ FILE: CHANGELOG.md ================================================ # 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. ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # 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 ================================================ FILE: CONTRIBUTING.md ================================================ # 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)`). ## 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: ~~~
| Bad | Good |
|---|---|
| ```go BAD CODE GOES HERE ``` | ```go GOOD CODE GOES HERE ``` |
| Bad | Good |
|---|---|
| ```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! } ``` | ```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() } ``` |
| Bad | Good |
|---|---|
| ```go var error string // `error` shadows the builtin // or func handleErrorMessage(error string) { // `error` shadows the builtin } ``` | ```go var errorMessage string // `error` refers to the builtin // or func handleErrorMessage(msg string) { // `error` refers to the builtin } ``` |
| ```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 } ``` | ```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 } ``` |
| Bad | Good |
|---|---|
| ```go // Ought to be enough for anybody! c := make(chan int, 64) ``` | ```go // Size of one c := make(chan int, 1) // or // Unbuffered channel, size of zero c := make(chan int) ``` |
| Bad | Good |
|---|---|
| ```go files, _ := os.ReadDir("./files") m := make(map[string]os.DirEntry) for _, f := range files { m[f.Name()] = f } ``` | ```go files, _ := os.ReadDir("./files") m := make(map[string]os.DirEntry, len(files)) for _, f := range files { m[f.Name()] = f } ``` |
| `m` is created without a size hint; the map will resize dynamically, causing multiple allocations as it grows. | `m` is created with a size hint; there may be fewer allocations at assignment time. |
| Bad | Good |
|---|---|
| ```go for n := 0; n < b.N; n++ { data := make([]int, 0) for k := 0; k < size; k++{ data = append(data, k) } } ``` | ```go for n := 0; n < b.N; n++ { data := make([]int, 0, size) for k := 0; k < size; k++{ data = append(data, k) } } ``` |
| ```plain BenchmarkBad-4 100000000 2.48s ``` | ```plain BenchmarkGood-4 100000000 0.21s ``` |
| Bad | Good |
|---|---|
| ```go func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := ... d1.SetTrips(trips) // Did you mean to modify d1.trips? trips[0] = ... ``` | ```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] = ... ``` |
| Bad | Good |
|---|---|
| ```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() ``` | ```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() ``` |
| Bad | Good |
|---|---|
| ```go import "a" import "b" ``` | ```go import ( "a" "b" ) ``` |
| Bad | Good |
|---|---|
| ```go const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 ``` | ```go const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) ``` |
| Bad | Good |
|---|---|
| ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply EnvVar = "MY_ENV" ) ``` | ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const EnvVar = "MY_ENV" ``` |
| Bad | Good |
|---|---|
| ```go func f() string { red := color.New(0xff0000) green := color.New(0x00ff00) blue := color.New(0x0000ff) // ... } ``` | ```go func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) // ... } ``` |
| Bad | Good |
|---|---|
| ```go func (c *client) request() { caller := c.name format := "json" timeout := 5*time.Second var err error // ... } ``` | ```go func (c *client) request() { var ( caller = c.name format = "json" timeout = 5*time.Second err error ) // ... } ``` |
| Bad | Good |
|---|---|
| ```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 ``` | ```go p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // more readable ``` |
| Bad | Good |
|---|---|
| ```go var a int if b { a = 100 } else { a = 10 } ``` | ```go a := 10 if b { a = 100 } ``` |
| Bad | Good |
|---|---|
| ```go // ConcreteList is a list of entities. type ConcreteList struct { *AbstractList } ``` | ```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) } ``` |
| Bad | Good |
|---|---|
| ```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 } ``` | ```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) } ``` |
| Bad | Good |
|---|---|
| ```go type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 ``` | ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 ``` |
| Description | Code |
|---|---|
| **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. | ```go u, err := getUser(id) if err != nil { // BAD: See description log.Printf("Could not get user %q: %v", id, err) return err } ``` |
| **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. | ```go u, err := getUser(id) if err != nil { return fmt.Errorf("get user %q: %w", id, err) } ``` |
| **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. | ```go if err := emitMetrics(); err != nil { // Failure to write metrics should not // break the application. log.Printf("Could not emit metrics: %v", err) } ``` |
| **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. | ```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) } } ``` |
| No error matching | Error matching |
|---|---|
| ```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") } ``` | ```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") } } ``` |
| No error matching | Error matching |
|---|---|
| ```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") } ``` | ```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") } } ``` |
| Bad | Good |
|---|---|
| ```go s, err := store.New() if err != nil { return fmt.Errorf( "failed to create new store: %w", err) } ``` | ```go s, err := store.New() if err != nil { return fmt.Errorf( "new store: %w", err) } ``` |
| ```plain failed to x: failed to y: failed to create new store: the error ``` | ```plain x: y: new store: the error ``` |
| Bad | Good |
|---|---|
| ```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) } ``` | ```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 } ``` |
| Bad | Good |
|---|---|
| ```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) } // ... } ``` | ```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 } // ... } ``` |
| Bad | Good |
|---|---|
| ```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{} } ``` | ```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 {...} ``` |
| Bad | Good |
|---|---|
| ```go // package db func Open( addr string, cache bool, logger *zap.Logger ) (*Connection, error) { // ... } ``` | ```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) { // ... } ``` |
| 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) ``` | 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), ) ``` |
| Bad | Good |
|---|---|
| ```go var _s string = F() func F() string { return "A" } ``` | ```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" } ``` |
| Bad | Good |
|---|---|
| ```go // sign.go var _timeNow = time.Now func sign(msg string) string { now := _timeNow() return signWithTime(msg, now) } ``` | ```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) } ``` |
| ```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)) } ``` | ```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)) } ``` |
| Bad | Good |
|---|---|
| ```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. } ``` | ```go // foo.go const ( _defaultPort = 8080 _defaultUser = "user" ) ``` |
| Bad | Good |
|---|---|
| ```go go func() { for { flush() time.Sleep(delay) } }() ``` | ```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 ``` |
| There's no way to stop this goroutine. This will run until the application exits. | This goroutine can be stopped with `close(stop)`, and we can wait for it to exit with `<-done`. |
| Bad | Good |
|---|---|
| ```go func init() { go doWork() } func doWork() { for { // ... } } ``` | ```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 } ``` |
| 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. | 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). |
| Bad | Good |
|---|---|
| ```go import ( "fmt" "os" runtimetrace "runtime/trace" nettrace "golang.net/x/trace" ) ``` | ```go import ( "fmt" "os" "runtime/trace" nettrace "golang.net/x/trace" ) ``` |
| Bad | Good |
|---|---|
| ```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ``` | ```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ``` |
| Bad | Good |
|---|---|
| ```go type Foo struct { // ... } var _defaultFoo Foo func init() { _defaultFoo = Foo{ // ... } } ``` | ```go var _defaultFoo = Foo{ // ... } // or, better, for testability: var _defaultFoo = defaultFoo() func defaultFoo() Foo { return Foo{ // ... } } ``` |
| ```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) } ``` | ```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 } ``` |
| Bad | Good |
|---|---|
| ```go type Handler struct { // ... } func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { ... } ``` | ```go type Handler struct { // ... } var _ http.Handler = (*Handler)(nil) func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { // ... } ``` |
| Bad | Good |
|---|---|
| ```go var ( // m1 is safe to read and write; // m2 will panic on writes. m1 = map[T1]T2{} m2 map[T1]T2 ) ``` | ```go var ( // m1 is safe to read and write; // m2 will panic on writes. m1 = make(map[T1]T2) m2 map[T1]T2 ) ``` |
| Declaration and initialization are visually similar. | Declaration and initialization are visually distinct. |
| Bad | Good |
|---|---|
| ```go m := make(map[T1]T2, 3) m[k1] = v1 m[k2] = v2 m[k3] = v3 ``` | ```go m := map[T1]T2{ k1: v1, k2: v2, k3: v3, } ``` |
| Bad | Good |
|---|---|
| ```go mu := new(sync.Mutex) mu.Lock() ``` | ```go var mu sync.Mutex mu.Lock() ``` |
| Bad | Good |
|---|---|
| ```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] } ``` | ```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] } ``` |
| The `Mutex` field, and the `Lock` and `Unlock` methods are unintentionally part of the exported API of `SMap`. | The mutex and its methods are implementation details of `SMap` hidden from its callers. |
| Bad | Good |
|---|---|
| ```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) } } ``` | ```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() } ``` |
| Bad | Good |
|---|---|
| ```go func run(args []string) { if len(args) == 0 { panic("an argument is required") } // ... } func main() { run(os.Args[1:]) } ``` | ```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) } } ``` |
| Bad | Good |
|---|---|
| ```go // func TestFoo(t *testing.T) f, err := os.CreateTemp("", "test") if err != nil { panic("failed to set up test") } ``` | ```go // func TestFoo(t *testing.T) f, err := os.CreateTemp("", "test") if err != nil { t.Fatal("failed to set up test") } ``` |
| Bad | Good |
|---|---|
| ```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true, true) ``` | ```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true /* isLocal */, true /* done */) ``` |
| Bad | Good |
|---|---|
| ```go msg := "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ``` | ```go const msg = "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ``` |
| Bad | Good |
|---|---|
| ```go if x == "" { return []int{} } ``` | ```go if x == "" { return nil } ``` |
| Bad | Good |
|---|---|
| ```go func isEmpty(s []string) bool { return s == nil } ``` | ```go func isEmpty(s []string) bool { return len(s) == 0 } ``` |
| Bad | Good |
|---|---|
| ```go nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ``` | ```go var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ``` |
| Bad | Good |
|---|---|
| ```go for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } ``` | ```go for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } ``` |
| ```plain BenchmarkFmtSprint-4 143 ns/op 2 allocs/op ``` | ```plain BenchmarkStrconv-4 64.2 ns/op 1 allocs/op ``` |
| Bad | Good |
|---|---|
| ```go for i := 0; i < b.N; i++ { w.Write([]byte("Hello world")) } ``` | ```go data := []byte("Hello world") for i := 0; i < b.N; i++ { w.Write(data) } ``` |
| ```plain BenchmarkBad-4 50000000 22.2 ns/op ``` | ```plain BenchmarkGood-4 500000000 3.25 ns/op ``` |
| Bad | Good |
|---|---|
| ```go wantError := "unknown name:\"test\"" ``` | ```go wantError := `unknown error:"test"` ``` |
| Bad | Good |
|---|---|
| ```go type Client struct { version int http.Client } ``` | ```go type Client struct { http.Client version int } ``` |
| Bad | Good |
|---|---|
| ```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 } ``` | ```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) } ``` |
| ```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 ``` | ```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 ``` |
| ```go type Client struct { sync.Mutex sync.WaitGroup bytes.Buffer url.URL } ``` | ```go type Client struct { mtx sync.Mutex wg sync.WaitGroup buf bytes.Buffer url url.URL } ``` |
| Bad | Good |
|---|---|
| ```go k := User{"John", "Doe", true} ``` | ```go k := User{ FirstName: "John", LastName: "Doe", Admin: true, } ``` |
| Bad | Good |
|---|---|
| ```go user := User{ FirstName: "John", LastName: "Doe", MiddleName: "", Admin: false, } ``` | ```go user := User{ FirstName: "John", LastName: "Doe", } ``` |
| Bad | Good |
|---|---|
| ```go sval := T{Name: "foo"} // inconsistent sptr := new(T) sptr.Name = "bar" ``` | ```go sval := T{Name: "foo"} sptr := &T{Name: "bar"} ``` |
| Bad | Good |
|---|---|
| ```go type Stock struct { Price int Name string } bytes, err := json.Marshal(Stock{ Price: 137, Name: "UBER", }) ``` | ```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", }) ``` |
| Bad | Good |
|---|---|
| ```go user := User{} ``` | ```go var user User ``` |
| Bad | Good |
|---|---|
| ```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) ``` | ```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) }) } ``` |
| Bad | Good |
|---|---|
| ```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) }) } } ``` | ```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") } ``` |
| Bad | Good |
|---|---|
| ```go func isActive(now, start, stop int) bool { return start <= now && now < stop } ``` | ```go func isActive(now, start, stop time.Time) bool { return (start.Before(now) || start.Equal(now)) && now.Before(stop) } ``` |
| Bad | Good |
|---|---|
| ```go func poll(delay int) { for { // ... time.Sleep(time.Duration(delay) * time.Millisecond) } } poll(10) // was it seconds or milliseconds? ``` | ```go func poll(delay time.Duration) { for { // ... time.Sleep(delay) } } poll(10*time.Second) ``` |
| Bad | Good |
|---|---|
| ```go // {"interval": 2} type Config struct { Interval int `json:"interval"` } ``` | ```go // {"intervalMillis": 2000} type Config struct { IntervalMillis int `json:"intervalMillis"` } ``` |
| Bad | Good |
|---|---|
| ```go t := i.(string) ``` | ```go t, ok := i.(string) if !ok { // handle the error gracefully } ``` |
| Bad | Good |
|---|---|
| ```go var s = "foo" ``` | ```go s := "foo" ``` |
| Bad | Good |
|---|---|
| ```go func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ``` | ```go func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ``` |
| Bad | Good |
|---|---|
| ```go err := os.WriteFile(name, data, 0644) if err != nil { return err } ``` | ```go if err := os.WriteFile(name, data, 0644); err != nil { return err } ``` |
| Bad | Good |
|---|---|
| ```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 } ``` | ```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 ``` |
| Bad | Good |
|---|---|
| ```go const ( _defaultPort = 8080 _defaultUser = "user" ) func Bar() { fmt.Println("Default port", _defaultPort) } ``` | ```go func Bar() { const ( defaultPort = 8080 defaultUser = "user" ) fmt.Println("Default port", defaultPort) } ``` |
| Bad | Good |
|---|---|
| ```go type Handler struct { // ... } func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { ... } ``` | ```go type Handler struct { // ... } var _ http.Handler = (*Handler)(nil) func (h *Handler) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { // ... } ``` |
| Bad | Good |
|---|---|
| ```go mu := new(sync.Mutex) mu.Lock() ``` | ```go var mu sync.Mutex mu.Lock() ``` |
| Bad | Good |
|---|---|
| ```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] } ``` | ```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] } ``` |
| The `Mutex` field, and the `Lock` and `Unlock` methods are unintentionally part of the exported API of `SMap`. | The mutex and its methods are implementation details of `SMap` hidden from its callers. |
| Bad | Good |
|---|---|
| ```go func (d *Driver) SetTrips(trips []Trip) { d.trips = trips } trips := ... d1.SetTrips(trips) // Did you mean to modify d1.trips? trips[0] = ... ``` | ```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] = ... ``` |
| Bad | Good |
|---|---|
| ```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() ``` | ```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() ``` |
| Bad | Good |
|---|---|
| ```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 ``` | ```go p.Lock() defer p.Unlock() if p.count < 10 { return p.count } p.count++ return p.count // more readable ``` |
| Bad | Good |
|---|---|
| ```go // Ought to be enough for anybody! c := make(chan int, 64) ``` | ```go // Size of one c := make(chan int, 1) // or // Unbuffered channel, size of zero c := make(chan int) ``` |
| Bad | Good |
|---|---|
| ```go type Operation int const ( Add Operation = iota Subtract Multiply ) // Add=0, Subtract=1, Multiply=2 ``` | ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) // Add=1, Subtract=2, Multiply=3 ``` |
| Bad | Good |
|---|---|
| ```go func isActive(now, start, stop int) bool { return start <= now && now < stop } ``` | ```go func isActive(now, start, stop time.Time) bool { return (start.Before(now) || start.Equal(now)) && now.Before(stop) } ``` |
| Bad | Good |
|---|---|
| ```go func poll(delay int) { for { // ... time.Sleep(time.Duration(delay) * time.Millisecond) } } poll(10) // was it seconds or milliseconds? ``` | ```go func poll(delay time.Duration) { for { // ... time.Sleep(delay) } } poll(10*time.Second) ``` |
| Bad | Good |
|---|---|
| ```go // {"interval": 2} type Config struct { Interval int `json:"interval"` } ``` | ```go // {"intervalMillis": 2000} type Config struct { IntervalMillis int `json:"intervalMillis"` } ``` |
| No error matching | Error matching |
|---|---|
| ```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") } ``` | ```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") } } ``` |
| No error matching | Error matching |
|---|---|
| ```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") } ``` | ```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") } } ``` |
| Bad | Good |
|---|---|
| ```go s, err := store.New() if err != nil { return fmt.Errorf( "failed to create new store: %w", err) } ``` | ```go s, err := store.New() if err != nil { return fmt.Errorf( "new store: %w", err) } ``` |
| ```plain failed to x: failed to y: failed to create new store: the error ``` | ```plain x: y: new store: the error ``` |
| Description | Code |
|---|---|
| **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. | ```go u, err := getUser(id) if err != nil { // BAD: See description log.Printf("Could not get user %q: %v", id, err) return err } ``` |
| **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. | ```go u, err := getUser(id) if err != nil { return fmt.Errorf("get user %q: %w", id, err) } ``` |
| **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. | ```go if err := emitMetrics(); err != nil { // Failure to write metrics should not // break the application. log.Printf("Could not emit metrics: %v", err) } ``` |
| **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. | ```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) } } ``` |
| Bad | Good |
|---|---|
| ```go t := i.(string) ``` | ```go t, ok := i.(string) if !ok { // handle the error gracefully } ``` |
| Bad | Good |
|---|---|
| ```go func run(args []string) { if len(args) == 0 { panic("an argument is required") } // ... } func main() { run(os.Args[1:]) } ``` | ```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) } } ``` |
| Bad | Good |
|---|---|
| ```go // func TestFoo(t *testing.T) f, err := os.CreateTemp("", "test") if err != nil { panic("failed to set up test") } ``` | ```go // func TestFoo(t *testing.T) f, err := os.CreateTemp("", "test") if err != nil { t.Fatal("failed to set up test") } ``` |
| Bad | Good |
|---|---|
| ```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! } ``` | ```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() } ``` |
| Bad | Good |
|---|---|
| ```go // sign.go var _timeNow = time.Now func sign(msg string) string { now := _timeNow() return signWithTime(msg, now) } ``` | ```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) } ``` |
| ```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)) } ``` | ```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)) } ``` |
| Bad | Good |
|---|---|
| ```go // ConcreteList is a list of entities. type ConcreteList struct { *AbstractList } ``` | ```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) } ``` |
| Bad | Good |
|---|---|
| ```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 } ``` | ```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) } ``` |
| Bad | Good |
|---|---|
| ```go var error string // `error` shadows the builtin // or func handleErrorMessage(error string) { // `error` shadows the builtin } ``` | ```go var errorMessage string // `error` refers to the builtin // or func handleErrorMessage(msg string) { // `error` refers to the builtin } ``` |
| ```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 } ``` | ```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 } ``` |
| Bad | Good |
|---|---|
| ```go type Foo struct { // ... } var _defaultFoo Foo func init() { _defaultFoo = Foo{ // ... } } ``` | ```go var _defaultFoo = Foo{ // ... } // or, better, for testability: var _defaultFoo = defaultFoo() func defaultFoo() Foo { return Foo{ // ... } } ``` |
| ```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) } ``` | ```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 } ``` |
| Bad | Good |
|---|---|
| ```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) } ``` | ```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 } ``` |
| Bad | Good |
|---|---|
| ```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) } // ... } ``` | ```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 } // ... } ``` |
| Bad | Good |
|---|---|
| ```go type Stock struct { Price int Name string } bytes, err := json.Marshal(Stock{ Price: 137, Name: "UBER", }) ``` | ```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", }) ``` |
| Bad | Good |
|---|---|
| ```go go func() { for { flush() time.Sleep(delay) } }() ``` | ```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 ``` |
| There's no way to stop this goroutine. This will run until the application exits. | This goroutine can be stopped with `close(stop)`, and we can wait for it to exit with `<-done`. |
| Bad | Good |
|---|---|
| ```go func init() { go doWork() } func doWork() { for { // ... } } ``` | ```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 } ``` |
| 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. | 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](#wait-for-goroutines-to-exit). |
| Bad | Good |
|---|---|
| ```go for i := 0; i < b.N; i++ { s := fmt.Sprint(rand.Int()) } ``` | ```go for i := 0; i < b.N; i++ { s := strconv.Itoa(rand.Int()) } ``` |
| ```plain BenchmarkFmtSprint-4 143 ns/op 2 allocs/op ``` | ```plain BenchmarkStrconv-4 64.2 ns/op 1 allocs/op ``` |
| Bad | Good |
|---|---|
| ```go for i := 0; i < b.N; i++ { w.Write([]byte("Hello world")) } ``` | ```go data := []byte("Hello world") for i := 0; i < b.N; i++ { w.Write(data) } ``` |
| ```plain BenchmarkBad-4 50000000 22.2 ns/op ``` | ```plain BenchmarkGood-4 500000000 3.25 ns/op ``` |
| Bad | Good |
|---|---|
| ```go files, _ := os.ReadDir("./files") m := make(map[string]os.DirEntry) for _, f := range files { m[f.Name()] = f } ``` | ```go files, _ := os.ReadDir("./files") m := make(map[string]os.DirEntry, len(files)) for _, f := range files { m[f.Name()] = f } ``` |
| `m` is created without a size hint; the map will resize dynamically, causing multiple allocations as it grows. | `m` is created with a size hint; there may be fewer allocations at assignment time. |
| Bad | Good |
|---|---|
| ```go for n := 0; n < b.N; n++ { data := make([]int, 0) for k := 0; k < size; k++{ data = append(data, k) } } ``` | ```go for n := 0; n < b.N; n++ { data := make([]int, 0, size) for k := 0; k < size; k++{ data = append(data, k) } } ``` |
| ```plain BenchmarkBad-4 100000000 2.48s ``` | ```plain BenchmarkGood-4 100000000 0.21s ``` |
| Bad | Good |
|---|---|
| ```go import "a" import "b" ``` | ```go import ( "a" "b" ) ``` |
| Bad | Good |
|---|---|
| ```go const a = 1 const b = 2 var a = 1 var b = 2 type Area float64 type Volume float64 ``` | ```go const ( a = 1 b = 2 ) var ( a = 1 b = 2 ) type ( Area float64 Volume float64 ) ``` |
| Bad | Good |
|---|---|
| ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply EnvVar = "MY_ENV" ) ``` | ```go type Operation int const ( Add Operation = iota + 1 Subtract Multiply ) const EnvVar = "MY_ENV" ``` |
| Bad | Good |
|---|---|
| ```go func f() string { red := color.New(0xff0000) green := color.New(0x00ff00) blue := color.New(0x0000ff) // ... } ``` | ```go func f() string { var ( red = color.New(0xff0000) green = color.New(0x00ff00) blue = color.New(0x0000ff) ) // ... } ``` |
| Bad | Good |
|---|---|
| ```go func (c *client) request() { caller := c.name format := "json" timeout := 5*time.Second var err error // ... } ``` | ```go func (c *client) request() { var ( caller = c.name format = "json" timeout = 5*time.Second err error ) // ... } ``` |
| Bad | Good |
|---|---|
| ```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ``` | ```go import ( "fmt" "os" "go.uber.org/atomic" "golang.org/x/sync/errgroup" ) ``` |
| Bad | Good |
|---|---|
| ```go import ( "fmt" "os" runtimetrace "runtime/trace" nettrace "golang.net/x/trace" ) ``` | ```go import ( "fmt" "os" "runtime/trace" nettrace "golang.net/x/trace" ) ``` |
| Bad | Good |
|---|---|
| ```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{} } ``` | ```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 {...} ``` |
| Bad | Good |
|---|---|
| ```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) } } ``` | ```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() } ``` |
| Bad | Good |
|---|---|
| ```go var a int if b { a = 100 } else { a = 10 } ``` | ```go a := 10 if b { a = 100 } ``` |
| Bad | Good |
|---|---|
| ```go var _s string = F() func F() string { return "A" } ``` | ```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" } ``` |
| Bad | Good |
|---|---|
| ```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. } ``` | ```go // foo.go const ( _defaultPort = 8080 _defaultUser = "user" ) ``` |
| Bad | Good |
|---|---|
| ```go type Client struct { version int http.Client } ``` | ```go type Client struct { http.Client version int } ``` |
| Bad | Good |
|---|---|
| ```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 } ``` | ```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) } ``` |
| ```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 ``` | ```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 ``` |
| ```go type Client struct { sync.Mutex sync.WaitGroup bytes.Buffer url.URL } ``` | ```go type Client struct { mtx sync.Mutex wg sync.WaitGroup buf bytes.Buffer url url.URL } ``` |
| Bad | Good |
|---|---|
| ```go var s = "foo" ``` | ```go s := "foo" ``` |
| Bad | Good |
|---|---|
| ```go func f(list []int) { filtered := []int{} for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ``` | ```go func f(list []int) { var filtered []int for _, v := range list { if v > 10 { filtered = append(filtered, v) } } } ``` |
| Bad | Good |
|---|---|
| ```go if x == "" { return []int{} } ``` | ```go if x == "" { return nil } ``` |
| Bad | Good |
|---|---|
| ```go func isEmpty(s []string) bool { return s == nil } ``` | ```go func isEmpty(s []string) bool { return len(s) == 0 } ``` |
| Bad | Good |
|---|---|
| ```go nums := []int{} // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ``` | ```go var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) } ``` |
| Bad | Good |
|---|---|
| ```go err := os.WriteFile(name, data, 0644) if err != nil { return err } ``` | ```go if err := os.WriteFile(name, data, 0644); err != nil { return err } ``` |
| Bad | Good |
|---|---|
| ```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 } ``` | ```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 ``` |
| Bad | Good |
|---|---|
| ```go const ( _defaultPort = 8080 _defaultUser = "user" ) func Bar() { fmt.Println("Default port", _defaultPort) } ``` | ```go func Bar() { const ( defaultPort = 8080 defaultUser = "user" ) fmt.Println("Default port", defaultPort) } ``` |
| Bad | Good |
|---|---|
| ```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true, true) ``` | ```go // func printInfo(name string, isLocal, done bool) printInfo("foo", true /* isLocal */, true /* done */) ``` |
| Bad | Good |
|---|---|
| ```go wantError := "unknown name:\"test\"" ``` | ```go wantError := `unknown error:"test"` ``` |
| Bad | Good |
|---|---|
| ```go k := User{"John", "Doe", true} ``` | ```go k := User{ FirstName: "John", LastName: "Doe", Admin: true, } ``` |
| Bad | Good |
|---|---|
| ```go user := User{ FirstName: "John", LastName: "Doe", MiddleName: "", Admin: false, } ``` | ```go user := User{ FirstName: "John", LastName: "Doe", } ``` |
| Bad | Good |
|---|---|
| ```go user := User{} ``` | ```go var user User ``` |
| Bad | Good |
|---|---|
| ```go sval := T{Name: "foo"} // inconsistent sptr := new(T) sptr.Name = "bar" ``` | ```go sval := T{Name: "foo"} sptr := &T{Name: "bar"} ``` |
| Bad | Good |
|---|---|
| ```go var ( // m1 is safe to read and write; // m2 will panic on writes. m1 = map[T1]T2{} m2 map[T1]T2 ) ``` | ```go var ( // m1 is safe to read and write; // m2 will panic on writes. m1 = make(map[T1]T2) m2 map[T1]T2 ) ``` |
| Declaration and initialization are visually similar. | Declaration and initialization are visually distinct. |
| Bad | Good |
|---|---|
| ```go m := make(map[T1]T2, 3) m[k1] = v1 m[k2] = v2 m[k3] = v3 ``` | ```go m := map[T1]T2{ k1: v1, k2: v2, k3: v3, } ``` |
| Bad | Good |
|---|---|
| ```go msg := "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ``` | ```go const msg = "unexpected values %v, %v\n" fmt.Printf(msg, 1, 2) ``` |
| Bad | Good |
|---|---|
| ```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) ``` | ```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) }) } ``` |
| Bad | Good |
|---|---|
| ```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) }) } } ``` | ```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") } ``` |
| Bad | Good |
|---|---|
| ```go // package db func Open( addr string, cache bool, logger *zap.Logger ) (*Connection, error) { // ... } ``` | ```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) { // ... } ``` |
| 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) ``` | 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), ) ``` |