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/jsonwe 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/