uber-go/guide Chinese translation
English Document Link
Uber Go language coding specifications
Uber is a technology company in Silicon Valley, USA, and an early adopter of the Go language. It has open sourced many golang projects, such as zap and jaegerGo style specification to GitHub. After a year of accumulation and updates, the specification has begun to take shape and is widely accepted. Gopher's attention. This article is the Chinese version of the specification. This version will be updated in real time based on the original version.
Version
- Current updated version: 2021-07-09 Version address:commit:#130
Table of contents
- Chinese translation of uber-go/guide
- English
- Uber Go language coding specifications
- Version
- Table of contents
- introduce
- Guiding Principles
- pointer to interface
- Interface plausibility verification
- Receiver and interface
- A zero value Mutex is valid
- Copy Slices and Maps at boundaries
- Use defer to release resources
- Channel size is either 1 or unbuffered
- Enumeration starts from 1
- Use time to handle time
- error type
- Error Wrapping
- Handling type assertion failure
- don't panic
- Use go.uber.org/atomic
- Avoid mutable global variables
- Avoid embedding types in public structures
- Avoid using built-in names
- avoid using
init()
- Prioritize specifying the slice capacity when appending
- Main function exit method (Exit)
- performance
- specification
- consistency
- Similar statements are grouped together
- import grouping
- Package names
- Function name
- Import alias
- Function grouping and order
- Reduce nesting
- unnecessary else
- Top-level variable declaration
- For unexported top-level constants and variables, use _ as a prefix
- Embedding in structures
- Initialize the structure using field names
- local variable declaration
- nil is a valid slice
- Reduce variable scope
- Avoid Naked Parameters
- Use raw string literals, avoid escaping
- Initialization structure
- Initialize Maps
- string string format
- Named Printf-style functions
- programming mode
- Linting
- Stargazers over time
introduce
Styles are the conventions that govern our code. The term样式
is a bit of a misnomer, as these conventions cover more than just the source file formats that gofmt handles for us.
The purpose of this guide is to manage this complexity by detailing the considerations for writing Go code at Uber. These rules exist to make the codebase manageable while still allowing engineers to use Go language features more efficiently.
This guide was originally written byPrashant Varanasi andSimon Newton Written to enable some colleagues to use Go quickly. Over the years, the guide has been modified based on feedback from others.
This document documents the conventions we follow in Go code at Uber. Many of these are general guidelines for Go, while others extend the guidelines below and rely on external guidelines:
All code should pass the golint
and go vet
checks without errors. We recommend setting your editor to:
- Run on save
goimports
- running
golint
sumgo vet
检查错误
You can find more detailed information on the following Go editor tool support page:
https://github.com/golang/go/wiki/IDEsAndTextEditorPlugins
Guiding Principles
pointer to interface
You rarely need pointers to interface types. You should pass the interface as a value, and in such a pass, the underlying data passed can still be a pointer in essence.
The interface is essentially represented by two fields under the hood:
- A pointer to some specific type of information. You can think of it as a "type".
- data pointer. If the stored data is a pointer, it is stored directly. If the data being stored is a value, a pointer to that value is stored.
If you want an interface method to modify the underlying data, you must use pointer passing (assigning an object pointer to an interface variable).
type F interface {
f()
}
type S1 struct{
}
func (s S1) f() {
}
type S2 struct{
}
func (s *S2) f() {
}
// f1.f()无法修改底层数据
// f2.f() 可以修改底层数据,给接口变量f2赋值时使用的是对象指针
var f1 F = S1{
}
var f2 F = &S2{
}
Interface plausibility verification
Verify interface compliance at compile time. This includes:
- Check exported types that implement a specific interface as part of the interface API
- Types (exported and non-exported) that implement the same interface belong to the collection of implementation types
- Any scenario that violates the interface rationality check will terminate the compilation and notify the user
Supplement: The above three items are the compiler's checking mechanism for interfaces.
Generally speaking, incorrect use of interfaces will cause errors during compilation.
So This mechanism can be used to expose some problems during compilation.
Bad | Good |
---|---|
// 如果Handler没有实现http.Handler,会在运行时报错
type Handler struct {
// ...
}
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
...
}
type Handler struct {
// ...
}
// 用于触发编译期的接口的合理性检查机制
// 如果Handler没有实现http.Handler,会在编译期报错
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
If*Handler
does not match the interface of http.Handler
,
then statementvar _ http.Handler = (*Handler)(nil)
Will fail to compile.
The right-hand side of the assignment should be the zero value of the asserted type.
For pointer types (such as *Handler
), slices, and maps, this is nil
;
For structure types, this is the empty structure.
type LogHandler struct {
h http.Handler
log *zap.Logger
}
var _ http.Handler = LogHandler{
}
func (h LogHandler) ServeHTTP(
w http.ResponseWriter,
r *http.Request,
) {
// ...
}
Receiver and interface
Methods using value receivers can be called either by value or by pointer.
Methods with pointer receivers can only be called with 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"}}
// 你只能通过值调用 Read
sVals[1].Read()
// 这不能编译通过:
// sVals[1].Write("test")
sPtrs := map[int]*S{
1: {
"A"}}
// 通过指针既可以调用 Read,也可以调用 Write 方法
sPtrs[1].Read()
sPtrs[1].Write("test")
Similarly, even if a method has a value receiver, it can also use a pointer receiver to satisfy the interface.
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
// 下面代码无法通过编译。因为 s2Val 是一个值,而 S2 的 f 方法中没有使用值接收器
// i = s2Val
There is a great explanation of pointers vs. values in . Effective Go
Replenish:
- A type can have a value receiver method set and a pointer receiver method set.
- The value receiver method set is a subset of the pointer receiver method set, but not vice versa.
- rule
- Value objects can only use the value receiver method set
- Pointer objects can use value receiver method set + pointer receiver method set
- Interface matching (or implementation)
- The type implements all methods of the interface, which is called matching
- Specifically speaking, either the value method set of the type matches the interface, or the pointer method set matches the interface.
There are two specific types of matching:
- Value method set matches interface
- Whether it is a value or a pointer object assigned to an interface variable, it is ok because it contains a set of value methods.
- Pointer method set and interface match
- Pointer objects can only be assigned to interface variables because only the pointer method set matches the interface.
- If a value object is assigned to an interface variable, an error will be reported at compile time (the interface rationality check mechanism will be triggered)
Why does i = s2Val report an error because the value method set does not match the interface.
A zero value Mutex is valid
The zero values sync.Mutex
and sync.RWMutex
are valid. So pointers to mutex are basically unnecessary.
Bad | Good |
---|---|
mu := new(sync.Mutex)
mu.Lock()
var mu sync.Mutex
mu.Lock()
If you use a structure pointer, the mutex should be used as a non-pointer field of the structure. Even if the structure is not exported, do not embed mutex directly into the structure.
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]
}
Mutex
The fields, Lock
and Unlock
methods are an unspecified part of the API exported by SMap
.
The mutex and its methods are implementation details of SMap
and are not visible to its caller.
Copy Slices and Maps at boundaries
Slices and maps contain pointers to the underlying data, so be careful when copying them.
Receive Slices and Maps
Remember that when a map or slice is passed in as a function argument, if you store a reference to them, the user can modify it.
Bad | Good |
---|---|
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
}
trips := ...
d1.SetTrips(trips)
// 你是要修改 d1.trips 吗?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
}
trips := ...
d1.SetTrips(trips)
// 这里我们修改 trips[0],但不会影响到 d1.trips
trips[0] = ...
Return slices or maps
Likewise, be aware of user modifications to maps or slices that expose internal state.
Bad | Good |
---|---|
type Stats struct {
mu sync.Mutex
counters map[string]int
}
// Snapshot 返回当前状态。
func (s *Stats) Snapshot() map[string]int {
s.mu.Lock()
defer s.mu.Unlock()
return s.counters
}
// snapshot 不再受互斥锁保护
// 因此对 snapshot 的任何访问都将受到数据竞争的影响
// 影响 stats.counters
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 现在是一个拷贝
snapshot := stats.Snapshot()
Use defer to release resources
Use defer to release 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
// 当有多个 return 分支时,很容易遗忘 unlock
p.Lock()
defer p.Unlock()
if p.count < 10 {
return p.count
}
p.count++
return p.count
// 更可读
The overhead of Defer is very small and should be avoided only if you can prove that the function execution time is in the nanosecond range. The readability improvements of using defers are worthwhile because the cost of using them is negligible. Especially useful for larger methods that do more than simple memory access, where the resource consumption of other calculations far exceeds defer
.
Channel size is either 1 or unbuffered
Channel should normally have size 1 or be unbuffered. By default, a channel is unbuffered and its size is zero. Any other sizes must undergo strict scrutiny. We need to consider how the size is determined, consider what prevents the channel from being written under high load and blocking writes, and what changes to the system logic when this happens. (Translation explanation: According to the original meaning, channel boundaries, race conditions, and logical context need to be defined)
Bad | Good |
---|---|
// 应该足以满足任何情况!
c := make(chan int, 64)
// 大小:1
c := make(chan int, 1) // 或者
// 无缓冲 channel,大小为 0
c := make(chan int)
Enumeration starts from 1
The standard way to introduce enumerations in Go is to declare a custom type and a const group using iota. Since the default value of a variable is 0, you should usually start an enumeration with 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 situations where it makes sense to use a zero value (enumerations start at zero), for example, when a zero value is the desired default behavior.
type LogOutput int
const (
LogToStdout LogOutput = iota
LogToFile
LogToRemote
)
// LogToStdout=0, LogToFile=1, LogToRemote=2
Use time to handle time
Time processing is complex. Wrong assumptions about time often include the following.
- There are 24 hours in a day
- There are 60 minutes in an hour
- There are seven days in a week
- 365 days a year
- there are more
For example, 1 means that adding 24 hours to a point in time does not always produce a new calendar day.
Therefore, always use the "time"
package when dealing with times as it helps to handle these incorrect assumptions in a safer and more accurate way.
Use time.Time
to express instantaneous time
Use time.Time
when working with instants of time, and methods in time.Time
when comparing, adding, or subtracting times.
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
to express time period
Use time.Duration
when dealing with time periods.
Bad | Good |
---|---|
func poll(delay int) {
for {
// ...
time.Sleep(time.Duration(delay) * time.Millisecond)
}
}
poll(10) // 是几秒钟还是几毫秒?
func poll(delay time.Duration) {
for {
// ...
time.Sleep(delay)
}
}
poll(10*time.Second)
Going back to the first example of adding 24 hours to a moment in time, the method we use to add time depends on the intent. If we want the same point in time on the next calendar day (the day after the current day), we should use Time.AddDate
. However, if we want to ensure that a certain moment is 24 hours later than the previous moment, we should use Time.Add
.
newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)
Use of external system time.Time
Sum time.Duration
Use time.Duration
and time.Time
whenever possible in interactions with external systems. For example:
-
Command-line title:
flag
Communicationtime.ParseDuration
Supporttime.Duration
-
JSON:
encoding/json
UnmarshalJSON
methodtime.Time
编码为 RFC 3339 Character skewer -
SQL:
database/sql
supports convertingDATETIME
orTIMESTAMP
columns totime.Time
, Returns if supported by the underlying driver -
YAML:
gopkg.in/yaml.v2
Support generaltime.Time
Work RFC 3339 Character skewer , 并通过time.ParseDuration
Supporttime.Duration
.
When time.Duration
cannot be used in these interactions, use int
or float64
in the field name Contains units.
For example, because 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 time.Time
cannot be used in these interactions, use string
and unless agreed, RFC 3339 The format timestamp defined in . By default, Time.UnmarshalText
uses this format and is available in and via time.RFC3339
use. Time.Format
time.Parse
Although this is not a problem in practice, keep in mind that the "time"
package does not support parsing leap second timestamps (8728) are not considered in the calculation. If you compare two instants of time, the difference will not include leap seconds that may have occurred between the two instants. 15190), and leap seconds (
error type
There are various options for declaring errors in Go:
errors.New
Error for simple static stringfmt.Errorf
Error string used for formatting- Custom type that implements
Error()
method - 用
"pkg/errors".Wrap
的 Wrapped errors
When an error is returned, consider the following factors to determine the best option:
-
Is this a simple error that requires no additional information? If so,
errors.New
is sufficient. -
Does the customer need to detect and handle this error? If so, you should use a custom type and implement the
Error()
method. -
Are you propagating errors returned by downstream functions? If so, check out the section on error wrapping later in this article.
-
Otherwise
fmt.Errorf
is fine.
If the client needs to detect errors and you have created a simple error using errors.New
, use an error variable.
Bad | Good |
---|---|
// package foo
func Open() error {
return errors.New("could not open")
}
// package bar
func use() {
if err := foo.Open(); err != nil {
if err.Error() == "could not open" {
// handle
} else {
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
} else {
panic("unknown error")
}
}
If you have an error that may require client side detection, and you want to add more information to it (e.g. it's not a static string), you should use a custom type.
Bad | Good |
---|---|
func open(file string) error {
return fmt.Errorf("file %q not found", file)
}
func use() {
if err := open("testfile.txt"); err != nil {
if strings.Contains(err.Error(), "not found") {
// handle
} else {
panic("unknown error")
}
}
}
type errNotFound struct {
file string
}
func (e errNotFound) Error() string {
return fmt.Sprintf("file %q not found", e.file)
}
func open(file string) error {
return errNotFound{
file: file}
}
func use() {
if err := open("testfile.txt"); err != nil {
if _, ok := err.(errNotFound); ok {
// handle
} else {
panic("unknown error")
}
}
}
Be careful when exporting custom error types directly, as they become part of the package's public API. It's better to expose the matcher function to check for errors.
// 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")
}
}
Error Wrapping
When a (function/method) call fails, there are three main ways errors are propagated:
- If there is no additional context to add and you want to maintain the original error type, return the original error.
- Add context, use
"pkg/errors".Wrap
so that the error message provides more context,"pkg/errors".Cause
can be used to extract the original error. - Use
fmt.Errorf
if there is a specific error condition that the caller does not need to detect or handle.
It is recommended to add context where possible so that you get more useful errors like "Invoking service foo: Connection refused" rather than obscure errors like "Connection refused".
When adding context to returned errors, keep context concise by avoiding phrases like "failed to" that state the obvious and accumulate as errors percolate up the stack:
Bad | Good |
---|---|
s, err := store.New()
if err != nil {
return fmt.Errorf(
"failed to create new store: %v", err)
}
s, err := store.New()
if err != nil {
return fmt.Errorf(
"new store: %v", err)
}
failed to x: failed to y: failed to create new store: the error
x: y: new store: the error
However, once the error is sent to another system, it should be clear that the message is an error message (such as using the err
tag, or prefixing the log with "Failed").
See alsoDon't just check errors, handle them gracefully. >
Handling type assertion failure
The single return form of type assertion will panic for the incorrect type. Therefore, always use the "comma ok" idiom.
Bad | Good |
---|---|
t := i.(string)
t, ok := i.(string)
if !ok {
// 优雅地处理错误
}
don't panic
Code running in production must avoid panics. Panics are the main source of cascading failures . If an error occurs, the function must return the 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 unrecoverable happens (for example, a nil reference). An exception is program initialization: undesirable conditions that should cause the program to abort when it starts may cause a panic.
var _statusTemplate = template.Must(template.New("name").Parse("_statusHTML"))
Even in test code, prefer using t.Fatal
or t.FailNow
instead of panic to ensure that failures are marked.
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")
}
Use go.uber.org/atomic
Usesync/atomic package's atomic operations on primitive types (int32
, < a i=4>, etc.) because it is easy to forget to use atomic operations to read or modify variables. int64
go.uber.org/atomic adds type safety to these operations by hiding the underlying type. Additionally, it includes a convenientatomic.Bool
type.
Bad | Good |
---|---|
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!
}
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()
}
Avoid mutable global variables
Use selective dependency injection to avoid changing global variables.
Applies to both function pointers and other value types
Bad | Good |
---|---|
// sign.go
var _timeNow = time.Now
func sign(msg string) string {
now := _timeNow()
return signWithTime(msg, now)
}
// 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)
}
// 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))
}
// sign_test.go
func TestSigner(t *testing.T) {
s := newSigner()
s.now = func() time.Time {
return someFixedTime
}
assert.Equal(t, want, s.Sign(give))
}
Avoid embedding types in public structures
These embedded types leak implementation details, prohibit type evolution, and obscure documentation.
Assuming you implement multiple list types using shared AbstractList
, avoid embedding AbstractList
in specific list implementations.
Instead, just manually write the method to the concrete list, which will delegate to the abstract list.
type AbstractList struct {
}
// 添加将实体添加到列表中。
func (l *AbstractList) Add(e Entity) {
// ...
}
// 移除从列表中移除实体。
func (l *AbstractList) Remove(e Entity) {
// ...
}
Bad | Good |
---|---|
// ConcreteList 是一个实体列表。
type ConcreteList struct {
*AbstractList
}
// ConcreteList 是一个实体列表。
type ConcreteList struct {
list *AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}
Go allows type embedding as a compromise between inheritance and composition.
The outer type gets an implicit copy of the embedded type's method.
By default, these methods delegate to the same method of the embedded instance.
The structure also gets a field with the same name as the type.
So, if the embedded type is public, then the field is public. To maintain backward compatibility, every future version of the external type must preserve the embedded type.
Embedded types are rarely needed.
This is a convenience that helps you avoid writing lengthy delegate methods.
Even embedding a compatible abstraction listinterface instead of a struct will give developers greater flexibility to make changes in the future , but still leaks the details of concrete list implementation using abstraction.
Bad | Good |
---|---|
// AbstractList 是各种实体列表的通用实现。
type AbstractList interface {
Add(Entity)
Remove(Entity)
}
// ConcreteList 是一个实体列表。
type ConcreteList struct {
AbstractList
}
// ConcreteList 是一个实体列表。
type ConcreteList struct {
list AbstractList
}
// 添加将实体添加到列表中。
func (l *ConcreteList) Add(e Entity) {
l.list.Add(e)
}
// 移除从列表中移除实体。
func (l *ConcreteList) Remove(e Entity) {
l.list.Remove(e)
}
Whether using embedded structures or embedded interfaces, embedded types limit the evolution of the type.
- Adding methods to an embedded interface is a breaking change.
- Removing embedded types is a breaking change.
- Even replacing an embedded type with an alternative that satisfies the same interface is a breaking change.
Although writing these delegate methods is tedious, the extra work hides implementation details, leaves more opportunities for changes, and eliminates the indirection of discovering the full list interface in the documentation.
Avoid using built-in names
Go language specificationlanguage specification outlines several built-ins that
should not be used in Go projects Name identifierspredeclared identifiers used.
Reusing these identifiers as names, depending on the context,
will hide the original identifier in the current scope (or any nested scopes), or obfuscate the code .
In the best case, the compiler will complain; in the worst case, such code may introduce potentially difficult-to-recover errors.
Bad | Good |
---|---|
var error string
// `error` 作用域隐式覆盖
// or
func handleErrorMessage(error string) {
// `error` 作用域隐式覆盖
}
var errorMessage string
// `error` 指向内置的非覆盖
// or
func handleErrorMessage(msg string) {
// `error` 指向内置的非覆盖
}
type Foo struct {
// 虽然这些字段在技术上不构成阴影,但`error`或`string`字符串的重映射现在是不明确的。
error error
string string
}
func (f Foo) Error() error {
// `error` 和 `f.error` 在视觉上是相似的
return f.error
}
func (f Foo) String() string {
// `string` and `f.string` 在视觉上是相似的
return f.string
}
type Foo struct {
// `error` and `string` 现在是明确的。
err error
str string
}
func (f Foo) Error() error {
return f.err
}
func (f Foo) String() string {
return f.str
}
Note that the compiler will not generate an error when using pre-separated identifiers,
but tools such as go vet
will correctly indicate Implicit questions in these and other situations.
avoid usinginit()
Avoid usinginit()
if possible. When init()
is unavoidable or desirable, the code should first try:
- Be completely certain regardless of the program environment or calls.
- Avoid relying on the order or side effects of other
init()
functions. Althoughinit()
the order is unambiguous, the code can change,
so the relationships betweeninit()
functions may cause the code to become Fragile and error-prone. - Avoid accessing or manipulating global or environmental state, such as machine information, environment variables, working directories, program parameters/inputs, etc.
- Avoid
I/O
, including file system, network, and system calls.
Code that does not meet these requirements may be part of a call to be made as part of main()
(or elsewhere in the program life cycle),
or as < /span> rather than performing "init magic"main()
is written as part of itself. In particular, libraries intended for use by other programs should take special care to be completely deterministic,
Bad | Good |
---|---|
type Foo struct {
// ...
}
var _defaultFoo Foo
func init() {
_defaultFoo = Foo{
// ...
}
}
var _defaultFoo = Foo{
// ...
}
// or, 为了更好的可测试性:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
return Foo{
// ...
}
}
type Config struct {
// ...
}
var _config Config
func init() {
// Bad: 基于当前目录
cwd, _ := os.Getwd()
// Bad: I/O
raw, _ := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
yaml.Unmarshal(raw, &_config)
}
type Config struct {
// ...
}
func loadConfig() Config {
cwd, err := os.Getwd()
// handle err
raw, err := ioutil.ReadFile(
path.Join(cwd, "config", "config.yaml"),
)
// handle err
var config Config
yaml.Unmarshal(raw, &config)
return config
}
Given the above, there are certain circumstances where init()
may be preferable or necessary, which may include:
-
Complex expressions that cannot be expressed as a single assignment.
-
Insertable hooks, such as
database/sql
, encoding type registry, etc. -
Optimizations forGoogle Cloud Functions and other forms of deterministic precomputation.
Prioritize specifying the slice capacity when appending
Prioritize specifying the slice capacity when appending
When possible, provide a capacity value formake()
when initializing the slice to be appended.
Bad | Good |
---|---|
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
BenchmarkBad-4 100000000 2.48s
BenchmarkGood-4 100000000 0.21s
Main function exit method (Exit)
The Go program uses os.Exit
or log.Fatal*
to exit immediately (using panic
is not a good way to exit the program, please a>.)don't panic
**Only calls one of or in main()
**. All other functions should return errors on signal failure. os.Exit
log.Fatal*
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
}
In principle: there are some problems with exiting programs with multiple functions:
- Obvious control flow: Any function can exit the program, so it is difficult to reason about control flow.
- is difficult to test: a function that exits the program will also exit the test that called it. This makes the function difficult to test and introduces the risk of skipping other tests that
go test
have not yet run. - Skip cleanup: When the function exits the program, it will skip the function calls that have entered the
defer
queue. This increases the risk of skipping important cleanup tasks.
One-time exit
If possible, call at most once yourmain()
functionor. If there are multiple error scenarios that stop program execution, put that logic under a separate function and return errors from it. This shortens the function and puts all the key business logic into a single, testable function. os.Exit
log.Fatal
main()
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()
// 如果我们调用log.Fatal 在这条线之后
// f.Close 将会被执行.
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 for performance only apply to high-frequency scenarios.
Prefer strconv over fmt
When converting primitives to and 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())
}
BenchmarkFmtSprint-4 143 ns/op 2 allocs/op
BenchmarkStrconv-4 64.2 ns/op 1 allocs/op
Avoid string to byte conversion
Don't repeatedly create byte slices from fixed strings. Instead, perform a conversion and capture the result.
Bad | Good |
---|---|
for i := 0; i < b.N; i++ {
w.Write([]byte("Hello world"))
}
data := []byte("Hello world")
for i := 0; i < b.N; i++ {
w.Write(data)
}
BenchmarkBad-4 50000000 22.2 ns/op
BenchmarkGood-4 500000000 3.25 ns/op
Specify container capacity
Specify the container capacity whenever possible so that memory is pre-allocated for the container. This will minimize subsequent allocations (by copying and resizing the container) when adding elements.
Specify Map capacity prompt
Wherever possible, provide capacity information when initializing with make()
make(map[T1]T2, hint)
Providing a capacity hint tomake()
will attempt to resize the map on initialization, which will reduce memory reallocation for the map when elements are added to the map.
Note that it is different from slices. The map capacity hint does not guarantee full preemptive allocation, but is used to estimate the number of hashmap buckets required.
Therefore, allocations may still occur when adding elements to the map, even when the map capacity is specified.
Bad | Good |
---|---|
m := make(map[string]os.FileInfo)
files, _ := ioutil.ReadDir("./files")
for _, f := range files {
m[f.Name()] = f
}
files, _ := ioutil.ReadDir("./files")
m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
m[f.Name()] = f
}
m
is created without size hints; more allocations may occur at runtime.
m
Are created with size hints; fewer allocations may occur at runtime.
Specify slice capacity
Wherever possible, provide capacity information when initializing slices usingmake()
, especially when appending slices.
make([]T, length, capacity)
Unlike maps, slice capacity is not a hint: the compiler will allocate enough memory for the capacity of the slice provided to make()
,
which means Subsequent append()` operations will result in zero allocations (until the slice's length matches the capacity, after which any append may be resized to accommodate additional elements).
Bad | Good |
---|---|
for n := 0; n < b.N; n++ {
data := make([]int, 0)
for k := 0; k < size; k++{
data = append(data, k)
}
}
for n := 0; n < b.N; n++ {
data := make([]int, 0, size)
for k := 0; k < size; k++{
data = append(data, k)
}
}
BenchmarkBad-4 100000000 2.48s
BenchmarkGood-4 100000000 0.21s
specification
consistency
Some of the criteria outlined in this article are objective assessments, based on scenario, context, or subjective judgments;
But most importantly,be consistent.
Consistent code is easier to maintain, more reasonable, requires less learning costs, and is easier to migrate, update, and fix bugs as new conventions emerge or errors occur.
Conversely, having multiple disparate or conflicting coding styles in a codebase can lead to maintenance cost overhead, uncertainty, and cognitive bias. All of this leads directly to slowdowns, painful code reviews, and increased bug counts.
When applying these standards to a code base, it is recommended that changes be made at the package (or larger) level, subpackage-level applications violate the above concerns by introducing multiple styles into the same code.
Similar statements are grouped together
The Go language supports placing similar declarations in a group.
Bad | Good |
---|---|
import "a"
import "b"
import (
"a"
"b"
)
The same 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
)
Group only related statements together. Do not group unrelated statements together.
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"
There are no restrictions on where groups can be used, for example: you can use them inside functions:
Bad | Good |
---|---|
func f() string {
var red = color.New(0xff0000)
var green = color.New(0x00ff00)
var blue = color.New(0x0000ff)
...
}
func f() string {
var (
red = color.New(0xff0000)
green = color.New(0x00ff00)
blue = color.New(0x0000ff)
)
...
}
import grouping
Imports should be divided into two groups:
- standard library
- Other libraries
By default, this is the grouping for goimports applications.
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"
)
Package names
When naming a package, choose a name according to the following rules:
- All lowercase. No capitalization or underlining.
- In most cases when using named imports, renaming is not required.
- Be short and concise. Remember to fully identify the name wherever it is used.
- does not require plural form. For example,
net/url
, notnet/urls
. - Do not use "common", "util", "shared" or "lib". These are bad, uninformative names.
Package Names 和 Go Package Instructions.
Function name
We follow the Go community's convention of using MixedCaps as function names. There is an exception, in order to group related test cases, the function name may contain underscores, such as: TestMyFunction_WhatIsBeingTested
.
Import alias
If the package name does not match the last element of the import path, an import alias must be used.
import (
"net/http"
client "example.com/client-go"
trace "example.com/trace/v2"
)
In all other cases, avoid importing aliases unless there is a direct conflict between the imports.
Bad | Good |
---|---|
import (
"fmt"
"os"
nettrace "golang.net/x/trace"
)
import (
"fmt"
"os"
"runtime/trace"
nettrace "golang.net/x/trace"
)
Function grouping and order
- Functions should be ordered in rough calling order.
- Functions in the same file should be grouped by recipient.
Therefore, the exported function should appear first in the file, defined in struct
, const
, var
later.
A newXYZ()
/ may appear after the type is defined but before the rest of the receiver's methodsNewXYZ()
Since functions are grouped by receiver, normal utility functions should appear at 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 {
...}
Reduce nesting
Code should reduce nesting by handling error cases/special cases first whenever possible and returning or continuing the loop as early as possible. Reduce the amount of code that is nested at 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()
}
unnecessary else
If a variable is set in both branches of the 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
}
Top-level variable declaration
At the top level, use standardvar
keywords. Do not specify a type unless it is different from the type of the expression.
Bad | Good |
---|---|
var _s string = F()
func F() string {
return "A" }
var _s = F()
// 由于 F 已经明确了返回一个字符串类型,因此我们没有必要显式指定_s 的类型
// 还是那种类型
func F() string {
return "A" }
If the expression's type does not exactly match the required type, specify the type.
type myError struct{
}
func (myError) Error() string {
return "error" }
func F() myError {
return myError{
} }
var _e error = F()
// F 返回一个 myError 类型的实例,但是我们要 error 类型
For unexported top-level constants and variables, use _ as a prefix
Prefix _ on unexported top-level vars
and consts
to make it clear when they are used that they are global symbols.
Exception: Unexported error value, should start with err
.
Basic rationale: Top-level variables and constants have package-wide scope. Using a common name can make it easy to accidentally use the wrong value in other files.
Bad | Good |
---|---|
// 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.
}
// foo.go
const (
_defaultPort = 8080
_defaultUser = "user"
)
Embedding in structures
Embedded types (such as mutex) should be at the top of the field list inside the structure, and there must be a blank line separating the embedded fields from the regular fields.
Bad | Good |
---|---|
type Client struct {
version int
http.Client
}
type Client struct {
http.Client
version int
}
Inline should provide tangible benefits, such as adding or enhancing functionality in a semantically appropriate manner.
It should do the job without adversely affecting the user (see also: 避免在公共结构中嵌入类型
Avoid Embedding Types in Public Structs).
嵌入 不应该:
- 纯粹是为了美观或方便。
- 使外部类型更难构造或使用。
- 影响外部类型的零值。如果外部类型有一个有用的零值,则在嵌入内部类型之后应该仍然有一个有用的零值。
- 作为嵌入内部类型的副作用,从外部类型公开不相关的函数或字段。
- 公开未导出的类型。
- 影响外部类型的复制形式。
- 更改外部类型的API或类型语义。
- 嵌入内部类型的非规范形式。
- 公开外部类型的实现详细信息。
- 允许用户观察或控制类型内部。
- 通过包装的方式改变内部函数的一般行为,这种包装方式会给用户带来一些意料之外情况。
简单地说,有意识地和有目的地嵌入。一种很好的测试体验是,
“是否所有这些导出的内部方法/字段都将直接添加到外部类型”
如果答案是some
或no
,不要嵌入内部类型-而是使用字段。
Bad | Good |
---|---|
type A struct {
// Bad: A.Lock() and A.Unlock() 现在可用
// 不提供任何功能性好处,并允许用户控制有关A的内部细节。
sync.Mutex
}
type countingWriteCloser struct {
// Good: Write() 在外层提供用于特定目的,
// 并且委托工作到内部类型的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: 指针更改零值的有用性
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: 有用的零值
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
}
使用字段名初始化结构体
初始化结构体时,应该指定字段名称。现在由 go vet
强制执行。
Bad | Good |
---|---|
k := User{
"John", "Doe", true}
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
例外:如果有 3 个或更少的字段,则可以在测试表中省略字段名称。
tests := []struct{
op Operation
want string
}{
{
Add, "add"},
{
Subtract, "subtract"},
}
本地变量声明
如果将变量明确设置为某个值,则应使用短变量声明形式 (:=
)。
Bad | Good |
---|---|
var s = "foo"
s := "foo"
但是,在某些情况下,var
使用关键字时默认值会更清晰。例如,声明空切片。
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 是一个有效的 slice
nil
是一个有效的长度为 0 的 slice,这意味着,
-
您不应明确返回长度为零的切片。应该返回
nil
来代替。Bad Good if x == "" { return []int{ } }
if x == "" { return nil }
-
要检查切片是否为空,请始终使用
len(s) == 0
。而非nil
。Bad Good func isEmpty(s []string) bool { return s == nil }
func isEmpty(s []string) bool { return len(s) == 0 }
-
零值切片(用
var
声明的切片)可立即使用,无需调用make()
创建。Bad Good nums := []int{ } // or, nums := make([]int) if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
var nums []int if add1 { nums = append(nums, 1) } if add2 { nums = append(nums, 2) }
记住,虽然nil切片是有效的切片,但它不等于长度为0的切片(一个为nil,另一个不是),并且在不同的情况下(例如序列化),这两个切片的处理方式可能不同。
缩小变量作用域
如果有可能,尽量缩小变量作用范围。除非它与 减少嵌套的规则冲突。
Bad | Good |
---|---|
err := ioutil.WriteFile(name, data, 0644)
if err != nil {
return err
}
if err := ioutil.WriteFile(name, data, 0644); err != nil {
return err
}
如果需要在 if 之外使用函数调用的结果,则不应尝试缩小范围。
Bad | Good |
---|---|
if data, err := ioutil.ReadFile(name); err == nil {
err = cfg.Decode(data)
if err != nil {
return err
}
fmt.Println(cfg)
return nil
} else {
return err
}
data, err := ioutil.ReadFile(name)
if err != nil {
return err
}
if err := cfg.Decode(data); err != nil {
return err
}
fmt.Println(cfg)
return nil
避免参数语义不明确(Avoid Naked Parameters)
函数调用中的意义不明确的参数
可能会损害可读性。当参数名称的含义不明显时,请为参数添加 C 样式注释 (/* ... */
)
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 */)
对于上面的示例代码,还有一种更好的处理方式是将上面的 bool
类型换成自定义类型。将来,该参数可以支持不仅仅局限于两个状态(true/false)。
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 支持使用 原始字符串字面值,也就是 " ` " 来表示原生字符串,在需要转义的场景下,我们应该尽量使用这种方案来替换。
可以跨越多行并包含引号。使用这些字符串可以避免更难阅读的手工转义的字符串。
Bad | Good |
---|---|
wantError := "unknown name:\"test\""
wantError := `unknown error:"test"`
初始化结构体
使用字段名初始化结构
初始化结构时,几乎应该始终指定字段名。目前由go vet
强制执行。
Bad | Good |
---|---|
k := User{
"John", "Doe", true}
k := User{
FirstName: "John",
LastName: "Doe",
Admin: true,
}
例外:当有3个或更少的字段时,测试表中的字段名may可以省略。
tests := []struct{
op Operation
want string
}{
{
Add, "add"},
{
Subtract, "subtract"},
}
省略结构中的零值字段
初始化具有字段名的结构时,除非提供有意义的上下文,否则忽略值为零的字段。
也就是,让我们自动将这些设置为零值
Bad | Good |
---|---|
user := User{
FirstName: "John",
LastName: "Doe",
MiddleName: "",
Admin: false,
}
user := User{
FirstName: "John",
LastName: "Doe",
}
这有助于通过省略该上下文中的默认值来减少阅读的障碍。只指定有意义的值。
在字段名提供有意义上下文的地方包含零值。例如,表驱动测试 中的测试用例可以受益于字段的名称,即使它们是零值的。
tests := []struct{
give string
want int
}{
{
give: "0", want: 0},
// ...
}
对零值结构使用 var
如果在声明中省略了结构的所有字段,请使用 var
声明结构。
Bad | Good |
---|---|
user := User{
}
var user User
这将零值结构与那些具有类似于为[初始化 Maps]创建的,区别于非零值字段的结构区分开来,
并与我们更喜欢的declare empty slices方式相匹配。
初始化 Struct 引用
在初始化结构引用时,请使用&T{}
代替new(T)
,以使其与结构体初始化一致。
Bad | Good |
---|---|
sval := T{
Name: "foo"}
// inconsistent
sptr := new(T)
sptr.Name = "bar"
sval := T{
Name: "foo"}
sptr := &T{
Name: "bar"}
初始化 Maps
对于空 map 请使用 make(..)
初始化, 并且 map 是通过编程方式填充的。
这使得 map 初始化在表现上不同于声明,并且它还可以方便地在 make 后添加大小提示。
Bad | Good |
---|---|
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = map[T1]T2{
}
m2 map[T1]T2
)
var (
// m1 读写安全;
// m2 在写入时会 panic
m1 = make(map[T1]T2)
m2 map[T1]T2
)
声明和初始化看起来非常相似的。
声明和初始化看起来差别非常大。
在尽可能的情况下,请在初始化时提供 map 容量大小,详细请看 指定Map容量提示。
另外,如果 map 包含固定的元素列表,则使用 map literals(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,
}
基本准则是:在初始化时使用 map 初始化列表 来添加一组固定的元素。否则使用 make
(如果可以,请尽量指定 map 容量)。
字符串 string format
如果你在函数外声明Printf
-style 函数的格式字符串,请将其设置为const
常量。
这有助于go vet
对格式字符串执行静态分析。
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)
命名 Printf 样式的函数
声明Printf
-style 函数时,请确保go vet
可以检测到它并检查格式字符串。
这意味着您应尽可能使用预定义的Printf
-style 函数名称。go vet
将默认检查这些。有关更多信息,请参见 Printf 系列。
如果不能使用预定义的名称,请以 f 结束选择的名称:Wrapf
,而不是Wrap
。go vet
可以要求检查特定的 Printf 样式名称,但名称必须以f
结尾。
$ go vet -printfuncs=wrapf,statusf
另请参阅 go vet: Printf family check.
编程模式
表驱动测试
当测试逻辑是重复的时候,通过 subtests 使用 table 驱动的方式编写 case 代码看上去会更简洁。
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 table 的方式在代码逻辑扩展的时候,比如新增 test case,都会显得更加的清晰。
We follow the convention of calling struct slicestests
. Each test case is called tt
. Additionally, we encourage the use of the give
and want
prefixes to specify the input and output values of each test case.
tests := []struct{
give string
wantHost string
wantPort string
}{
// ...
}
for _, tt := range tests {
// ...
}
Feature options
Functional options are a pattern in which you declare an opaque Option type that records information in some internal structure. You accept the variable numbering of these options and act based on all the information recorded by the options on the internal structure.
Use this pattern for optional parameters in constructors and other public APIs that you need to extend, especially if those functions already have three or more parameters.
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) {
// ...
}
Cache and logger parameters must always be provided, even if the user wishes to use default values.
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)
Provide options only when 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.
We recommend that the way to implement this pattern is to use a Option
interface that holds an unexported method in an unexported options
structure Record options.
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: There is also a way to implement this pattern using closures, but we believe the above pattern provides more flexibility for authors and is easier for users to debug and test. In particular, it allows options to be compared in tests and simulations where comparison is not possible. Additionally, it allows options to implement other interfaces, including fmt.Stringer
, which allows the user to read the string representation of an option.
You can also refer to the following information:
Linting
More importantly than any "blessed" set of linters, linters are always consistent across a codebase.
We recommend using at least the following linters, as I think they help uncover the most common problems and establish a high standard for code quality without the need for prescriptions:
-
errcheck to ensure errors are handled
-
goimports Format code and manage imports
-
golint Point out common stylistic errors
-
govet Analyze common errors in code
-
staticcheck Various static analysis checks
Lint Runners
We recommendgolangci-lint as the go-to lint runner, mainly due to its ease of use in larger code bases performance and the ability to configure and use many specifications simultaneously. This repo has a sample configuration file.golangci.yml and recommended linter settings.
golangci-lint hasvarious-linters available. It is recommended that the above linters are used as the base set, and teams are encouraged to add any additional linters that make sense for their projects.