通向Golang的捷徑【16. 常見的陷阱和誤用】

在之前的章節中, 對一些誤用給出了提示, 爲了避免讓用戶在不同的章節中, 查找上述提示, 以下給出了 Go 語言的一些常見陷阱, 以方便查找:

• 不要使用類似於 var p*a 的聲明, 因爲這將與指針聲明和乘法操作相沖突 (4.9 節)
• 不要在 for 循環中, 修改計數器變量 (5.4 節)
• 在 for-range 條件中使用的數值, 不要在循環中進行修改 (5.4.4 節)
• 不要使用 goto 語句, 跳轉到之前的 label(5.6 節)
• 不要在函數名之後, 使用 forget 函數 (第 6 章), 尤其是在接收器中調用方法或是調用匿名函數 (將啓動
併發協程) 時.
• 不要使用 new() 創建 map, 而應該使用 make(8.1 節)
• 如果爲某一類型提供了 String() 方法, 則不要使用 fmt.Print 或類似函數 (10.7 節)
• 在終止帶緩衝的寫入操作時, 不要忘記使用 Flush 函數 (12.2.3 節)
• 不要忽略錯誤, 因爲忽略錯誤纔會導致程序的崩潰 (13.1 節)
• 不要使用全局變量或是共享內存, 它們將導致併發執行的不安全性 (14.1 節)
• println 函數只用於調試

以下將給出一些推薦用法

• 使用正確的方法, 初始化一個 map slice(8.1.3 節)
• 在類型斷言中, 可一直使用 comma,ok(或 checked) 變量 (11.3 節)
• 使用工廠模式, 進行類型的創建和初始化 (10.2 節,18.4 節)
• 如果方法需要修改一個結果, 該結構可使用指針傳遞, 如果不需要修改, 則應當使用數值傳遞 (10.6.3 節)

本章將會提供 Go 編程的一些常見濫用和禁忌, 同時還會使用之前章節給出的一些示例.

16.1 濫用簡單聲明會隱藏變量

在這裏插入圖片描述
在上述代碼中, 變量 remember 在 if 語句塊之外, 不會變爲 true, 如果 if 條件爲真, 在 if 語句塊中, 將會創建一個新的 remember 變量, 並會隱藏外部 remember, 因爲給出了:= 操作符, 當退出 if 語句塊後,remember 將恢復原有值 (false), 因此應改爲:
在這裏插入圖片描述
上述濫用也將出現在 for 循環中, 尤其是函數的命名返回變量, 如下:
在這裏插入圖片描述
在這裏插入圖片描述

16.2 字符串的濫用

