golang中常見的認知錯誤記錄

      網友投稿 668 2025-03-31

      最近的一個項目中, 我采用了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您可以輕松檢查您的代碼是否存在此問題。

      golang中常見的認知錯誤記錄

      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小時內刪除侵權內容。

      上一篇:按上下左右鍵整個表格跟著走,(上下左右表格跟著動)
      下一篇:工程管理圖表甘特圖(工程進度表甘特圖)
      相關文章
      天天综合亚洲色在线精品| 国产精品亚洲专区无码不卡| 成人亚洲综合天堂| jlzzjlzz亚洲jzjzjz| 亚洲人成伊人成综合网久久| 久久av无码专区亚洲av桃花岛| 亚洲人成网站在线播放影院在线| 久久精品国产99精品国产亚洲性色| 亚洲AV无码一区东京热| 日本红怡院亚洲红怡院最新 | 亚洲综合色婷婷在线观看| 久久精品国产亚洲av麻豆蜜芽| 亚洲国产亚洲片在线观看播放| 亚洲黄色中文字幕| 亚洲日本在线播放| 亚洲伦理中文字幕| 亚洲永久网址在线观看| 亚洲av中文无码乱人伦在线观看| 亚洲av永久无码一区二区三区| 中文字幕亚洲码在线| 亚洲AV综合永久无码精品天堂| 亚洲熟妇AV一区二区三区宅男| 亚洲成a∧人片在线观看无码| 99亚洲精品卡2卡三卡4卡2卡| 国产综合成人亚洲区| 亚洲无线一二三四区手机| 亚洲一区二区三区影院| 亚洲av午夜福利精品一区| 久久夜色精品国产噜噜亚洲AV| 亚洲国产精品综合久久网各 | 亚洲午夜久久久久久久久久| 亚洲精品卡2卡3卡4卡5卡区| 亚洲AV日韩AV鸥美在线观看| 亚洲高清视频免费| 亚洲毛片基地4455ww| 亚洲AV无码专区在线电影成人| 亚洲精品国产精品乱码不卞| 国产日韩亚洲大尺度高清| 久久精品国产亚洲AV嫖农村妇女| 亚洲一区二区久久| jzzijzzij在线观看亚洲熟妇|