Google: 12 Golang best practices

Keywords: Go

Included in On Technology , author "Lao Miao"

These are the 12 items that have been directly summarized. Continue to look at them in detail:

  1. Handle errors first to avoid nesting
  2. Try to avoid repetition
  3. Write the most important code first
  4. Write documentation comments to the code
  5. Naming should be as concise as possible
  6. Using multiple packages
  7. Use go get to get your package
  8. Understand your needs
  9. Maintain package independence
  10. Avoid using concurrency internally
  11. Use Goroutine to manage status
  12. Avoid Goroutine leakage

Best practices

This is a translated article. In order to make readers better understand it, some explanations or descriptions will be added on the basis of the original translation.

On Wikipedia:

"A best practice is a method or technique that has consistently shown results superior
to those achieved with other means"

A best practice is a method or technique whose results are always better than others.

Technical requirements for writing Go Code:

  • Simplicity
  • Readability
  • Maintainability

Sample code

Code that needs to be optimized.

type Gopher struct {
    Name     string
    AgeYears int
}

func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
    err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err == nil {
        size += 4
        var n int
        n, err = w.Write([]byte(g.Name))
        size += int64(n)
        if err == nil {
            err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
            if err == nil {
                size += 4
            }
            return
        }
        return
    }
    return
}

Look at the above code and think about how to write the code better. Let me briefly say what the code means:

  • Store the Name and AgeYears field data into the io.Writer type.
  • If the stored data is of string or [] byte type, its length data is appended.

If you don't know how to use the binary standard package, take a look at my other article Quick understanding of "small end" and "big end" and their use in Go language.

Handle errors first to avoid nesting

func (g *Gopher) WriteTo(w io.Writer) (size int64, err error) {
    err = binary.Write(w, binary.LittleEndian, int32(len(g.Name)))
    if err != nil {
        return
    }
    size += 4
    n, err := w.Write([]byte(g.Name))
    size += int64(n)
    if err != nil {
        return
    }
    err = binary.Write(w, binary.LittleEndian, int64(g.AgeYears))
    if err == nil {
        size += 4
    }
    return
}

Reducing the nesting of judgment errors will make readers look easier.

Try to avoid repetition

The Write in the WriteTo method in the above code occurs three times, which is repeated. After simplification, it is as follows:

type binWriter struct {
    w    io.Writer
    size int64
    err  error
}

// Write writes a value to the provided writer in little endian form.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
        w.size += int64(binary.Size(v))
    }
}

Use the binWriter structure.

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(int32(len(g.Name)))
    bw.Write([]byte(g.Name))
    bw.Write(int64(g.AgeYears))
    return bw.size, bw.err
}

Type switch handles different types

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch v.(type) {
    case string:
        s := v.(string)
        w.Write(int32(len(s)))
        w.Write([]byte(s))
    case int:
        i := v.(int)
        w.Write(int64(i))
    default:
        if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
            w.size += int64(binary.Size(v))
        }
    }
}

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.AgeYears)
    return bw.size, bw.err
}

Type switch reduction

The use of v.(string) and v.(int) type reflection in the above code is abandoned.

func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch x := v.(type) {
    case string:
        w.Write(int32(len(x)))
        w.Write([]byte(x))
    case int:
        w.Write(int64(x))
    default:
        if w.err = binary.Write(w.w, binary.LittleEndian, v); w.err == nil {
            w.size += int64(binary.Size(v))
        }
    }
}

When entering different branches, the x variable corresponds to the type of the branch.

Write at your discretion

type binWriter struct {
    w   io.Writer
    buf bytes.Buffer
    err error
}

// Write writes a value to the provided writer in little endian form.
func (w *binWriter) Write(v interface{}) {
    if w.err != nil {
        return
    }
    switch x := v.(type) {
    case string:
        w.Write(int32(len(x)))
        w.Write([]byte(x))
    case int:
        w.Write(int64(x))
    default:
        w.err = binary.Write(&w.buf, binary.LittleEndian, v)
    }
}

// Flush writes any pending values into the writer if no error has occurred.
// If an error has occurred, earlier or with a write by Flush, the error is
// returned.
func (w *binWriter) Flush() (int64, error) {
    if w.err != nil {
        return 0, w.err
    }
    return w.buf.WriteTo(w.w)
}

func (g *Gopher) WriteTo(w io.Writer) (int64, error) {
    bw := &binWriter{w: w}
    bw.Write(g.Name)
    bw.Write(g.AgeYears)
    return bw.Flush()
}

The WriteTo method is divided into two parts to increase flexibility:

  • Assembly information
  • Call the Flush method to determine whether to write w.

function adaptor

func init() {
    http.HandleFunc("/", handler)
}

func handler(w http.ResponseWriter, r *http.Request) {
    err := doThis()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("handling %q: %v", r.RequestURI, err)
        return
    }

    err = doThat()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        log.Printf("handling %q: %v", r.RequestURI, err)
        return
    }
}

