@zhongjianxin
2020-02-11T18:20:27.000000Z
字数 30943
阅读 701
AFS-Golang
Editing this document:
Code Samples:
Use 2 spaces to indent. Horizontal real estate is important in side-by-side
samples.
For side-by-side code samples, use the following snippet.
~~~
Bad | Good |
---|---|
|
|
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")
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
Bad | Good |
---|---|
|
|
|
|
Embed for private types or types that need to implement the Mutex interface. | For exported types, use a private field. |
Bad | Good |
---|---|
|
|
Similarly, be wary of user modifications to maps or slices exposing internal
state.
Bad | Good |
---|---|
|
|
Use defer to clean up resources such as files and locks.
Bad | Good |
---|---|
|
|
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.
Bad | Good |
---|---|
|
|
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 |
---|---|
|
|
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"
to handle timeTime is complicated. Incorrect assumptions often made about time include the
following.
For example, 1 means that adding 24 hours to a time instant will not always
yield a new calendar day.
Therefore, always use the ["time"
] package when dealing with time because it
helps deal with these incorrect assumptions in a safer, more accurate manner.
time.Time
for instants of timeUse [time.Time
] when dealing with instants of time, and the methods on
time.Time
when comparing, adding, or subtracting time.
Bad | Good |
---|---|
|
|
time.Duration
for periods of timeUse [time.Duration
] when dealing with periods of time.
Bad | Good |
---|---|
|
|
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)
time.Time
and time.Duration
with external systemsUse time.Duration
and time.Time
in interactions with external systems when
possible. For example:
flag
] supports time.Duration
via time.ParseDuration
]encoding/json
] supports encoding time.Time
as an RFC 3339 UnmarshalJSON
method]database/sql
] supports converting DATETIME
or TIMESTAMP
columns time.Time
and back if the underlying driver supports itgopkg.in/yaml.v2
] supports time.Time
as an RFC 3339 string, and time.Duration
via [time.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 |
---|---|
|
|
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.
There are various options for declaring errors:
errors.New
] for errors with simple static stringsfmt.Errorf
] for formatted error stringsError()
method"pkg/errors".Wrap
]When returning errors, consider the following to determine the best choice:
errors.New
] Error()
method.fmt.Errorf
] is okay.If the client needs to detect the error, and you have created a simple error
using [errors.New
], use a var for the error.
Bad | Good |
---|---|
|
|
If you have an error that clients may need to detect, and you would like to add
more information to it (e.g., it is not a static string), then you should use a
custom type.
Bad | Good |
---|---|
|
|
Be careful with exporting custom error types directly since they become part of
the public API of the package. It is preferable to expose matcher functions to
check the error instead.
// package foo
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func IsNotFoundError(err error) bool {
_, ok := err.(errNotFound)
return ok
}
func Open(file string) error {
return errNotFound{file: file}
}
// package bar
if err := foo.Open("foo"); err != nil {
if foo.IsNotFoundError(err) {
// handle
} else {
panic("unknown error")
}
}
There are three main options for propagating errors if a call fails:
"pkg/errors".Wrap
] so that the error message provides "pkg/errors".Cause
] can be used to extract the original fmt.Errorf
] if the callers do not need to detect or handle that It is recommended to add context where possible so that instead of a vague
error such as "connection refused", you get more useful errors such as
"call service foo: connection refused".
When adding context to returned errors, keep the context succinct by avoiding
phrases like "failed to", which state the obvious and pile up as the error
percolates up through the stack:
Bad | Good |
---|---|
|
|
|
|
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.
The single return value form of a type assertion will panic on an incorrect
type. Therefore, always use the "comma ok" idiom.
Bad | Good |
---|---|
|
|
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 |
---|---|
|
|
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 |
---|---|
|
|
Atomic operations with the sync/atomic package operate on the raw types
(int32
, int64
, etc.) so it is easy to forget to use the atomic operation to
read or modify the variables.
go.uber.org/atomic adds type safety to these operations by hiding the
underlying type. Additionally, it includes a convenient atomic.Bool
type.
Bad | Good |
---|---|
|
|
Avoid mutating global variables, instead opting for dependency injection.
This applies to function pointers as well as other kinds of values.
Bad | Good |
---|---|
|
|
|
|
These embedded types leak implementation details, inhibit type evolution, and
obscure documentation.
Assuming you have implemented a variety of list types using a shared
AbstractList
, avoid embedding the AbstractList
in your concrete list
implementations.
Instead, hand-write only the methods to your concrete list that will delegate
to the abstract list.
type AbstractList struct {}
// Add adds an entity to the list.
func (l *AbstractList) Add(e Entity) {
// ...
}
// Remove removes an entity from the list.
func (l *AbstractList) Remove(e Entity) {
// ...
}
Bad | Good |
---|---|
|
|
Go allows type embedding as a compromise between inheritance and composition.
The outer type gets implicit copies of the embedded type's methods.
These methods, by default, delegate to the same method of the embedded
instance.
The struct also gains a field by the same name as the type.
So, if the embedded type is public, the field is public.
To maintain backward compatibility, every future version of the outer type must
keep the embedded type.
An embedded type is rarely necessary.
It is a convenience that helps you avoid writing tedious delegate methods.
Even embedding a compatible AbstractList interface, instead of the struct,
would offer the developer more flexibility to change in the future, but still
leak the detail that the concrete lists use an abstract implementation.
Bad | Good |
---|---|
|
|
Either with an embedded struct or an embedded interface, the embedded type
places limits on the evolution of the type.
Although writing these delegate methods is tedious, the additional effort hides
an implementation detail, leaves more opportunities for change, and also
eliminates indirection for discovering the full List interface in
documentation.
Performance-specific guidelines apply only to the hot path.
When converting primitives to/from strings, strconv
is faster than
fmt
.
Bad | Good |
---|---|
|
|
|
|
Do not create byte slices from a fixed string repeatedly. Instead, perform the
conversion once and capture the result.
Bad | Good |
---|---|
|
|
|
|
Where possible, provide capacity hints when initializing
maps with make()
.
make(map[T1]T2, hint)
Providing a capacity hint to make()
tries to right-size the
map at initialization time, which reduces the need for growing
the map and allocations as elements are added to the map. Note
that the capacity hint is not guaranteed for maps, so adding
elements may still allocate even if a capacity hint is provided.
Bad | Good |
---|---|
|
|
`m` is created without a size hint; there may be more allocations at assignment time. | `m` is created with a size hint; there may be fewer allocations at assignment time. |
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 |
---|---|
|
|
This also applies to constants, variables, and type declarations.
Bad | Good |
---|---|
|
|
Only group related declarations. Do not group declarations that are unrelated.
Bad | Good |
---|---|
|
|
Groups are not limited in where they can be used. For example, you can use them
inside of functions.
Bad | Good |
---|---|
|
|
There should be two import groups:
This is the grouping applied by goimports by default.
Bad | Good |
---|---|
|
|
When naming packages, choose a name that is:
net/url
, not net/urls
.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 |
---|---|
|
|
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 |
---|---|
|
|
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 |
---|---|
|
|
If a variable is set in both branches of an if, it can be replaced with a
single if.
Bad | Good |
---|---|
|
|
At the top level, use the standard var
keyword. Do not specify the type,
unless it is not the same type as the expression.
Bad | Good |
---|---|
|
|
Specify the type if the type of the expression does not match the desired type
exactly.
type myError struct{}
func (myError) Error() string { return "error" }
func F() myError { return myError{} }
var _e error = F()
// F returns an object of type myError but we want error.
Prefix unexported top-level var
s and const
s with _
to make it clear when
they are used that they are global symbols.
Exception: Unexported error values, which should be prefixed with err
.
Rationale: Top-level variables and constants have a package scope. Using a
generic name makes it easy to accidentally use the wrong value in a different
file.
Bad | Good |
---|---|
|
|
Embedded types (such as mutexes) 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 |
---|---|
|
|
You should almost always specify field names when initializing structs. This is
now enforced by [go vet
].
Bad | Good |
---|---|
|
|
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"},
}
Short variable declarations (:=
) should be used if a variable is being set to
some value explicitly.
Bad | Good |
---|---|
|
|
However, there are cases where the default value is clearer when the var
keyword is used. Declaring Empty Slices, for example.
Bad | Good |
---|---|
|
|
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 |
---|---|
|
|
To check if a slice is empty, always use len(s) == 0
. Do not check for
nil
.
Bad | Good |
---|---|
|
|
The zero value (a slice declared with var
) is usable immediately without
make()
.
Bad | Good |
---|---|
|
|
Where possible, reduce scope of variables. Do not reduce the scope if it
conflicts with Reduce Nesting.
Bad | Good |
---|---|
|
|
If you need a result of a function call outside of the if, then you should not
try to reduce the scope.
Bad | Good |
---|---|
|
|
Naked parameters in function calls can hurt readability. Add C-style comments
(/* ... */
) for parameter names when their meaning is not obvious.
Bad | Good |
---|---|
|
|
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 = 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 |
---|---|
|
|
Use &T{}
instead of new(T)
when initializing struct references so that it
is consistent with the struct initialization.
Bad | Good |
---|---|
|
|
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 |
---|---|
|
|
Declaration and initialization are visually similar. | Declaration and initialization are visually distinct. |
Where possible, provide capacity hints when initializing
maps with make()
. See
Prefer 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 |
---|---|
|
|
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 |
---|---|
|
|
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 |
---|---|
|
|
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 {
// ...
}
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.
// 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) {
// ...
}
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)
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 |
---|---|
|
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,