Go language programming specification recommendations


The first part introduces the coding specifications of some common problems in the development process, and the second part mainly introduces the better writing methods of some common problems, as shown in the figure below.

1. Guiding principles

1.1 pointer to interface

There is little need for pointers to interface types. The interface should be passed as a value, in which the underlying data passed can still be pointers.

The interface is essentially represented by two fields:

  • A pointer to some specific type of information, which can be regarded as "type"
  • Data pointer. If the stored data is a pointer, it is stored directly. If the stored data is a value, a pointer to the value is stored.

If you want an interface method to modify basic 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) the underlying data cannot be modified
// f2.f() can modify the underlying data, and the object pointer is used when assigning value to the interface variable F2
var f1 F = S1{}
var f2 F = &S2{}

1.2 interface rationality verification

Verify the compliance of the interface at compile time. This includes:

  • The export type that implements a specific interface is checked as part of the interface API
  • Types that implement the same interface (export and non export) belong to a collection of implementation types
  • Any scenario that violates the interface rationality check will terminate the compilation and notify the user

Note: the above three items are too difficult to understand. In short, using the interface incorrectly will report errors during compilation. Therefore, this mechanism can be used to expose some problems in the compiler.

Bad:

// If the handler does not implement http Handler, an error will be reported when running
type Handler struct {
  // ...
}
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  ...
}

Good:

type Handler struct {
  // ...
}
// Rationality checking mechanism for triggering compile time interfaces
// If the handler does not implement http Handler, an error will be reported during compilation
var _ http.Handler = (*Handler)(nil)
func (h *Handler) ServeHTTP(
  w http.ResponseWriter,
  r *http.Request,
) {
  // ...
}

Note: if * handler and HTTP If the interface of the handler does not match, the statement var_ http.Handler = (* handler) (NIL) will not compile.

The right side of the assignment should be the zero value of the assertion type. For pointer types (such as * Handler), slice and map, this is nil; For structure types, this is an 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,
) {
  // ...
}

1.3 receiver and interface

Methods that use a value sink can be called either by value or by pointer. Methods with pointer receivers can only pass through pointers or addressable values Call.

example 🌰

type S struct {
  data string
}

func (s S) Read() string {
  return s.data
}

func (s *S) Write(str string) {
  s.data = str
}

sVals := map[int]S{1: {"A"}}

// You can only call Read by value
sVals[1].Read()

// This does not compile through:
//  sVals[1].Write("test")

sPtrs := map[int]*S{1: {"A"}}

// Through the pointer, you can call either the Read method or the Write method
sPtrs[1].Read()
sPtrs[1].Write("test")

Similarly, even if the method has a value receiver, the pointer receiver can also be used to meet 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

//  The following code cannot be compiled. Because s2Val is a value, the value receiver is not used in the f method of S2
//   i = s2Val

Note: Effective Go There is a paragraph about pointers vs. values Wonderful explanation.

1.4 zero Mutex is valid

  • Zero value sync Mutex and sync Rwmutex is valid. Therefore, the pointer to mutex is basically unnecessary.

    Bad:

    mu := new(sync.Mutex)
    mu.Lock()
    

    Good:

    var mu sync.Mutex
    mu.Lock()
    
  • Using a structure pointer, mutex should be a non pointer field of the structure. Even if the structure is not exported, do not directly embed mutex into the structure.

    Bad: Mutex field, Lock and Unlock methods are not intentionally described in the API exported by SMap.

    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]
    }
    

    Good: mutex and its methods are the implementation details of SMap and are not visible to its callers.

    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]
    }
    

    Note: the author expresses doubts about this article.

1.5 copy Slices and Maps at the boundary

