Go 編程語言的 12 條最佳實踐
本文來自 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) {
越簡潔越好
或者 長代碼往往不是最好的.
試著使用能自解釋的最短的變量名.
- 用
MarshalIndent
,別用MarshalWithIndentation
.
別忘了包名會出現在你選擇的標識符前面
- In package
encoding/json
we find the typeEncoder
, notJSONEncoder
.
- It is referred as
json.Encoder
.
有多個文件的包
需要將一個包分散到多個文件中嗎?
- 避免行數非常多的文件
標準庫中 net/http
包有47個文件,共計 15734 行.
- 拆分代碼并測試
net/http/cookie.go
和 net/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 首頁 golang.org
- Go 交互式體驗 tour.golang.org
其他演講
謝謝
原文鏈接: Francesc Campoy Flores 翻譯: 伯樂在線 - Codefor
譯文鏈接: http://blog.jobbole.com/44608/