Go-advice 中文版本
Go-advice 中文版本
Go 箴言
- 不要通過共享內存進行通信,通過通信共享內存
- 併發不是並行
- 管道用於協調;互斥量(鎖)用於同步
- 接口越大,抽象就越弱
- 利用好零值
- 空接口
interface{}
沒有任何類型約束 - Gofmt 的風格不是人們最喜歡的,但 gofmt 是每個人的最愛
- 允許一點點重複比引入一點點依賴更好
- 系統調用必須始終使用構建標記進行保護
- 必須始終使用構建標記保護 Cgo
- Cgo 不是 Go
- 使用標準庫的
unsafe
包,不能保證能如期運行 - 清晰比聰明更好
- 反射永遠不清晰
- 錯誤是值
- 不要只檢查錯誤,還要優雅地處理它們
- 設計架構,命名組件,(文檔)記錄細節
- 文檔是供用戶使用的
- 不要(在生產環境)使用
panic()
Author: Rob Pike See more: https://go-proverbs.github.io/
Go 之禪
- 每個 package 實現單一的目的
- 顯式處理錯誤
- 儘早返回,而不是使用深嵌套
- 讓調用者處理併發(帶來的問題)
- 在啓動一個 goroutine 時,需要知道何時它會停止
- 避免 package 級別的狀態
- 簡單很重要
- 編寫測試以鎖定 package API 的行爲
- 如果你覺得慢,先編寫 benchmark 來證明
- 適度是一種美德
- 可維護性
Author: Dave Cheney See more: https://the-zen-of-go.netlify.com/
代碼
使用 go fmt
格式化
讓團隊一起使用官方的 Go 格式工具,不要重新發明輪子。 嘗試減少代碼複雜度。 這將幫助所有人使代碼易於閱讀。
多個 if 語句可以摺疊成 switch
// NOT BAD
if foo() {
// ...
} else if bar == baz {
// ...
} else {
// ...
}
// BETTER
switch {
case foo():
// ...
case bar == baz:
// ...
default:
// ...
}
用 chan struct{}
來傳遞信號, chan bool
表達的不夠清楚
當你在結構中看到 chan bool
的定義時,有時不容易理解如何使用該值,例如:
type Service struct {
deleteCh chan bool // what does this bool mean?
}
但是我們可以將其改爲明確的 chan struct {}
來使其更清楚:我們不在乎值(它始終是 struct {}
),我們關心可能發生的事件,例如:
type Service struct {
deleteCh chan struct{} // ok, if event than delete something.
}
30 * time.Second
比 time.Duration(30) * time.Second
更好
你不需要將無類型的常量包裝成類型,編譯器會找出來。
另外最好將常量移到第一位:
// BAD
delay := time.Second * 60 * 24 * 60
// VERY BAD
delay := 60 * time.Second * 60 * 24
// GOOD
delay := 24 * 60 * 60 * time.Second
用 time.Duration
代替 int64
+ 變量名
// BAD
var delayMillis int64 = 15000
// GOOD
var delay time.Duration = 15 * time.Second
按類型分組 const
聲明,按邏輯和 / 或類型分組 var
// BAD
const (
foo = 1
bar = 2
message = "warn message"
)
// MOSTLY BAD
const foo = 1
const bar = 2
const message = "warn message"
// GOOD
const (
foo = 1
bar = 2
)
const message = "warn message"
這個模式也適用於 var
。
- [ ] 每個阻塞或者 IO 函數操作應該是可取消的或者至少是可超時的
- [ ] 爲整型常量值實現
Stringer
接口 - [ ] 檢查
defer
中的錯誤
defer func() {
err := ocp.Close()
if err != nil {
rerr = err
}
}()
- [ ] 不要在
checkErr
函數中使用panic()
或os.Exit()
- [ ] 僅僅在很特殊情況下才使用 panic, 你必須要去處理 error
- [ ] 不要給枚舉使用別名,因爲這打破了類型安全
package main
type Status = int
type Format = int // remove `=` to have type safety
const A Status = 1
const B Format = 1
func main() {
println(A == B)
}
- 如果你想省略返回參數,你最好表示出來
* `_ = f()` 比 `f()` 更好
-
我們用
a := []T{}
來簡單初始化 slice -
用 range 循環來進行數組或 slice 的迭代
* `for _, c := range a[3:7] {...}` 比 `for i := 3; i < 7; i++ {...}` 更好
-
多行字符串用反引號 (`)
-
用
_
來跳過不用的參數
func f(a int, _ string) {}
- [ ] 如果你要比較時間戳,請使用
time.Before
或time.After
,不要使用time.Sub
來獲得 duration (持續時間),然後檢查它的值。 - [ ] 帶有上下文的函數第一個參數名爲
ctx
,形如:func foo(ctx Context, ...)
- [ ] 幾個相同類型的參數定義可以用簡短的方式來進行
func f(a int, b int, s string, p string)
func f(a, b int, s, p string)
- [ ] 一個 slice 的零值是 nil
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
// Output:
// [] 0 0
// nil!
var a []string
b := []string{}
fmt.Println(reflect.DeepEqual(a, []string{}))
fmt.Println(reflect.DeepEqual(b, []string{}))
// Output:
// false
// true
- [ ] 不要將枚舉類型與
<
,>
,<=
和>=
進行比較- 使用確定的值,不要像下面這樣做:
value := reflect.ValueOf(object)
kind := value.Kind()
if kind >= reflect.Chan && kind <= reflect.Slice {
// ...
}
- [ ] 用
%+v
來打印數據的比較全的信息 - [ ] 注意空結構
struct{}
, 看 issue: https://github.com/golang/go/issues/23440
func f1() {
var a, b struct{}
print(&a, "\n", &b, "\n") // Prints same address
fmt.Println(&a == &b) // Comparison returns false
}
func f2() {
var a, b struct{}
fmt.Printf("%p\n%p\n", &a, &b) // Again, same address
fmt.Println(&a == &b) // ...but the comparison returns true
}
* 例如: `errors.Wrap(err, "additional message to a given error")`
- 在 Go 裏面要小心使用
range
:
* `for i := range a` and `for i, v := range &a` ,都不是 `a` 的副本
* 但是 `for i, v := range a` 裏面的就是 `a` 的副本
* 更多: [https://play.golang.org/p/4b181zkB1O](https://play.golang.org/p/4b181zkB1O)
- 從 map 讀取一個不存在的 key 將不會 panic
* `value := map["no_key"]` 將得到一個 0 值
* `value, ok := map["no_key"]` 更好
- 不要使用原始參數進行文件操作
* 而不是一個八進制參數 `os.MkdirAll(root, 0700)`
* 使用此類型的預定義常量 `os.FileMode`
- 不要忘記爲
iota
指定一種類型
* [https://play.golang.org/p/mZZdMaI92cI](https://play.golang.org/p/mZZdMaI92cI)
const (
_ = iota
testvar // testvar 將是 int 類型
)
vs
type myType int
const (
_ myType = iota
testvar // testvar 將是 myType 類型
)
不要在你不擁有的結構上使用 encoding/gob
在某些時候,結構可能會改變,而你可能會錯過這一點。因此,這可能會導致很難找到 bug。
不要依賴於計算順序,特別是在 return 語句中。
// BAD
return res, json.Unmarshal(b, &res)
// GOOD
err := json.Unmarshal(b, &res)
return res, err
防止結構體字段用純值方式初始化,添加 _ struct {}
字段:
type Point struct {
X, Y float64
_ struct{} // to prevent unkeyed literals
}
對於 Point {X:1,Y:1}
都可以,但是對於 Point {1,1}
則會出現編譯錯誤:
./file.go:1:11: too few values in Point literal
當在你所有的結構體中添加了 _ struct{}
後,使用 go vet
命令進行檢查,(原來聲明的方式)就會提示沒有足夠的參數。
爲了防止結構比較,添加 func
類型的空字段
type Point struct {
_ [0]func() // unexported, zero-width non-comparable field
X, Y float64
}
http.HandlerFunc
比 http.Handler
更好
用 http.HandlerFunc
你僅需要一個 func,http.Handler
需要一個類型。
移動 defer
到頂部
這可以提高代碼可讀性並明確函數結束時調用了什麼。
JavaScript 解析整數爲浮點數並且你的 int64 可能溢出
用 json:"id,string"
代替
type Request struct {
ID int64 `json:"id,string"`
}
併發
- [ ] 以線程安全的方式創建單例(只創建一次)的最好選擇是
sync.Once
- 不要用 flags, mutexes, channels or atomics
- [ ] 永遠不要使用
select{}
, 省略通道, 等待信號 - [ ] 不要關閉一個發送(寫入)管道,應該由創建者關閉
- 往一個關閉的 channel 寫數據會引起 panic
- [ ]
math/rand
中的func NewSource(seed int64) Source
不是併發安全的,默認的lockedSource
是併發安全的, see issue: https://github.com/golang/go/issues/3611 - [ ] 當你需要一個自定義類型的 atomic 值時,可以使用 atomic.Value
性能
- [ ] 不要省略
defer
- 在大多數情況下 200ns 加速可以忽略不計
- [ ] 總是關閉 http body
defer r.Body.Close()
- 除非你需要泄露 goroutine
- [ ] 過濾但不分配新內存
b := a[:0]
for _, x := range a {
if f(x) {
b = append(b, x)
}
}
爲了幫助編譯器刪除綁定檢查,請參見此模式 _ = b [7]
- [ ]
time.Time
有指針字段time.Location
並且這對 go GC 不好- 只有使用了大量的
time.Time
才(對性能)有意義,否則用 timestamp 代替
- 只有使用了大量的
- [ ]
regexp.MustCompile
比regexp.Compile
更好- 在大多數情況下,你的正則表達式是不可變的,所以你最好在
func init
中初始化它
- 在大多數情況下,你的正則表達式是不可變的,所以你最好在
- [ ] 請勿在你的熱點代碼中過度使用
fmt.Sprintf
. 由於維護接口的緩衝池和動態調度,它是很昂貴的。- 如果你正在使用
fmt.Sprintf("%s%s", var1, var2)
, 考慮使用簡單的字符串連接。 - 如果你正在使用
fmt.Sprintf("%x", var)
, 考慮使用hex.EncodeToString
orstrconv.FormatInt(var, 16)
- 如果你正在使用
- [ ] 如果你不需要用它,可以考慮丟棄它,例如
io.Copy(ioutil.Discard, resp.Body)
- HTTP 客戶端的傳輸不會重用連接,直到 body 被讀完和關閉。
res, _ := client.Do(req)
io.Copy(ioutil.Discard, res.Body)
defer res.Body.Close()
- [ ] 不要在循環中使用 defer,否則會導致內存泄露
- 因爲這些 defer 會不斷地填滿你的棧(內存)
- [ ] 不要忘記停止 ticker, 除非你需要泄露 channel
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
- [ ] 用自定義的 marshaler 去加速 marshaler 過程
- 但是在使用它之前要進行定製!例如:https://play.golang.org/p/SEm9Hvsi0r
func (entry Entry) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString("{")
first := true
for key, value := range entry {
jsonValue, err := json.Marshal(value)
if err != nil {
return nil, err
}
if !first {
buffer.WriteString(",")
}
first = false
buffer.WriteString(key + ":" + string(jsonValue))
}
buffer.WriteString("}")
return buffer.Bytes(), nil
}
sync.Map
不是萬能的,沒有很強的理由就不要使用它。
* 瞭解更多: [https://github.com/golang/go/blob/master/src/sync/map.go#L12](https://github.com/golang/go/blob/master/src/sync/map.go#L12)
- 在
sync.Pool
中分配內存存儲非指針數據
* 瞭解更多: [https://github.com/dominikh/go-tools/blob/master/cmd/staticcheck/docs/checks/SA6002](https://github.com/dominikh/go-tools/blob/master/cmd/staticcheck/docs/checks/SA6002)
- 爲了隱藏逃生分析的指針,你可以小心使用這個函數::
* 來源: [https://go-review.googlesource.com/c/go/+/86976](https://go-review.googlesource.com/c/go/+/86976)
// noescape hides a pointer from escape analysis. noescape is
// the identity function but escape analysis doesn't think the
// output depends on the input. noescape is inlined and currently
// compiles down to zero instructions.
//go:nosplit
func noescape(p unsafe.Pointer) unsafe.Pointer {
x := uintptr(p)
return unsafe.Pointer(x ^ 0)
}
-
對於最快的原子交換,你可以使用這個
m := (*map[int]int)(atomic.LoadPointer(&ptr))
-
如果執行許多順序讀取或寫入操作,請使用緩衝 I/O
* 減少系統調用次數
- 有 2 種方法清空一個 map:
* 重用 map 內存 (但是也要注意 m 的回收)
for k := range m {
delete(m, k)
}
- 分配新的
m = make(map[int]int)
模塊
- [ ] 如果你想在 CI 中測試
go.mod
(和go.sum
)是否是最新 https://blog.urth.org/2019/08/13/testing-go-mod-tidiness-in-ci/
構建
- [ ] 用這個命令
go build -ldflags="-s -w" ...
去掉你的二進制文件 - [ ] 拆分構建不同版本的簡單方法
- 用
// +build integration
並且運行他們go test -v --tags integration .
- 用
- [ ] 最小的 Go Docker 鏡像
- https://twitter.com/bbrodriges/status/873414658178396160
CGO_ENABLED=0 go build -ldflags="-s -w" app.go && tar C app | docker import - myimage:latest
- [ ] run go format on CI and compare diff
- 這將確保一切都是生成的和承諾的
- [ ] 用最新的 Go 運行 Travis-CI,用
travis 1
- [ ] 檢查代碼格式是否有錯誤
diff -u <(echo -n) <(gofmt -d .)
測試
- [ ] 測試名稱
package_test
比package
要好 - [ ]
go test -short
允許減少要運行的測試數
func TestSomething(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode.")
}
}
- [ ] 根據系統架構跳過測試
if runtime.GOARM == "arm" {
t.Skip("this doesn't work under ARM")
}
- [ ] 用
testing.AllocsPerRun
跟蹤你的內存分配 - [ ] 多次運行你的基準測試可以避免噪音。
go test -test.bench=. -count=20
工具
-
快速替換
gofmt -w -l -r "panic(err) -> log.Error(err)" .
-
go list
允許找到所有直接和傳遞的依賴關係
* `go list -f '{{ .Imports }}' package`
* `go list -f '{{ .Deps }}' package`
- 對於快速基準比較,我們有一個
benchstat
工具。
* [https://godoc.org/golang.org/x/perf/cmd/benchstat](https://godoc.org/golang.org/x/perf/cmd/benchstat)
-
go-critic linter 從這個文件中強制執行幾條建議
-
go mod why -m <module>
告訴我們爲什麼特定的模塊在go.mod
文件中。 -
GOGC=off go build ...
應該會加快構建速度 source -
內存分析器每 512KB 記錄一次分配。你能通過
GODEBUG
環境變量增加比例,來查看你的文件的更多詳細信息。
* 來源:[https://twitter.com/bboreham/status/1105036740253937664](https://twitter.com/bboreham/status/1105036740253937664)
go mod why -m <module>
告訴我們爲什麼特定的模塊是在go.mod
文件中。
其他
- [ ] dump goroutines https://stackoverflow.com/a/27398062/433041
go func() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGQUIT)
buf := make([]byte, 1<<20)
for {
<-sigs
stacklen := runtime.Stack(buf, true)
log.Printf("=== received SIGQUIT ===\n*** goroutine dump...\n%s\n*** end\n" , buf[:stacklen])
}
}()
-
[ ] 在編譯期檢查接口的實現
var _ io.Reader = (*MyFastReader)(nil)
-
[ ] len(nil) = 0
-
[ ] 匿名結構很酷
var hits struct {
sync.Mutex
n int
}
hits.Lock()
hits.n++
hits.Unlock()
httputil.DumpRequest
是非常有用的東西,不要自己創建
* [https://godoc.org/net/http/httputil#DumpRequest](https://godoc.org/net/http/httputil#DumpRequest)
- 獲得調用堆棧,我們可以使用
runtime.Caller
* [https://golang.org/pkg/runtime/#Caller](https://golang.org/pkg/runtime/#Caller)
-
要 marshal 任意的 JSON, 你可以 marshal 爲
map[string]interface{}{}
-
配置你的
CDPATH
以便你能在任何目錄執行cd github.com/golang/go
* 添加這一行代碼到 `bashrc`(或者其他類似的) `export CDPATH=$CDPATH:$GOPATH/src`
- 從一個 slice 生成簡單的隨機元素
* `[]string{"one", "two", "three"}[rand.Intn(3)]`
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://github.com/cristaloleg/go-advice/blob/master/README_ZH.md