如果需要對字符串進行維護時, 不要忘記在 Go 語言 (以及 Java 和 C#) 中, 字符串是固定的, 字符串合併操作 a += b 相當低效, 尤其是在一個循環中, 執行字符串的合併時, 這將造成大量的內存複製以及重新分配, 因此可使用一個 bytes.Buffer, 來實現字符串的處理, 如下:
在這裏插入圖片描述
基於編譯器的優化操作, 以及字符串的尺寸, 同時循環迭代的次數大於 15 時, 使用一個緩衝, 可實現字符串的高效處理.

16.3 defer 的濫用

假定需要在 for 循環中, 處理一組文件, 當文件處理完畢後, 必須保證所有文件都被關閉, 因此使用了 defer.
在這裏插入圖片描述
同時在 for 循環退出時,defer 並不會指向, 因此所有文件都沒有關閉! 這時垃圾收集操作可能會關閉這些文件,但會給出錯誤提示, 所以最好使用以下方式:
在這裏插入圖片描述
只有函數返回時,defer 纔會執行, 在循環退出或是其他受限區域退出時, 不會執行 defer.

16.4 new() 和 make() 的衝突

從 7.2.4 節和 10.2.2 節中, 都可找到 new 和 make 的正確用法和示例, 它們重要的區別在於:
• 對於 slice,map 和 channel 類型來說, 應使用 make
• 對於數組, 結構和所有數值類型來說, 應使用 new

16.5 函數的 slice 形參

在 4.9 節中可見, 一個 slice 實爲下層數組的一個指針, 將 slice 傳遞給函數形參, 是爲了實現數據的指針傳遞(可對原有數據進行修改), 而不是傳遞一個數據副本.
在這裏插入圖片描述
當函數形參爲 slice 類型, 則不能給出 slice 的反向引用, 如上例.

16.6 接口類型的指針

在以下示例中,nexter 是一個包含 next 方法 (可讀取下一個字節數據) 的接口類型, nextFew1 包含了一個接口類型的形參, 可讀取後續的 num 個字節數據, 能將這些數據放入一個 slice 類型中, 並返回 slice 類型, 因此上述操作可正常工作. 而 nextFew2 包含一個接口類型 (與 nextFew1 形參相同) 的指針, 作爲函數形參, 當調用next() 函數時, 將產生一個編譯器錯誤:n.next undefined (type *nexter has no field or method next).

例 16.1 pointer_interface.go(無法編譯)

在這裏插入圖片描述
不要使用接口類型的指針, 因爲接口類型本身就是一個指針.

16.7 數值類型的指針濫用

在函數或方法接收器的數值傳遞中 (傳遞給形參), 感覺會造成內存的低效使用, 因爲數值需要進行復制, 但是這些數值通常放置在棧中, 因此速度很快且開銷很小, 如果使用該數值的指針進行傳遞, 在大多數情況下, Go編譯器會將其視爲一個對象, 並會在堆 (heap) 上移動該對象, 從而造成不必要的內存分配, 因此不要傳遞數值類型的指針.

16.8 併發協程和併發通道的濫用

爲了描述和討論, 第 14 章給出了大量的併發協程和併發通道的應用示例, 重要是一些相當簡單的算法, 比如生成器或迭代器, 但在實際情況下, 並不需要經常使用併發, 或是在意併發協程和併發通道的開銷, 因爲在大多數情況下, 棧中進行的形參傳遞, 可實現更高的效率.

如果使用 break,return 或 panic, 退出一個循環時, 可能會產生內存泄露, 因爲併發協程正處於阻塞狀態, 所以在實際代碼中, 通常會給出一個簡單的順序循環 (而無須使用併發協程), 因此併發協程和併發通道只能用於併發執行.

16.9 使用併發協程實現封裝

例 16.2 closures_goroutines.go

在這裏插入圖片描述
在這裏插入圖片描述
版本 A 調用了 5 次匿名函數, 並打印出相應的索引值, 版本 B 同樣調用了 5 次匿名函數, 但每次調用都封裝在併發協程中, 這可加快執行的速度, 因爲 5 個函數調用可併發執行, 如果這 5 個函數的執行時間足夠, 爲什麼版本 B 的輸出爲4 4 4 4 4, 這是因爲 B 循環的 ix 變量是獨立變量, 所以可基於循環的迭代而動態變化, 同時 5 個併發的函數調用都引用了同一個變量, 因此在 5 個函數調用中, 都只能獲得最後一個索引值 (4), 所以在循環中所調用的併發協程, 並不會立即執行.

版本 C 給出了正確的編碼方式, 每次函數調用時, 都將 ix 視爲一個實參, 因此每次迭代給出的 ix, 都將放置到併發協程的棧中, 所以每個併發協程都能得到有效的 ix 值, 但索引值的輸出結果, 則依賴於併發協程的執行時間, 比如0 2 1 3 4 或0 3 1 2 4 打印結果都有可能.

在版本 D 中, 打印出了數組的元素值, 但是它與版本 B 的結果並一致? 即如果索引值一致, 那麼元素值也應當一致, 因爲在循環語句塊中, 給出一個變量聲明 (val), 因此該變量並不會在併發協程之間共享.

16.10 錯誤處理

第 13 章詳細描述了錯誤的處理, 同時會在 17.1 和 17.2 中, 對錯誤處理進行必要的總結.

16.10.1 不使用布爾類型

創建一個布爾變量, 以實現錯誤條件的測試, 但以下代碼有些累贅:
在這裏插入圖片描述
但錯誤測試必須在函數返回之後立即執行:
在這裏插入圖片描述

16.10.2 不要讓錯誤檢查攪亂代碼

應當避免以下的編碼風格:
在這裏插入圖片描述
在這裏插入圖片描述
可在 if 條件的初始化語句中, 給出函數的調用, 如果在整個代碼中, 散落着 if 語句塊實現的錯誤報告, 將難以區分正常的代碼邏輯和錯誤檢查 (報告), 所以在大多數情況下, 應在代碼的某些執行點上, 進行專門的錯誤檢查, 一個更好的推薦方式, 則是在一個函數中, 封裝所需的錯誤檢查, 如下:
在這裏插入圖片描述
使用上述的編碼風格, 可簡單區分錯誤檢查, 錯誤報告和正常的代碼邏輯, 參見 13.5 節.

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章