本文來自 Google 工程師 Francesc Campoy Flores 分享的幻燈片。內容包括:程式碼組織、API、併發最佳實踐和一些推薦的相關資源。
最佳實踐
維基百科的定義是:
“最佳實踐是一種方法或技術,其結果始終優於其他方式。”
寫Go程式碼的目標就是:
- 簡潔
- 可讀性強
- 可維護性好
樣例程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 } |
避免巢狀的處理錯誤
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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) } |
減少巢狀意味著提高程式碼的可讀性
儘可能避免重複
功能單一,程式碼更簡潔
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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 } |
使用型別推斷來處理特殊情況
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 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 } |
型別推斷的變數宣告要短
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 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) } } |
函式介面卡
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
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 宣告,相關的包連起來構成組,組與組之間用空行隔開.。
1 2 3 4 5 6 7 |
import ( "fmt" "io" "log" "code.google.com/p/go.net/websocket" ) |
接下來程式碼以最重要的型別開始,以工具函式和型別結束。
如何編寫文件
包名之前要寫相關文件
1 2 3 |
// Package playground registers an HTTP handler at "/compile" that // proxies requests to the golang.org playground service. package playground |
匯出的識別符號(譯者按:大寫的識別符號為匯出識別符號)會出現在 godoc
中,所以要正確的編寫文件。
1 2 3 4 5 6 7 8 9 |
// 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型別為例
1 2 3 4 5 |
type Gopher struct { Name string Age int32 FurColor color.Color } |
我們可以定義這個方法
1 |
func (g *Gopher) DumpToFile(f *os.File) error { |
但是使用一個具體的型別會讓程式碼難以測試,因此我們使用介面.
1 |
func (g *Gopher) DumpToReadWriter(rw io.ReadWriter) error { |
進而,由於使用的是介面,我們可以只請求我們需要的.
1 |
func (g *Gopher) DumpToWriter(f io.Writer) error { |
讓獨立的包彼此獨立
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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) } |
解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 } |
描繪
1 2 3 4 5 6 7 8 |
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 { |
使用介面來避免依賴.
1 2 3 4 5 6 7 8 9 |
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 { |
測試
使用介面而不是具體型別讓測試更簡潔.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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. |
在介面中避免併發
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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) } } } |
如果我們想序列的使用它會怎樣?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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通訊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
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記憶體洩漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
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永遠不會垃圾回收
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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記憶體洩漏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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
其他演講