The function handler contains business logic and error handling. Next, write the error handling into a separate function. The code is modified as follows:

func init() {
    http.HandleFunc("/", errorHandler(betterHandler))
}

func errorHandler(f func(http.ResponseWriter, *http.Request) error) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        err := f(w, r)
        if err != nil {
            http.Error(w, err.Error(), http.StatusInternalServerError)
            log.Printf("handling %q: %v", r.RequestURI, err)
        }
    }
}

func betterHandler(w http.ResponseWriter, r *http.Request) error {
    if err := doThis(); err != nil {
        return fmt.Errorf("doing this: %v", err)
    }

    if err := doThat(); err != nil {
        return fmt.Errorf("doing that: %v", err)
    }
    return nil
}

Organize your code

1. Write the most important first

License information, build information, package documentation.

import statement: correlation groups are separated by empty lines.

import (
    "fmt"
    "io"
    "log"

    "golang.org/x/net/websocket"
)

The rest of the code starts with the most important types and ends with auxiliary functions and types.

2. Document notes

Related documents before package name.

// Package playground registers an HTTP handler at "/compile" that
// proxies requests to the golang.org playground service.
package playground

The identifiers (variables, structures, etc.) in Go language should be correctly recorded in the articles exported by godoc.

// Author represents the person who wrote and/or is presenting the document.
type Author struct {
    Elem []Elem
}