slices and maps contain pointers to the underlying data, so be careful when you need to copy them.

  • Slices and Maps are used as input parameters

    Note that when a map or slice is passed in as a function parameter, if references to them are stored inside the function, you can modify them.

    Bad:

    func (d *Driver) SetTrips(trips []Trip) {
      d.trips = trips
    }
    
    trips := ...
    d1.SetTrips(trips)
    
    // You want to modify D1 Trips?
    trips[0] = ...
    

    Good:

    func (d *Driver) SetTrips(trips []Trip) {
      d.trips = make([]Trip, len(trips))
      copy(d.trips, trips)
    }
    
    trips := ...
    d1.SetTrips(trips)
    
    // Here we modify trips[0], but it will not affect D1 trips
    trips[0] = ...
    
  • Returns slices or maps

    Note: pay attention to the modification of the map or slice that exposes the internal state of the user.

    Bad:

    type Stats struct {
      mu sync.Mutex
    
      counters map[string]int
    }
    
    // The Snapshot returns the current status.
    func (s *Stats) Snapshot() map[string]int {
      s.mu.Lock()
      defer s.mu.Unlock()
    
      return s.counters
    }
    
    // Snapshots are no longer protected by mutexes
    // Therefore, any access to the snapshot will be affected by data competition
    // Impact stats counters
    snapshot := stats.Snapshot()
    

    Good:

    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
    }
    
    // The snapshot is now a copy
    snapshot := stats.Snapshot()
    

​

1.6 using defer to release resources

Use defer to release resources, such as files or locks.

Bad:

p.Lock()
if p.count < 10 {
  p.Unlock()
  return p.count
}

p.count++
newCount := p.count
p.Unlock()

return newCount

// When there are multiple return branches, it is easy to forget unlock

Good:

p.Lock()
defer p.Unlock()

if p.count < 10 {
  return p.count
}

p.count++
return p.count

// More readable

Note: using defer to improve readability is worthwhile because the cost of using them is acceptable compared to the readability of the code. It is especially suitable for larger methods that are not only simple memory access, in which the resource consumption of other calculations is much higher than that of defer.

1.7 the channel size is either 1 or unbuffered

The usual size of the channel should be 1 or unbuffered. By default, channel is unbuffered and its size is zero.

Bad:

// Should be enough to meet any situation!
c := make(chan int, 64)

Good:

// Size: 1
c := make(chan int, 1) // perhaps
// Unbuffered channel, size 0
c := make(chan int)

Note: it is recommended to carefully select the size of the channel because it may affect the state conditions and the logic of the context.

1.8 enumeration starts with 1

The standard way to introduce enumeration in Go is to declare a custom type and a const group using iota. Since the default value of a variable is 0, enumeration should usually start with a non-zero value.

Bad:

type Operation int

const (
  Add Operation = iota
  Subtract
  Multiply
)

// Add=0, Subtract=1, Multiply=2

Good:

type Operation int

const (
  Add Operation = iota + 1
  Subtract
  Multiply
)

// Add=1, Subtract=2, Multiply=3

1.9 processing time using time

Time processing is complicated. Wrong assumptions about time usually include the following:

  • There are 24 hours a day
  • There are 60 minutes in an hour
  • There are seven days a week
  • There are 365 days in a year
  • More

1.9.1 use time Time denotes instantaneous time

Use time when processing the moment of time Time, use time when comparing, adding, or subtracting time Method in time.

Bad:

func isActive(now, start, stop int) bool {
  return start <= now && now < stop
}

Good:

func isActive(now, start, stop time.Time) bool {
  return (start.Before(now) || start.Equal(now)) && now.Before(stop)
}

Note: the method used to add time depends on the intention. If you want the same time on the next calendar day (the next day of the current day), you should use time Adddate. However, if you want to ensure that a certain time is 24 hours later than the previous time, you should use time Add.

newDay := t.AddDate(0 /* years */, 0 /* months */, 1 /* days */)
maybeNewDay := t.Add(24 * time.Hour)

1.9.2 use time Duration expression period

Use time. When processing time periods Duration.

Bad:

func poll(delay int) {
  for {
    // ...
    time.Sleep(time.Duration(delay) * time.Millisecond)
  }
}
poll(10) // Seconds or milliseconds?

Good:

func poll(delay time.Duration) {
  for {
    // ...
    time.Sleep(delay)
  }
}
poll(10*time.Second)

Use time. For external systems Time and time Duration

Whenever possible, use time in interactions with external systems Duration and time Time. For example:

When you can no longer use time in these interactions For duration, use int or float64 and include units in the field name.

Bad:

// {"interval": 2}
type Config struct {
  Interval int `json:"interval"`
}

Good:

// {"intervalMillis": 2000}
type Config struct {
  IntervalMillis int `json:"intervalMillis"`
}

Note 1: because encoding/json does not support time Duration, so the unit is included in the name of the field.

