『每周譯Go』Google:12 條 Golang 最佳實踐
這是直接總結(jié)好的 12 條,詳細的再繼續(xù)往下看:
先處理錯誤避免嵌套
盡量避免重復(fù)
先寫最重要的代碼
給代碼寫文檔注釋
命名盡可能簡潔
使用多文件包
使用?
go get?可獲取你的包了解自己的需求
保持包的獨立性
避免在內(nèi)部使用并發(fā)
使用 Goroutine 管理狀態(tài)
避免 Goroutine 泄露
# 最佳實踐?#
這是一篇翻譯文章,為了使讀者更好的理解,會在原文翻譯的基礎(chǔ)增加一些講解或描述。
來在維基百科:
"A best practice is a method or technique that has consistently shown results superior
to those achieved with other means"
最佳實踐是一種方法或技術(shù),其結(jié)果始終優(yōu)于其他方式。
寫 Go 代碼時的技術(shù)要求:
簡單性
可讀性
可維護性
#?樣例代碼?#
需要優(yōu)化的代碼。
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
}
看看上面的代碼,自己先思索在代碼編寫方式上怎么更好,我先簡單說下代碼意思是啥:
將?
Name?和?AgeYears?字段數(shù)據(jù)存入?io.Writer?類型中。如果存入的數(shù)據(jù)是?
string?或?[]byte?類型,再追加其長度數(shù)據(jù)。
如果對?binary?這個標準包不知道怎么使用,就看看我的另一篇文章《快速了解 “小字端” 和 “大字端” 及 Go 語言中的使用》。
#?先處理錯誤避免嵌套?#
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
}
減少判斷錯誤的嵌套,會使讀者看起來更輕松。
#?盡量避免重復(fù)?#
上面代碼中?WriteTo?方法中的?Write?出現(xiàn)了 3 次,比較重復(fù),精簡后如下:
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))
}
}
使用?binWriter?結(jié)構(gòu)體。
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 處理不同類型?#
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 精簡?#
摒棄了上面代碼的?v.(string)?、v.(int)?類型反射使用。
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))
}
}
}
進入不同分支,x?變量對應(yīng)的就是該分支的類型。
#?自行決定是否寫入?#
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()
}
WriteTo?方法中,分了兩大部分,增加了靈活性:
組裝信息
調(diào)用?
Flush?方法來決定是否寫入?w。
#?函數(shù)適配器?#
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
}
}
函數(shù)?handler?包含了業(yè)務(wù)的邏輯和錯誤處理,下來將錯誤處理單獨寫一個函數(shù)處理,代碼修改如下:
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
}
#?組織你的代碼?#
1. 先寫最重要的
許可信息、構(gòu)建信息、包文檔。
import?語句:相關(guān)聯(lián)組使用空行分隔。
import (
"fmt"
"io"
"log"
"golang.org/x/net/websocket"
)
其余代碼,以最重要的類型開始,以輔助函數(shù)和類型結(jié)尾。
2. 文檔注釋
包名前的相關(guān)文檔。
// Package playground registers an HTTP handler at "/compile" that
// proxies requests to the golang.org playground service.
package playground
Go 語言中的標示符(變量、結(jié)構(gòu)體等等)在 godoc 導(dǎo)出的文章中應(yīng)該被正確的記錄下來。
// 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) {
擴展:
使用 godoc 工具在網(wǎng)頁上查看 go 項目文檔。
# 安裝
go get golang.org/x/tools/cmd/godoc
# 啟動服務(wù)
godoc -http=:6060
直接在本地訪問 localhost:6060?查看文檔。
3. 命名盡可能簡潔
或者說,長命名不一定好。
盡可能找到一個可以清晰表達的簡短命名,例如:
MarshalIndent?比?MarshalWithIndentation?好。
不要忘了,在調(diào)用包內(nèi)容時,會先寫包名。
在?
encoding/json?包內(nèi),有一個結(jié)構(gòu)體?Encoder,不要寫成?JSONEncoder。這樣被使用?
json.Encoder?。
4. 多文件包
是否應(yīng)該將一個包拆分到多個文件?
應(yīng)避免代碼太長
標準包?net/http?總共 15734 行代碼,被拆分到 47 個文件中。
拆分代碼和測試。
net/http/cookie.go 和 net/http/cookie_test.go 文件都放置在 http 包下。
測試代碼只有在測試時才被編譯。
拆分包文檔
當在一個包內(nèi)有多個文件時,按照慣例,創(chuàng)建一個 doc.go 文件編寫包的文檔描述。
個人思考:當一個包的說明信息比較多時,可以考慮創(chuàng)建 doc.go 文件。
5. 使用 go get 可獲取你的包
當你的包被提供使用時,應(yīng)該清晰的讓使用者知道哪些可復(fù)用,哪些不可復(fù)用。
所以,當一些包可能會被復(fù)用,有些則不會的情況下怎么做?
例如:定義一些網(wǎng)絡(luò)協(xié)議的包可能會復(fù)用,而定義一些可執(zhí)行命令的包則不會。

cmd?可執(zhí)行命令的包,不提供復(fù)用pkg?可復(fù)用的包
個人思考:如果一個項目中的可執(zhí)行入口比較多,建議放置在 cmd 目錄中,而對于 pkg 目錄目前是不太建議,所以不用借鑒。
#?API?#
1. 了解自己的需求
我們繼續(xù)使用之前的 Gopher 類型。
type Gopher struct {
Name string
AgeYears int
}
我們可以定義這個方法。
func (g *Gopher) WriteToFile(f *os.File) (int64, error) {
但方法的參數(shù)使用具體的類型時會變得難以測試,因此我們使用接口。
func (g *Gopher) WriteToReadWriter(rw io.ReadWriter) (int64, error) {
并且,當使用了接口后,我們應(yīng)該只需定義我們所需要的方法。
func (g *Gopher) WriteToWriter(f io.Writer) (int64, error) {
2. 保持包的獨立性
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)
}
代碼中?Draw?方法接受了?Parse?函數(shù)返回的?f?變量,從邏輯上看?drawer?包依賴?parser?包,下來看看如何取消這種依賴性。
parser?包:
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?包:
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 {
使用接口類型,避免依賴。
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.
4. 避免在內(nèi)部使用并發(fā)
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)
}
}
}
如果這樣做,那如果我們想同步調(diào)用?doConcurrently?該如何做?
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)
}
}
}
對外暴露同步的函數(shù),這樣并發(fā)調(diào)用時也是容易的,同樣也滿足同步調(diào)用。
#?最佳的并發(fā)實踐?#
1. 使用 Goroutine 管理狀態(tài)
Goroutine 之間使用一個 “通道” 或帶有通道字段的 “結(jié)構(gòu)體” 來通信。
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. 使用帶緩沖的通道避免 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
}
這段代碼有個問題,如果提前返回了?err?變量,errc?通道將不會被讀取,因此 Goroutine 將會阻塞。
總結(jié):
在寫入通道時 Goroutine 被阻塞。
Goroutine 持有對通道的引用。
通道不會被 gc 回收。
使用緩沖通道解決 Goroutine 阻塞問題。
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
}
如果我們不能預(yù)知通道的緩沖大小,也稱容量,那該怎么辦?
創(chuàng)建一個傳遞退出狀態(tài)的通道來避免 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
}
#?參考?#
原文鏈接:https://talks.golang.org/2013/bestpractices.slide#1
視頻鏈接:https://www.youtube.com/watch?v=8D3Vmm1BGoY
想要了解關(guān)于 Go 的更多資訊,還可以通過掃描的方式,進群一起探討哦~
