golang中常見的認知錯誤記錄
最近的一個項目中, 我采用了go作為我的后端基礎,需求總體上并不復雜,代碼寫著寫著就變多了,除去腳手架生成的代碼,代碼其實并不多;期間遇到不少關于go語法認知的小問題,早就想開個帖子單獨記錄下,這周終于有空開始發發博客了,整理下集中放一個帖子,帖子上面放我自己的一些收集,下面部分放一些網絡上的相關帖子.
PART.A
golang中的switch(參考https://yourbasic.org/golang/switch-statement/,https://www.runoob.com/go/go-switch-statement.html,https://studygolang.com/articles/28415,https://www.cnblogs.com/yahuian/p/11615408.html)
需要注意的點,代碼段中自帶break,由于這點多條件語句不能像其他語言中那樣寫,多條件的語法是單行中逗號這種形式,由于經常寫不同的語言,我不傾向于使用fallthrough這個關鍵詞;
由于golang中存在指針,雖然他的解指針等等已經做的很舒適了,但是其實容易犯一種不易察覺的錯誤,slice中存儲了同一個指針,循環中操作到最后所有的值其實是同一個;
gorm使用很方便,但是我有個有個比較常犯的錯誤,查詢出錯并不包含查詢到0條記錄;
待續
PART.B
Go: what to return? A slice of structs vs a slice of pointers?(https://andrii-kushch.medium.com/go-what-to-return-a-slice-of-structs-vs-a-slice-of-pointers-42647912530a)
我多次回答了同樣的問題:從go 中的函數返回什么更可取,一片結構還是一片指向這些結構的指針?所以我決定寫這篇文章來展示這兩種方法之間的區別。
換句話說,問題是以下哪個功能更好。
func ReturnSliceWithPointers() []*Person func ReturnSliceWithStructs() []Person
更好的,在這種情況下,工作手段的快d使用較少的內存。最簡單的方法是使用 golang 測試包提供的工具。我寫了兩個類似的函數,它們創建、填充和返回一個數組。我為他們寫了一個基準。
package main import "testing" type Person struct { Age int } func ReturnSliceWithPointers(size int) []*Person { res := make([]*Person, size) for i := 0; i < size; i++ { res[i] = &Person{} } return res } func ReturnSliceWithStructs(size int) []Person { res := make([]Person, size) for i := 0; i < size; i++ { res[i] = Person{} } return res } func Benchmark_ReturnSliceWithPointers(b *testing.B) { for i := 0; i < b.N; i++ { ReturnSliceWithPointers(10000) } } func Benchmark_ReturnSliceWithStructs(b *testing.B) { for i := 0; i < b.N; i++ { ReturnSliceWithStructs(10000) } }
讓我們運行它
go test -bench=. -benchmem -benchtime=10000x
結論
我們看到函數ReturnSliceWithStructs的分配更少。每次操作使用的內存也更少,性能更好。
同時,函數ReturnSliceWithPointers看起來更糟:性能和內存效率更低。
它有更多的內存分配:一個分配給一個切片,一個分配給一個切片中的每個項目。
res := make([]*Person, size) for i := 0; i < size; i++ { res[i] = &Person{} }
正因為如此,它會在 GC 上產生更多的負載。
那么使用哪一種呢?看起來選擇是顯而易見的,但并非總是如此。在某些情況下,您可以更喜歡一種方法而不是另一種方法。首先,問問自己,你有必要在乎它嗎?如果是,那么決定完全取決于您的應用程序設計和您使用的庫的接口。請記住:您始終可以使用類似的基準來查找提示。
5 Mistakes I’ve Made in Go(https://medium.com/swlh/5-mistakes-ive-made-in-go-75fb64b943b8)
To err is human, to forgive divine.
— Alexander Pope
這些是我在編寫 Go 時犯的錯誤。雖然這些可能不會導致任何類型的錯誤,但它們可能會影響軟件。
1. 內循環
有幾種方法可以在循環中弄亂您需要注意的問題。
1.1 使用引用來循環迭代器變量
由于效率原因,循環迭代器變量是單個變量,在每次循環迭代中采用不同的值。它可能會導致不知情的行為。
in := []int{1, 2, 3} var out []*int for _, v := range in { out = append(out, &v) } fmt.Println("Values:", *out[0], *out[1], *out[2]) fmt.Println("Addresses:", out[0], out[1], out[2])
結果將是:
Values: 3 3 3 Addresses: 0xc000014188 0xc000014188 0xc000014188
如您所見,out切片中的所有元素都是 3。實際上很容易解釋為什么會發生這種情況:在每次迭代中,我們都將 的地址附加v到out切片中。如前所述,v是一個在每次迭代中都采用新值的單個變量。因此,正如您在輸出的第二行中看到的那樣,地址是相同的,并且所有地址都指向相同的值。
簡單的解決方法是將循環迭代器變量復制到一個新變量中:
in := []int{1, 2, 3} var out []*int for _, v := range in { v := v out = append(out, &v) } fmt.Println("Values:", *out[0], *out[1], *out[2]) fmt.Println("Addresses:", out[0], out[1], out[2])
新的輸出:
Values: 1 2 3 Addresses: 0xc0000b6010 0xc0000b6018 0xc0000b6020
同樣的問題可以發現循環迭代變量正在 Goroutine 中使用。
list := []int{1, 2, 3} for _, v := range list { go func() { fmt.Printf("%d ", v) }() }
輸出將是:
3 3 3
可以使用上述相同的解決方案來修復它。請注意,沒有使用 Goroutine 運行該函數,代碼會按預期運行。
1.2 循環調用WaitGroup.Wait
這個錯誤可以使用類型的共享變量來犯WaitGroup,如下面的代碼所示Wait(),當Done()第 5 行被調用len(tasks)次數時,第7 行只能被解除阻塞,因為它被用作在第 2 行調用的參數Add()。但是,在Wait()循環內部調用了 ,因此它會在下一次迭代中阻止在第 4 行創建 Goroutine。簡單的解決方案是將Wait()out的調用從循環中移出。
var wg sync.WaitGroup wg.Add(len(tasks)) for _, t := range tasks { go func(t *task) { defer group.Done() }(t) // group.Wait() } group.Wait()
1.3 在循環中使用 defer
defer在函數返回之前不會執行。defer除非您確定自己在做什么,否則不應在循環中使用。
var mutex sync.Mutex type Person struct { Age int } persons := make([]Person, 10) for _, p := range persons { mutex.Lock() // defer mutex.Unlock() p.Age = 13 mutex.Unlock() }
在上面的例子中,如果你使用第 8 行而不是第 10 行,下一次迭代不能持有互斥鎖,因為鎖已經被使用并且循環永遠阻塞。
如果您真的需要在循環內使用 defer,您可能需要委托另一個函數來完成這項工作。
var mutex sync.Mutex type Person struct { Age int } persons := make([]Person, 10) for _, p := range persons { func() { mutex.Lock() defer mutex.Unlock() p.Age = 13 }() }
但是,有時defer在循環中使用可能會變得方便。所以你真的需要知道你在做什么。
2. 發送到無保障頻道
您可以將值從一個 Goroutine 發送到通道,然后將這些值接收到另一個 Goroutine。默認情況下,發送和接收阻塞,直到對方準備好。這允許 Goroutines 在沒有顯式鎖或條件變量的情況下進行同步。
func doReq(timeout time.Duration) obj { // ch :=make(chan obj) ch := make(chan obj, 1) go func() { obj := do() ch <- result } () select { case result = <- ch : return result case<- time.After(timeout): return nil } }
讓我們檢查上面的代碼。該doReq函數在第 4 行創建一個子 Goroutine 來處理請求,這是 Go 服務器程序中的常見做法。子 Goroutine在第 6 行執行do函數并通過 channel 將結果發送回父ch。子將在第 6 行阻塞,直到父ch在第 9 行收到結果。同時,父將阻塞,select直到子將結果發送到ch(第 9 行)或發生超時時(第 11 行)。如果超時發生得更早,父doReq進程將在第 12 行從func返回,并且沒有其他人可以再收到結果ch,這導致子進程被永遠阻塞。解決方法是改變ch從一個無緩沖通道到一個緩沖通道,這樣子 Goroutine 總是可以發送結果,即使父 Goroutine 已經退出。另一個解決方法是在第 6 行使用一個select帶有空defaultcase的語句,這樣如果沒有 Goroutine 接收ch,default就會發生。盡管此解決方案可能并不總是有效。
... select { case ch <- result: default: } ...
3. 不使用接口
接口可以使代碼更加靈活。這是在代碼中引入多態的一種方式。接口允許您請求一組行為而不是特定類型。不使用接口可能不會導致任何錯誤,但可能會導致代碼不那么簡單、不靈活和可擴展性較差。
在眾多的接口,io.Reader并且io.Writer可能是最可愛的人。
type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) }
這些接口可能非常強大。假設您要將一個對象寫入文件,因此您定義了一個Save方法:
func (o *obj) Save(file os.File) error
如果你需要寫到http.ResponseWriter第二天怎么辦?您不想定義新方法。你?所以使用io.Writer.
func (o *obj) Save(w io.Writer) error
還有一個重要的注意事項,您應該知道的是,始終詢問您將要使用的行為。在上面的示例中,請求 anio.ReadWriteCloser也可以工作,但是當您要使用的唯一方法是Write.?接口越大,抽象越弱。
所以大多數時候你最好保持行為而不是具體的類型。
4. 錯誤的有序結構
這個錯誤也不會導致任何錯誤,但它會導致更多的內存使用。
type BadOrderedPerson struct { Veteran bool // 1 byte Name string // 16 byte Age int32 // 4 byte } type OrderedPerson struct { Name string Age int32 Veteran bool }
似乎兩種類型的大小都相同,均為 21 字節,但結果顯示出完全不同的內容。使用 編譯代碼GOARCH=amd64,BadOrderedPerson類型分配 32 個字節,而OrderedPerson類型分配24 個字節。為什么?嗯,原因是數據結構對齊。在 64 位架構中,內存分配 8 字節的連續數據包。需要添加的 Padding 可以通過以下方式計算:
padding = (align - (offset mod align)) mod align aligned = offset + padding = offset + ((align - (offset mod align)) mod align)
type BadOrderedPerson struct { Veteran bool // 1 byte _ [7]byte // 7 byte: padding for alignment Name string // 16 byte Age int32 // 4 byte _ struct{} // to prevent unkeyed literals // zero sized values, like struct{} and [0]byte occurring at // the end of a structure are assumed to have a size of one byte. // so padding also will be addedd here as well. } type OrderedPerson struct { Name string Age int32 Veteran bool _ struct{} }
當您有一個大的常用類型時,它可能會導致性能問題。但別擔心,您不必手動處理所有結構。使用maligned您可以輕松檢查您的代碼是否存在此問題。
5. 在測試中不使用種族檢測器
數據競爭會導致神秘的失敗,通常是在代碼部署到生產之后很久。因此,這些是并發系統中最常見和最難調試的錯誤類型。為了幫助區分這些類型的錯誤,Go 1.1 引入了一個內置的數據競爭檢測器。只需添加-race標志即可使用。
$ go test -race pkg // to test the package $ go run -race pkg.go // to run the source file $ go build -race // to build the package $ go install -race pkg // to install the package
啟用競爭檢測器后,編譯器將記錄在代碼中訪問內存的時間和方式,同時runtime監視對共享變量的非同步訪問。
當發現數據競爭時,競爭檢測器會打印一份報告,其中包含沖突訪問的堆棧跟蹤。下面是一個例子:
WARNING: DATA RACE Read by goroutine 185: net.(*pollServer).AddFD() src/net/fd_unix.go:89 +0x398 net.(*pollServer).WaitWrite() src/net/fd_unix.go:247 +0x45 net.(*netFD).Write() src/net/fd_unix.go:540 +0x4d4 net.(*conn).Write() src/net/net.go:129 +0x101 net.func·060() src/net/timeout_test.go:603 +0xaf Previous write by goroutine 184: net.setWriteDeadline() src/net/sockopt_posix.go:135 +0xdf net.setDeadline() src/net/sockopt_posix.go:144 +0x9c net.(*conn).SetDeadline() src/net/net.go:161 +0xe3 net.func·061() src/net/timeout_test.go:616 +0x3ed Goroutine 185 (running) created at: net.func·061() src/net/timeout_test.go:609 +0x288 Goroutine 184 (running) created at: net.TestProlongTimeout() src/net/timeout_test.go:618 +0x298 testing.tRunner() src/testing/testing.go:301 +0xe8
最后的話
唯一真正的錯誤是我們一無所獲。
Go HTTP
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。