When time cannot be used in interaction Time, use string and unless agreed RFC 3339 Format timestamp defined in. The time package does not support resolving leap second timestamps, nor does it consider leap seconds in calculations.

1.10 Errors

1.10.1 error type

There are few instances of declaration errors. When using, please consider the following situations:

  • Does the caller need to match the error so that they can handle it?

    If so, errors must be supported by declaring a top-level error or custom type Is or errors As function.

  • Is the error message a static string or a dynamic string that requires context information?

    If it is a static string, you can use errors New, but for dynamic strings, you must use FMT Errorfor custom error type.

  • Are you passing new errors returned by downstream functions?

    If yes, refer to the error packaging section later.

    Wrong match?Error messageguidance
    Nostaticerrors.New
    Nodynamicfmt.Errorf
    Yesstatictop-level var with errors.New
    Yesdynamiccustom error type

errors.New represents an example of an error with a static string

No error matching:

// package foo

func Open() error {
  return errors.New("could not open")
}

// package bar

if err := foo.Open(); err != nil {
  // Can't handle the error.
  panic("unknown error")
}

Wrong match:

// package foo

var ErrCouldNotOpen = errors.New("could not open")

func Open() error {
  return ErrCouldNotOpen
}

// package bar

if err := foo.Open(); err != nil {
  if errors.Is(err, foo.ErrCouldNotOpen) {
    // handle the error
  } else {
    panic("unknown error")
  }
}

fmt. Errorfexamples of errors in dynamic strings 🌰

No error matching:

// package foo

func Open(file string) error {
  return fmt.Errorf("file %q not found", file)
}

// package bar

if err := foo.Open("testfile.txt"); err != nil {
  // Can't handle the error.
  panic("unknown error")
}

Wrong match:

// package foo

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

func Open(file string) error {
  return &NotFoundError{File: file}
}


// package bar

if err := foo.Open("testfile.txt"); err != nil {
  var notFound *NotFoundError
  if errors.As(err, &notFound) {
    // handle the error
  } else {
    panic("unknown error")
  }
}

Note: if you export error variables or types from the package, they will become part of the public API of the package

1.10.2 wrong packaging

If the call fails, there are several processing methods:

  • Returns the original error as is
  • Use FMT Errorfand% w
  • Use FMT Errorfand% v

If there is no other context to add, the original error is returned as is. Otherwise, add context to the error message as much as possible so that fuzzy errors such as "connection rejected" do not occur.

Use FMT Errorfadd up-down questions of errors and choose between% w or% v verbs according to the needs of the caller.

  • Use% w if the caller should have access to the underlying error. This is a good default for most packaging errors.
  • Use% v to confuse the underlying error. The caller cannot match it, but it can be changed to% w in the future if necessary.

Note: when adding context to the returned error, avoid using phrases such as "fail to" to keep the context concise. When the error penetrates upward through the stack, it will be stacked layer by layer:

Bad:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "failed to create new store: %w", err)
}
failed to x: failed to y: failed to create new store: the error

Good:

s, err := store.New()
if err != nil {
    return fmt.Errorf(
        "new store: %w", err)
}
x: y: new store: the error

Note: once an error is sent to another system, it should be clear that the message is an error (for example, err tag or "Failed" prefix in the log)

In addition Don't just check for errors, handle them gracefully.

1.10.3 wrong naming

For error values stored as global variables, the prefix err or err is used depending on whether they are exported

var (
  // Export the following two errors so that users of this package can associate them with errors Match is.

  ErrBrokenLink = errors.New("link is broken")
  ErrCouldNotOpen = errors.New("could not open")

  // This error was not exported because we don't want it to be part of our public API. We may still use it in packages with errors.

  errNotFound = errors.New("not found")
)

For custom Error types, use the suffix Error instead.

// Again, the error is exported so that users of the package can associate it with errors As match.

type NotFoundError struct {
  File string
}

func (e *NotFoundError) Error() string {
  return fmt.Sprintf("file %q not found", e.File)
}

// And this error was not exported because we don't want it to be part of the public API. We can still do it with errors Use it in as's package.
type resolveError struct {
  Path string
}

func (e *resolveError) Error() string {
  return fmt.Sprintf("resolve %q", e.Path)
}

1.11 processing assertion failed

