Go 编程语言的 12 条最佳实践

jopen 11年前

本文来自 Google 工程师 Francesc Campoy Flores 分享的幻灯片。内容包括:代码组织、API、并发最佳实践和一些推荐的相关资源。

最佳实践

维基百科的定义是:

“最佳实践是一种方法或技术,其结果始终优于其他方式。”

写Go代码的目标就是:

  • 简洁
  • 可读性强
  • 可维护性好

样例代码

type Gopher struct {      Name     string      Age      int32      FurColor color.Color  }    func (g *Gopher) DumpBinary(w io.Writer) error {      err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))      if err == nil {          _, err := w.Write([]byte(g.Name))          if err == nil {              err := binary.Write(w, binary.LittleEndian, g.Age)              if err == nil {                  return binary.Write(w, binary.LittleEndian, g.FurColor)              }              return err          }          return err      }      return err  }

避免嵌套的处理错误

func (g *Gopher) DumpBinary(w io.Writer) error {      err := binary.Write(w, binary.LittleEndian, int32(len(g.Name)))      if err != nil {          return err      }      _, err = w.Write([]byte(g.Name))      if err != nil {          return err      }      err = binary.Write(w, binary.LittleEndian, g.Age)      if err != nil {          return err      }      return binary.Write(w, binary.LittleEndian, g.FurColor)  }

减少嵌套意味着提高代码的可读性

尽可能避免重复

功能单一,代码更简洁

type binWriter struct {      w   io.Writer      err error  }    // Write writes a value into its writer using little endian.  func (w *binWriter) Write(v interface{}) {      if w.err != nil {          return      }      w.err = binary.Write(w.w, binary.LittleEndian, v)  }    func (g *Gopher) DumpBinary(w io.Writer) error {      bw := &binWriter{w: w}      bw.Write(int32(len(g.Name)))      bw.Write([]byte(g.Name))      bw.Write(g.Age)      bw.Write(g.FurColor)      return bw.err  }

使用类型推断来处理特殊情况

// Write writes a value into its writer using little endian.  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))      default:          w.err = binary.Write(w.w, binary.LittleEndian, v)      }  }    func (g *Gopher) DumpBinary(w io.Writer) error {      bw := &binWriter{w: w}      bw.Write(g.Name)      bw.Write(g.Age)      bw.Write(g.FurColor)      return bw.err  }

类型推断的变量声明要短

// Write write the given value into the writer using little endian.  func (w *binWriter) Write(v interface{}) {      if w.err != nil {          return      }      switch v := v.(type) {      case string:          w.Write(int32(len(v)))          w.Write([]byte(v))      default:          w.err = binary.Write(w.w, binary.LittleEndian, v)      }  }

函数适配器

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

如何组织代码

 

将重要的代码放前面

版权信息,构建信息,包说明文档

Import 声明,相关的包连起来构成组,组与组之间用空行隔开.。

import (      "fmt"      "io"      "log"        "code.google.com/p/go.net/websocket"  )

接下来代码以最重要的类型开始,以工具函数和类型结束。

 

如何编写文档

包名之前要写相关文档

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

导出的标识符(译者按:大写的标识符为导出标识符)会出现在 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) {

生成的文档示例

Gocode: 文档化Go代码

 

越简洁越好

或者 长代码往往不是最好的.

试着使用能自解释的最短的变量名.

  • MarshalIndent ,别用 MarshalWithIndentation.

别忘了包名会出现在你选择的标识符前面

  • In package encoding/json we find the type Encoder, not JSONEncoder.
  • It is referred as json.Encoder.

 

有多个文件的包

需要将一个包分散到多个文件中吗?

  • 避免行数非常多的文件

标准库中 net/http 包有47个文件,共计 15734 行.

  • 拆分代码并测试

net/http/cookie.gonet/http/cookie_test.go 都是 http 包的一部分.

测试代码 只有 在测试时才会编译.

  • 多文件包的文档编写

如果一个包中有多个文件, 可以很方便的创建一个 doc.go 文件,包含包文档信息.

让包可以”go get”到

一些包将来可能会被复用,另外一些不会.

定义了一些网络协议的包可能会在开发一个可执行命令时复用.

github.com/bradfitz/camlistore

 

接口

你需要什么

让我们以之前的Gopher类型为例

type Gopher struct {      Name     string      Age      int32      FurColor color.Color  }

我们可以定义这个方法

func (g *Gopher) DumpToFile(f *os.File) error {

但是使用一个具体的类型会让代码难以测试,因此我们使用接口.

func (g *Gopher) DumpToReadWriter(rw io.ReadWriter) error {

进而,由于使用的是接口,我们可以只请求我们需要的.

func (g *Gopher) DumpToWriter(f io.Writer) error {

让独立的包彼此独立

import (      "code.google.com/p/go.talks/2013/bestpractices/funcdraw/drawer"      "code.google.com/p/go.talks/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)      }

解析

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 }

描绘

import (      "image"        "code.google.com/p/go.talks/2013/bestpractices/funcdraw/parser"  )    // Draw draws an image showing a rendering of the passed ParsedFunc.  func DrawParsedFunc(f parser.ParsedFunc) image.Image {

使用接口来避免依赖.

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 {

测试

使用接口而不是具体类型让测试更简洁.

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.

在接口中避免并发

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

如果我们想串行的使用它会怎样?

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

暴露同步的接口,这样异步调用这些接口会简单.

 

并发的最佳实践

 

使用goroutines管理状态

使用chan或者有chan的结构体和goroutine通信

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

使用带缓存的chan,来避免goroutine内存泄漏

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  }
  • goroutine阻塞在chan写操作
  • goroutine保存了一个chan的引用
  • chan永远不会垃圾回收
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  }

如果我们不能预测channel的容量呢?

使用quit chan避免goroutine内存泄漏

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  }

12条最佳实践

1. 避免嵌套的处理错误
2. 尽可能避免重复
3. 将重要的代码放前面
4. 为代码编写文档
5. 越简洁越好
6. 讲包拆分到多个文件中
7. 让包”go get”到
8. 按需请求
9. 让独立的包彼此独立
10. 在接口中避免并发
11. 使用goroutine管理状态
12. 避免goroutine内存泄漏

一些链接

资源

其他演讲

  • 用go做词法扫描 video
  • 并发不是并行 video
  • Go并发模式 video
  • Go高级并发模式 video

谢谢

原文链接: Francesc Campoy Flores   翻译: 伯乐在线 - Codefor
译文链接: http://blog.jobbole.com/44608/