Included in On Technology , author "Lao Miao"
These are the 12 items that have been directly summarized. Continue to look at them in detail:
- Handle errors first to avoid nesting
- Try to avoid repetition
- Write the most important code first
- Write documentation comments to the code
- Naming should be as concise as possible
- Using multiple packages
- Use go get to get your package
- Understand your needs
- Maintain package independence
- Avoid using concurrency internally
- Use Goroutine to manage status
- 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