Type Asserts panic will be returned as a single return value when an incorrect type is detected. Therefore, always use the "x, ok" method.

Bad:

t := i.(string)

Good:

t, ok := i.(string)
if !ok {
  // Handle errors gracefully
}

1.12 do not use panic

Code running in a build environment must avoid painc. panic is Cascade failure The main source of. If an error occurs, the function must return an error and allow the calling method to decide how to handle it.

Bad:

func run(args []string) {
  if len(args) == 0 {
    panic("an argument is required")
  }
  // ...
}

func main() {
  run(os.Args[1:])
}

Good:

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)
  }
}

Note: panic/recover is not an error handling strategy. The program must panic only if something unrecoverable (such as a nil reference) occurs. Program initialization is an exception: the bad condition that the program should be stopped when the program starts may cause panic.

Even in test code, t.Fatal or t.FailNow is preferred over panic to ensure that failures are marked.

Bad:

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  panic("failed to set up test")
}

Good:

// func TestFoo(t *testing.T)

f, err := ioutil.TempFile("", "test")
if err != nil {
  t.Fatal("failed to set up test")
}

1.13 avoiding variable global variables

Use selective dependency injection to avoid changing global variables. This applies to both function pointers and other value types.

Bad:

// sign.go
var _timeNow = time.Now
func sign(msg string) string {
  now := _timeNow()
  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))
}

Good:

// 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 TestSigner(t *testing.T) {
  s := newSigner()
  s.now = func() time.Time {
    return someFixedTime
  }
  assert.Equal(t, want, s.Sign(give))
}

1.14 avoid embedding types in public structures

Assuming that a shared AbstractList is created to implement multiple list types, please avoid embedding AbstractList in a specific list implementation. Instead, you just need to manually write the method to the concrete list, which will be delegated to the abstract list.

type AbstractList struct {}
// Add adds an entity to the list.
func (l *AbstractList) Add(e Entity) {
  // ...
}
// Remove removes an entity from the list.
func (l *AbstractList) Remove(e Entity) {
  // ...
}

Bad:

// ConcreteList is a list of entities.
type ConcreteList struct {
  *AbstractList
}

Good:

// ConcreteList is a list of entities.
type ConcreteList struct {
  list *AbstractList
}
// Add adds an entity to the list.
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}
// Remove removes an entity from the list.
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

Go allow Type embedding As a compromise between inheritance and composition.

Note: the embedded structure obtains a field with the same name as the type. Therefore, if the embedded type is public, the field is public. To maintain backward compatibility, each future version of an external type must retain the embedded type.

Embedded types are rarely required. This is a convenience that can help avoid lengthy writing, but it still risks leaking specific implementation details.

Bad:

// AbstractList is a common implementation of various entity lists.
type AbstractList interface {
  Add(Entity)
  Remove(Entity)
}
// ConcreteList is a list of entities.
type ConcreteList struct {
  AbstractList
}

Good:

// ConcreteList is a list of entities.
type ConcreteList struct {
  list AbstractList
}
// Add adds an entity to the list.
func (l *ConcreteList) Add(e Entity) {
  l.list.Add(e)
}
// Remove removes an entity from the list.
func (l *ConcreteList) Remove(e Entity) {
  l.list.Remove(e)
}

Both embedded structures and embedded interfaces will limit the evolution of types.

  • Adding methods to embedded interfaces is a disruptive change
  • The method of deleting from embedded structures is a destructive change.
  • Deleting embedded types is a disruptive change.
  • Even if you replace an embedded type with a type that satisfies the same interface, this operation is a destructive change.

Note: Although writing these delegate methods is tedious, it hides the implementation details, leaves more opportunities for change, and eliminates the indirect operation of discovering the complete list interface in the document.

1.15 avoid using built-in names

Go language norm Outlines several built-in that should not be used in Go projects Pre declared identifier.

Depending on the context, reusing these identifiers as names will hide the original identifiers in the current scope (or any nested scope), or confuse the code. In the best case, the compiler will report an error; In the worst case, such code may introduce potential and difficult to recover errors.

Bad:

var error string
// `error ` scope implicit override

// or

func handleErrorMessage(error string) {
    // `error ` scope implicit override
}
type Foo struct {
    // Although these fields do not form a shadow technically, the remapping of 'error' or 'string' strings is not clear now.
    error  error
    string string
}