// TextElem returns the first text elements of the author details.
// This is used to display the author' name, job title, and company
// without the contact details.
func (p *Author) TextElem() (elems []Elem) {

Extension:

Use the godoc tool to view the go project documents on the web page.

# install
go get golang.org/x/tools/cmd/godoc

# Start service
godoc -http=:6060

Direct local access localhost:6060 View the document.

3. The naming shall be as concise as possible

In other words, long naming is not necessarily good.

Try to find a short name that can be clearly expressed, such as:

  • Marshall indent is better than Marshall with indentation.

Don't forget to write the package name first when calling the package content.

  • In the encoding/json package, there is a structure Encoder, which should not be written as JSONEncoder.
  • In this way, json.Encoder is used.

4. Multiple packages

Should a package be split into multiple files?

  • Avoid code that is too long

The standard package net/http has 15734 lines of code and is split into 47 files.

  • Split code and test.

net/http/cookie.go and net / http / cookie_ The test.go files are placed under the http package.

Test code is compiled only when testing.

  • Split package document

When there are multiple files in a package, according to the Convention, create a doc.go file to write the document description of the package.

Personal thinking: when a package has a lot of descriptive information, you can consider creating a doc.go file.

5. Use go get to get your package

When your package is provided for use, you should clearly let users know what is reusable and what is not reusable.

So what happens when some packages may be reused and others may not?

For example, packets defining some network protocols may be reused, while packets defining some executable commands will not.

  • cmd is a package of executable commands and does not provide reuse
  • pkg reusable package

Personal thinking: if there are many executable entries in a project, it is recommended to place them in the cmd directory, but it is not recommended for the pkg directory at present, so there is no need to learn from them.

API

1. Understand your needs

We continue to use the previous Gopher type.

type Gopher struct {
    Name     string
    AgeYears int
}

We can define this method.

func (g *Gopher) WriteToFile(f *os.File) (int64, error) {

However, when the parameters of a method use specific types, it becomes difficult to test, so we use interfaces.

func (g *Gopher) WriteToReadWriter(rw io.ReadWriter) (int64, error) {

Moreover, when the interface is used, we should only define the methods we need.

func (g *Gopher) WriteToWriter(f io.Writer) (int64, error) {

2. Keep the package independent

import (
    "golang.org/x/talks/content/2013/bestpractices/funcdraw/drawer"
    "golang.org/x/talks/content/2013/bestpractices/funcdraw/parser"
)
// Parse the text into an executable function.
  f, err := parser.Parse(text)
  if err != nil {
      log.Fatalf("parse %q: %v", text, err)
  }

  // Create an image plotting the function.
  m := drawer.Draw(f, *width, *height, *xmin, *xmax)

  // Encode the image into the standard output.
  err = png.Encode(os.Stdout, m)
  if err != nil {
      log.Fatalf("encode image: %v", err)
  }

In the code, the Draw method accepts the f variable returned by the Parse function. Logically, the drawer package depends on the parser package. Let's see how to cancel this dependency.

parser package:

type ParsedFunc struct {
    text string
    eval func(float64) float64
}

func Parse(text string) (*ParsedFunc, error) {
    f, err := parse(text)
    if err != nil {
        return nil, err
    }
    return &ParsedFunc{text: text, eval: f}, nil
}

func (f *ParsedFunc) Eval(x float64) float64 { return f.eval(x) }
func (f *ParsedFunc) String() string         { return f.text }

drawer package:

import (
    "image"

    "golang.org/x/talks/content/2013/bestpractices/funcdraw/parser"
)

// Draw draws an image showing a rendering of the passed ParsedFunc.
func DrawParsedFunc(f parser.ParsedFunc) image.Image {

Use interface types to avoid dependencies.

import "image"

// Function represent a drawable mathematical function.
type Function interface {
    Eval(float64) float64
}

// Draw draws an image showing a rendering of the passed Function.
func Draw(f Function) image.Image {

Testing: interface types are easier to test than specific types.

package drawer

import (
    "math"
    "testing"
)

type TestFunc func(float64) float64

func (f TestFunc) Eval(x float64) float64 { return f(x) }

var (
    ident = TestFunc(func(x float64) float64 { return x })
    sin   = TestFunc(math.Sin)
)

func TestDraw_Ident(t *testing.T) {
    m := Draw(ident)
    // Verify obtained image.

4. Avoid using concurrency internally

func doConcurrently(job string, err chan error) {
    go func() {
        fmt.Println("doing job", job)
        time.Sleep(1 * time.Second)
        err <- errors.New("something went wrong!")
    }()
}

func main() {
    jobs := []string{"one", "two", "three"}

    errc := make(chan error)
    for _, job := range jobs {
        doConcurrently(job, errc)
    }
    for _ = range jobs {
        if err := <-errc; err != nil {
            fmt.Println(err)
        }
    }
}

If so, what should we do if we want to call docurrently synchronously?

func do(job string) error {
    fmt.Println("doing job", job)
    time.Sleep(1 * time.Second)
    return errors.New("something went wrong!")
}

func main() {
    jobs := []string{"one", "two", "three"}

    errc := make(chan error)
    for _, job := range jobs {
        go func(job string) {
            errc <- do(job)
        }(job)
    }
    for _ = range jobs {
        if err := <-errc; err != nil {
            fmt.Println(err)
        }
    }
}

It is easy to expose synchronous functions for concurrent calls, and it also meets synchronous calls.

Best concurrency practices

1. Use Goroutine to manage status

Goroutine s communicate with each other using a "channel" or a "structure" with channel fields.

type Server struct{ quit chan bool }

func NewServer() *Server {
    s := &Server{make(chan bool)}
    go s.run()
    return s
}

func (s *Server) run() {
    for {
        select {
        case <-s.quit:
            fmt.Println("finishing task")
            time.Sleep(time.Second)
            fmt.Println("task done")
            s.quit <- true
            return
        case <-time.After(time.Second):
            fmt.Println("running task")
        }
    }
}

func (s *Server) Stop() {
    fmt.Println("server stopping")
    s.quit <- true
    <-s.quit
    fmt.Println("server stopped")
}

func main() {
    s := NewServer()
    time.Sleep(2 * time.Second)
    s.Stop()
}

2. Use buffered channels to avoid Goroutine leakage

func sendMsg(msg, addr string) error {
    conn, err := net.Dial("tcp", addr)
    if err != nil {
        return err
    }
    defer conn.Close()
    _, err = fmt.Fprint(conn, msg)
    return err
}

func main() {
    addr := []string{"localhost:8080", "http://google.com"}
    err := broadcastMsg("hi", addr)

    time.Sleep(time.Second)

    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("everything went fine")
}

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error)
    for _, addr := range addrs {
        go func(addr string) {
            errc <- sendMsg(msg, addr)
            fmt.Println("done")
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

There is a problem with this code. If the err variable is returned in advance, the errc channel will not be read, so Goroutine will be blocked.

Summary:

  • Goroutine is blocked while writing to the channel.
  • Goroutine holds a reference to the channel.
  • Channels are not gc recycled.

Use buffer channel to solve Goroutine blocking problem.

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error, len(addrs))
    for _, addr := range addrs {
        go func(addr string) {
            errc <- sendMsg(msg, addr)
            fmt.Println("done")
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

What if we can't predict the buffer size, also known as capacity, of the channel?

Create a channel to pass the exit status to avoid Goroutine leakage.

func broadcastMsg(msg string, addrs []string) error {
    errc := make(chan error)
    quit := make(chan struct{})

    defer close(quit)

    for _, addr := range addrs {
        go func(addr string) {
            select {
            case errc <- sendMsg(msg, addr):
                fmt.Println("done")
            case <-quit:
                fmt.Println("quit")
            }
        }(addr)
    }

    for _ = range addrs {
        if err := <-errc; err != nil {
            return err
        }
    }
    return nil
}

reference resources

Original link: https://talks.golang.org/2013/bestpractices.slide#1

Video link: https://www.youtube.com/watch?v=8D3Vmm1BGoY

Posted by mikewooten on Tue, 16 Nov 2021 22:40:54 -0800