Uber Go language coding specifications

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

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:

  1. Effective Go
  2. Go Common Mistakes
  3. Go Code Review Comments

All code should pass the golint and go vet checks without errors. We recommend setting your editor to:

  • Run on savegoimports
  • running golint sum go 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:

  1. A pointer to some specific type of information. You can think of it as a "type".
  2. 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]
}

MutexThe 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.

  1. There are 24 hours in a day
  2. There are 60 minutes in an hour
  3. There are seven days in a week
  4. 365 days a year
  5. 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:

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.Formattime.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:

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.Booltype.

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:

  1. Be completely certain regardless of the program environment or calls.
  2. Avoid relying on the order or side effects of otherinit() 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.
  3. Avoid accessing or manipulating global or environmental state, such as machine information, environment variables, working directories, program parameters/inputs, etc.
  4. AvoidI/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 asdatabase/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.Exitlog.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.Exitlog.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
}

mis created without size hints; more allocations may occur at runtime.

mAre 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, not net/urls.
  • Do not use "common", "util", "shared" or "lib". These are bad, uninformative names.

Package NamesGo 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或类型语义。
  • 嵌入内部类型的非规范形式。
  • 公开外部类型的实现详细信息。
  • 允许用户观察或控制类型内部。
  • 通过包装的方式改变内部函数的一般行为,这种包装方式会给用户带来一些意料之外情况。

简单地说,有意识地和有目的地嵌入。一种很好的测试体验是,
“是否所有这些导出的内部方法/字段都将直接添加到外部类型”
如果答案是someno,不要嵌入内部类型-而是使用字段。

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,而不是Wrapgo 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.

Stargazers over time

The external link image transfer failed. The source site may have an anti-leeching mechanism. It is recommended to save the image and upload it directly.

Guess you like

Origin blog.csdn.net/cljdsc/article/details/134789906