func (f Foo) Error() error {
    // `Error 'and' f.error 'are visually similar
    return f.error
}

func (f Foo) String() string {
    // `string` and `f.string ` are visually similar
    return f.string
}

Good:

var errorMessage string
// `error ` points to the built-in non overlay

// or

func handleErrorMessage(msg string) {
    // `error ` points to the built-in non overlay
}
type Foo struct {
    // `error` and `string ` are now explicit.
    err error
    str string
}

func (f Foo) Error() error {
    return f.err
}

func (f Foo) String() string {
    return f.str
}

Note: the compiler does not generate errors when using pre segmented identifiers, but tools such as go vet correctly address these and other implicit problems.

1.16 avoid using init()

Avoid using init() whenever possible. Of course, init () is inevitable or desirable, and the code should ensure that:

  • Regardless of the program environment or call, it should be completely determined.
  • Avoid ordering or side effects that depend on other init() functions. Although the init () order is explicit, the code can be changed, so the relationship between init () functions may make the code fragile and error prone.
  • Avoid accessing or manipulating global or environmental states, 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 written as part of the main() call (or elsewhere in the program life cycle), or as part of the main() itself. In particular, libraries intended for use by other programs should pay particular attention to complete certainty rather than executing init magic

Bad:

type Foo struct {
    // ...
}
var _defaultFoo Foo
func init() {
    _defaultFoo = Foo{
        // ...
    }
}
type Config struct {
    // ...
}
var _config Config
func init() {
    // Bad: Based on current directory
    cwd, _ := os.Getwd()
    // Bad: I/O
    raw, _ := ioutil.ReadFile(
        path.Join(cwd, "config", "config.yaml"),
    )
    yaml.Unmarshal(raw, &_config)
}

Good:

var _defaultFoo = Foo{
    // ...
}
// or, for better testability:
var _defaultFoo = defaultFoo()
func defaultFoo() Foo {
    return Foo{
        // ...
    }
}
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
}

Considering the above, in some cases, init() may be preferable or necessary, which may include:

  • A complex expression that cannot be represented as a single assignment
  • Pluggable hooks, such as database/sql, encoding type registry, etc.
  • yes Google Cloud Functions And other forms of deterministic precomputation optimization.

1.17 when adding, the slice capacity shall be specified preferentially

Append preferentially specifies the slice capacity. Whenever possible, provide a capacity value for make() when initializing the slice to append.

Bad:

for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
BenchmarkBad-4    100000000    2.48s

Good:

for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
BenchmarkGood-4   100000000    0.21s

1.18 main function Exit mode (Exit)

The Go program uses OS Exit or log Fatal * quit now. Only one of the aforementioned ones is invoked in main(). All other functions shall return the error to the signal failure.

Bad:

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)
}

Good:

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, the exit with multiple exits will have the following problems:

  • Unobtrusive process flow: any function can exit the program, so it is difficult to reason about the control flow.
  • Difficult to test: the function that exits the program will also exit the test that calls it. This makes the function difficult to test and introduces the risk of skipping other tests that go test has not yet run.
  • Skip Cleanup: when the function exits the program, it will skip the function calls that have entered the defer queue. This may result in important cleanup tasks not being performed

One time exit

If possible, OS. Is called at most once in the main() function Exit or log Fatal. If there are multiple error scenarios to stop program execution, put the logic under a separate function and return an error from it.

Note: This shortens the main() function and puts all the key business logic into a single, testable function.

Bad:

package main
func main() {
  args := os.Args[1:]
  if len(args) != 1 {
    log.Fatal("missing file")
  }
  name := args[0]
  f, err := os.Open(name)
  if err != nil {
    log.Fatal(err)
  }
  defer f.Close()
  // If we call log Fatal is behind this line
  // f.Close will be executed
  b, err := ioutil.ReadAll(f)
  if err != nil {
    log.Fatal(err)
  }
  // ...
}

Good:

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
  }
  // ...
}

2. Performance

2.1 strconv is preferred over fmt

strconv is faster than fmt when converting primitives to or from strings.

Bad:

for i := 0; i < b.N; i++ {
  s := fmt.Sprint(rand.Int())
}
BenchmarkFmtSprint-4    143 ns/op    2 allocs/op

