- Introduction
- Guidelines
- Performance
- Style
- Avoid overly long lines
- Be Consistent
- Group Similar Declarations
- Import Group Ordering
- Package Names
- Function Names
- Import Aliasing
- Function Grouping and Ordering
- Reduce Nesting
- Unnecessary Else
- Embedding in Structs
- Local Variable Declarations
- nil is a valid slice
- Avoid Naked Parameters
- Use Raw String Literals to Avoid Escaping
- Initializing Structs
- Patterns
- Linting
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.
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:
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
andgo vet
to check for errors
You can find information in editor support for Go tools here: https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
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:
- A pointer to some type-specific information. You can think of this as "type."
- 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.
아래는 인터페이스 예시 (인터페이스 설명 [link])
type Animal interface {
run()
}
type Dog struct {
}
func (d Dog) run() {
println("run")
}
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
Bad | Good |
---|---|
type Handler struct {
// ...
}
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
...
} |
type Handler struct {
// ...
}
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
} Handler가 http.Handler 인터페이스와 매치가 되는지 컴파일 |
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.
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
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.
For example,
type S struct {
data string
}
func (s S) Read() string {
return s.data
}
func (s *S) Write(str string) {
s.data = str
}
sVals := map[int]S{1: {"A"}}
// You can only call Read using a value
sVals[1].Read()
// This will not compile:
// sVals[1].Write("test")
sPtrs := map[int]*S{1: {"A"}}
// You can call both Read and Write using a pointer
sPtrs[1].Read()
sPtrs[1].Write("test")
Similarly, an interface can be satisfied by a pointer, even if the method has a value receiver.
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.
The zero-value of sync.Mutex
and sync.RWMutex
is valid, so you almost
never need a pointer to a mutex.
Bad | Good |
---|---|
mu := new(sync.Mutex)
mu.Lock() |
var mu sync.Mutex
mu.Lock() |
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.
Bad | Good |
---|---|
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]
} |
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 |
The mutex and its methods are implementation details of |
Slices and maps contain pointers to the underlying data so be wary of scenarios when they need to be copied.
Keep in mind that users can modify a map or slice you received as an argument if you store a reference to it.
Bad | Good |
---|---|
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// Did you mean to modify d1.trips?
trips[0] = ... |
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] = ... |
Similarly, be wary of user modifications to maps or slices exposing internal state.
Bad | Good |
---|---|
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() |
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() |
Use defer to clean up resources such as files and locks.
Bad | Good |
---|---|
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 |
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// more readable |
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
.
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.
channel size
0: sender는 receiver가 값을 읽을 때까지 기다린다.
1: sender는 receiver가 값을 받았다고 가정하고 행동한다. 즉, concurrency를 허용한다.
n: 버퍼
채널
package main
import (
"fmt"
)
func main() {
done := make(chan bool, 1)
go func() {
for i := 0; i < 6; i++ {
done <- true
fmt.Println("고루틴 : ", i)
}
}()
for i := 0; i < 6; i++ {
<-done
fmt.Println("메인 함수 : ", i)
}
}
Bad | Good |
---|---|
// Ought to be enough for anybody!
c := make(chan int, 64) |
// Size of one
c := make(chan int, 1) // or
// Unbuffered channel, size of zero
c := make(chan int) |
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.
Bad | Good |
---|---|
type Operation int
const (
Add Operation = iota
Subtract
Multiply
)
// Add=0, Subtract=1, Multiply=2 |
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
// Add=1, Subtract=2, Multiply=3 |
There are cases where using the zero value makes sense, for example when the zero value case is the desirable default behavior.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Time is complicated. Incorrect assumptions often made about time include the following.
- A day has 24 hours
- An hour has 60 minutes
- A week has 7 days
- A year has 365 days
- And a lot more
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.
int type 으로 의미상 ‘초’ 에 해당하는 값을 받지 말고, param 자체를 time.Time 혹은 time.Duration 으로 직접 받으라는 의미이다.
Use time.Time
when dealing with instants of time, and the methods on
time.Time
when comparing, adding, or subtracting time.
Bad | Good |
---|---|
func isActive(now, start, stop int) bool {
return start <= now && now < stop
} |
func isActive(now, start, stop time.Time) bool {
return (start.Before(now) || start.Equal(now)) && now.Before(stop)
} |
Use time.Duration
when dealing with periods of time.
Bad | Good |
---|---|
func poll(delay int) {
for {
// ...
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
poll(10) // was it seconds or milliseconds? |
func poll(delay time.Duration) {
for {
// ...
time.Sleep(delay)
}
}
poll(10*time.Second) |
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
.
newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)
Use time.Duration
and time.Time
in interactions with external systems when
possible. For example:
- Command-line flags:
flag
supportstime.Duration
viatime.ParseDuration
- JSON:
encoding/json
supports encodingtime.Time
as an RFC 3339 string via itsUnmarshalJSON
method - SQL:
database/sql
supports convertingDATETIME
orTIMESTAMP
columns intotime.Time
and back if the underlying driver supports it - YAML:
gopkg.in/yaml.v2
supportstime.Time
as an RFC 3339 string, andtime.Duration
viatime.ParseDuration
.
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.
Bad | Good |
---|---|
// {"interval": 2}
type Config struct {
Interval int `json:"interval"`
} |
// {"intervalMillis": 2000}
type Config struct {
IntervalMillis int `json:"intervalMillis"`
} |
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
.
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.
에러를 선언하는데 있어서 다양한 옵션들이 존재한다:
errors.New
간단한 정적 문자열(simple static strings)과 함께하는 에러fmt.Errorf
형식화된 오류 문자열Error()
메서드를 구현한 커스텀 타입 (Custom types)
오류를 반환할 때, 가장 좋은 선택을 하기 위해서 아래의 사항을 고려하라:
- 추가 정보가 필요없는 간단한 에러인가? 그렇다면,
errors.New
가 충분하다. - 클라이언트가 오류를 감지하고 처리(handle)해야 하는가? 그렇다면, 커스텀 타입을 사용해야 하고
Error()
메서드를 구현해야 한다. - 다운스트림 함수(downstream function)에 의해 반환된 에러를 전파(propagating)하고 있는가? 그렇다면, 오류 포장(Error Wrapping)을 참고하라.
- 이외의 경우,
fmt.Errorf
로 충분하다.
만약 클라이언트가 오류를 감지해야 하고, 여러분들이 errors.New
을 사용하여 간단한 에러를 생성한 경우, var
에 에러를 사용해라.
No error matching | Error matching |
---|---|
// 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")
} |
// 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 |
---|---|
// 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")
} |
// 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")
}
} |
Note that if you export error variables or types from a package, they will become part of the public API of the package.
호출이 실패할 경우 에러를 전파(propagating)하기 위한 주요 옵션이 있다:
- 추가적인 컨텍스트(additional context)가 없고 원래의 에러 타입을 유지하려는 경우 본래의 에러(original error)를 반환.
- 호출자(callers)가 특정한 에러 케이스를(specific error case)를 감지하거나 다룰(handle) 필요가 없는 경우 fmt.Errorf를 사용.
"connection refused"와 같은 모호한 오류보다, 컨텍스트를 추가하는 것을 추천한다. 따라서 여러분들은 "call service foo: connection refused."와 같이 더욱 유용한 에러를 얻을 수 있을 것이다.
반환된 오류에서 컨텍스트를 추가 할 때, "failed to"와 같은 사족의 명백한 문구를 피하며 컨텍스트를 간결하게 유지하도록 해라. 이러한 문구들이 에러가 스택에 퍼지면서/스며들면서(percolates) 계속해서 쌓이게 된다:
Bad | Good |
---|---|
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %w", err)
} |
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %w", err)
} |
|
|
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.
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 _.
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.
// 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)
}
The single return value form of a type assertion will panic on an incorrect type. Therefore, always use the "comma ok" idiom.
Bad | Good |
---|---|
t := i.(string) |
t, ok := i.(string)
if !ok {
// handle the error gracefully
} |
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.
Bad | Good |
---|---|
func run(args []string) {
if len(args) == 0 {
panic("an argument is required")
}
// ...
}
func main() {
run(os.Args[1:])
} |
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)
}
} |
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.
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.
Bad | Good |
---|---|
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
panic("failed to set up test")
} |
// func TestFoo(t *testing.T)
f, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal("failed to set up test")
} |
Go programs use os.Exit
or log.Fatal*
to exit immediately. (Panicking
is not a good way to exit programs, please don't panic.)
Call one of os.Exit
or log.Fatal*
only in main()
. All other
functions should return errors to signal failure.
Bad | Good |
---|---|
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 := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
return string(b)
} |
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 := ioutil.ReadAll(f)
if err != nil {
return "", err
}
return string(b), nil
} |
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.
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.
Bad | Good |
---|---|
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 := ioutil.ReadAll(f)
if err != nil {
log.Fatal(err)
}
// ...
} |
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 := ioutil.ReadAll(f)
if err != nil {
return err
}
// ...
} |
Performance-specific guidelines apply only to the hot path.
When converting primitives to/from strings, strconv
is faster than
fmt
.
Bad | Good |
---|---|
for i := 0; i < b.N; i++ {
s := fmt.Sprint(rand.Int())
} |
for i := 0; i < b.N; i++ {
s := strconv.Itoa(rand.Int())
} |
|
|
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.
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.
일관성을 가지라는 얘긴데 일관적인 코드는 유지보수가 쉽고, 쉽게 이해할 수 있고, 새로운 컨벤션이나 버그 수정에 있어서 쉽게 변경할 수 있다. 반대로, 여러 충돌하는 코드 스타일을 갖고있는 경우에 유지보수가 힘들어지고, 코드 리뷰 하기 힘들다.
Go supports grouping similar declarations.
Bad | Good |
---|---|
import "a"
import "b" |
import (
"a"
"b"
) |
This also applies to constants, variables, and type declarations.
Bad | Good |
---|---|
const a = 1
const b = 2
var a = 1
var b = 2
type Area float64
type Volume float64 |
const (
a = 1
b = 2
)
var (
a = 1
b = 2
)
type (
Area float64
Volume float64
) |
Only group related declarations. Do not group declarations that are unrelated.
Bad | Good |
---|---|
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
EnvVar = "MY_ENV"
) |
type Operation int
const (
Add Operation = iota + 1
Subtract
Multiply
)
const EnvVar = "MY_ENV" |
Groups are not limited in where they can be used. For example, you can use them inside of functions.
Bad | Good |
---|---|
func f() string {
red := color.New(0xff0000)
green := color.New(0x00ff00)
blue := color.New(0x0000ff)
// ...
} |
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
// ...
} |
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.
Bad | Good |
---|---|
func (c *client) request() {
caller := c.name
format := "json"
timeout := 5*time.Second
var err error
// ...
} |
func (c *client) request() {
var (
caller = c.name
format = "json"
timeout = 5*time.Second
err error
)
// ...
} |
There should be two import groups:
- Standard library
- Everything else
This is the grouping applied by goimports by default.
Bad | Good |
---|---|
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
import (
"fmt"
"os"
"go.uber.org/atomic"
"golang.org/x/sync/errgroup"
) |
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
, notnet/urls
. - Not "common", "util", "shared", or "lib". These are bad, uninformative names.
See also Package Names and Style guideline for Go packages.
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
.
Import aliasing must be used if the package name does not match the last element of the import path.
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.
Bad | Good |
---|---|
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
) |
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
) |
- 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.
Bad | Good |
---|---|
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{}
} |
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 {...} |
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.
Bad | Good |
---|---|
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)
}
} |
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()
} |
If a variable is set in both branches of an if, it can be replaced with a single if.
Bad | Good |
---|---|
var a int
if b {
a = 100
} else {
a = 10
} |
a := 10
if b {
a = 100
} |
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.
Bad | Good |
---|---|
type Client struct {
version int
http.Client
} |
type Client struct {
http.Client
version int
} |
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).
Exception: Mutexes should not be embedded, even on unexported types. See also: Zero-value Mutexes are Valid.
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.
Bad | Good |
---|---|
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
} |
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)
} |
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 |
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 |
type Client struct {
sync.Mutex
sync.WaitGroup
bytes.Buffer
url.URL
} |
type Client struct {
mtx sync.Mutex
wg sync.WaitGroup
buf bytes.Buffer
url url.URL
} |
Short variable declarations (:=
) should be used if a variable is being set to
some value explicitly.
Bad | Good |
---|---|
var s = "foo" |
s := "foo" |
However, there are cases where the default value is clearer when the var
keyword is used. Declaring Empty Slices, for example.
Bad | Good |
---|---|
func f(list []int) {
filtered := []int{}
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
} |
func f(list []int) {
var filtered []int
for _, v := range list {
if v > 10 {
filtered = append(filtered, v)
}
}
} |
nil
is a valid slice of length 0. This means that,
-
You should not return a slice of length zero explicitly. Return
nil
instead.Bad Good if x == "" { return []int{} }
if x == "" { return nil }
-
To check if a slice is empty, always use
len(s) == 0
. Do not check fornil
.Bad Good func isEmpty(s []string) bool { return s == nil }
func isEmpty(s []string) bool { return len(s) == 0 }
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).
Naked parameters in function calls can hurt readability. Add C-style comments
(/* ... */
) for parameter names when their meaning is not obvious.
Bad | Good |
---|---|
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true, true) |
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */) |
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.
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)
Go supports raw string literals, which can span multiple lines and include quotes. Use these to avoid hand-escaped strings which are much harder to read.
Bad | Good |
---|---|
wantError := "unknown name:\"test\"" |
wantError := `unknown error:"test"` |
You should almost always specify field names when initializing structs. This is
now enforced by go vet
.
Bad | Good |
---|---|
k := User{"John", "Doe", true} |
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
} |
Exception: Field names may be omitted in test tables when there are 3 or fewer fields.
tests := []struct{
op Operation
want string
}{
{Add, "add"},
{Subtract, "subtract"},
}
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.
Bad | Good |
---|---|
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
} |
user := User{
FirstName: "John",
LastName: "Doe",
} |
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 can benefit from names of fields even when they are zero-valued.
tests := []struct{
give string
want int
}{
{give: "0", want: 0},
// ...
}
When all the fields of a struct are omitted in a declaration, use the var
form to declare the struct.
Bad | Good |
---|---|
user := User{} |
var user User |
This differentiates zero valued structs from those with non-zero fields similar to the distinction created for map initialization, and matches how we prefer to declare empty slices.
Use &T{}
instead of new(T)
when initializing struct references so that it
is consistent with the struct initialization.
Bad | Good |
---|---|
sval := T{Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar" |
sval := T{Name: "foo"}
sptr := &T{Name: "bar"} |
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.
Bad | Good |
---|---|
var (
// m1 is safe to read and write;
// m2 will panic on writes.
m1 = map[T1]T2{}
m2 map[T1]T2
) |
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. |
Where possible, provide capacity hints when initializing
maps with make()
. See
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.
Bad | Good |
---|---|
m := make(map[T1]T2, 3)
m[k1] = v1
m[k2] = v2
m[k3] = v3 |
m := map[T1]T2{
k1: v1,
k2: v2,
k3: v3,
} |
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).
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.
Bad | Good |
---|---|
msg := "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2) |
const msg = "unexpected values %v, %v\n"
fmt.Printf(msg, 1, 2) |
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.
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.
$ go vet -printfuncs=wrapf,statusf
See also go vet: Printf family check.
Use table-driven tests with subtests to avoid duplicating code when the core test logic is repetitive.
Bad | Good |
---|---|
// 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) |
// 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)
})
} |
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.
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range 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.
tests := []struct{
give string
// ...
}{
// ...
}
for _, tt := range tests {
tt := tt // for t.Parallel
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.
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.
Bad | Good |
---|---|
// package db
func Open(
addr string,
cache bool,
logger *zap.Logger
) (*Connection, error) {
// ...
} |
// 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. 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. db.Open(addr)
db.Open(addr, db.WithLogger(log))
db.Open(addr, db.WithCache(false))
db.Open(
addr,
db.WithCache(false),
db.WithLogger(log),
) |
Our suggested way of implementing this pattern is with an Option
interface
that holds an unexported method, recording options on an unexported options
struct.
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,
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
- golint to point out common style mistakes
- govet to analyze code for common mistakes
- staticcheck to do various static analysis checks
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.