Good:

for i := 0; i < b.N; i++ {
  s := strconv.Itoa(rand.Int())
}
BenchmarkStrconv-4    64.2 ns/op    1 allocs/op

2.2 avoid string to byte conversion

Do not repeatedly create byte slice from fixed string. Instead, perform a transformation and capture the results.

Bad:

for i := 0; i < b.N; i++ {
  w.Write([]byte("Hello world"))
}
BenchmarkBad-4   50000000   22.2 ns/op

Good:

data := []byte("Hello world")
for i := 0; i < b.N; i++ {
  w.Write(data)
}
BenchmarkGood-4  500000000   3.25 ns/op

2.3 specified container capacity

Specify the container capacity as much as possible to pre allocate memory for the container. This minimizes subsequent allocations (by copying and resizing containers) when adding elements.

2.3.1 indication of Map capacity

If possible, provide capacity information when initializing with make(). Providing a capacity hint to make () will attempt to resize the map at initialization, which will reduce the reallocation of memory for the map when elements are added to the map.

make(map[T1]T2, hint)

Note: 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, an allocation can occur when an element is added to a map, even when the map capacity is specified.

**Bad: ** m is created without size prompt. There may be more allocations at run time.

m := make(map[string]os.FileInfo)

files, _ := ioutil.ReadDir("./files")
for _, f := range files {
    m[f.Name()] = f
}

Good: m is created with size prompt, and there will be less allocation at run time.

files, _ := ioutil.ReadDir("./files")

m := make(map[string]os.FileInfo, len(files))
for _, f := range files {
    m[f.Name()] = f
}

2.3.2 specify slice capacity

Whenever possible, provide capacity information when initializing slices using make(), especially when adding slices.

make([]T, length, capacity)

Note: slice capacity is not a hint: the compiler will allocate enough memory for the slice capacity provided to make(), which means that the subsequent append() operation will result in zero allocation.

After the length of the slice matches the capacity, any append will resize the slice to accommodate other elements.

Bad:

for n := 0; n < b.N; n++ {
  data := make([]int, 0)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
BenchmarkBad-4    100000000    2.48s

Good:

for n := 0; n < b.N; n++ {
  data := make([]int, 0, size)
  for k := 0; k < size; k++{
    data = append(data, k)
  }
}
BenchmarkGood-4   100000000    0.21s

3. Programming mode

3.1 table drive test

When the test logic is repeated, pass subtests Writing case code in a table driven way looks much simpler.

Bad:

// 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)

Good:

// 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)
  })
}

Obviously, the method of using test table will be clearer when the code logic is extended, such as adding a test case.

We follow this Convention: the structure slice is called tests. Each test case is called tt. In addition, it is recommended to use the prefix give and want to describe the input and output values of each test case.

tests := []struct{
  give     string
  wantHost string
  wantPort string
}{
  // ...
}

for _, tt := range tests {
  // ...
}

3.2 function options

A function Option is a pattern in which you can declare an opaque Option type that records information in some internal structures. Accept the numbers of these options and take action based on all the information recorded by the options on the internal structure.

Use this pattern to extend optional parameters in constructors and other public API s, especially if you already have three or more parameters on these functions.

Bad:

// package db

func Open(
  addr string,
  cache bool,
  logger *zap.Logger
) (*Connection, error) {
  // ...
}

Cache and logger parameters must always be provided, even if the user wants 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)

Good:

// 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) {
  // ...
}

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),
)

The recommended way to implement this pattern is to use an Option interface, which saves an unexported method and records the options on an unexported options structure.

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: the above pattern provides developers with more flexibility and easier debugging and testing. In particular, it allows options to be compared in tests and simulations where comparison is not possible. In addition, it allows options to implement other interfaces, including FMT Stringer, which allows the user to read the string representation of the option.

There is another way to implement this pattern using closures.

You can also refer to the following information:

4. Broken thoughts

Another beautiful day:

  • Everything we do in life is to avoid pain and obtain happiness.
  • It's amazing to live your life every day, isn't it.
  • Nothing in the world is meaningful, meaning is given by people, and persistence itself is an incomparably shining meaning.

5. References

Keywords: Go Back-end

Added by bben95 on Wed, 12 Jan 2022 13:18:52 +0200