從錯誤中學習改正Go語言五個壞習慣提高編程技巧

引言

從他人的錯誤中學習,通過本指南避免常見陷阱和壞習慣,提高你的 Go 編程技巧

在 Go 語言中,就像在任何編程語言中一樣,瞭解常見陷阱和壞習慣是編寫幹凈、高效代碼的關鍵。

盡管下面列出的某些做法通常被認為是不好的,但在某些情況下它們可以有效地使用。 這篇文章旨在提醒大傢這些做法的問題所在,並教導如何避免這些陷阱。

讓我們深入探討。

1. 使用 init()

在 Go 中,init()函數是在主函數之前執行的特殊函數。

“如果在任何包中初始化是如此重要的過程,為什麼在 Go 中init()被認為是一種不好的實踐?” —— 讀者

是的,雖然init()函數有助於在運行核心邏輯之前進行初始化,但它們的執行順序可能難以理解。 這可能會導致關於初始化順序的混亂。

如果兩個模塊互相依賴於初始化並位於不同的包中,則可能會增加復雜性並需要額外的代碼添加等待邏輯。然而,這也可能導致死鎖的可能性。

另一個init()函數的問題是它會使測試更加困難。因為它們自動運行,很難控制它們何時執行,這可能使設置測試用例和測試代碼行為變得具有挑戰性。

我遇到瞭一個問題,我的服務從部署狀態到準備就緒需要 10 分鐘的時間。我在主函數的第一行設置瞭斷點,但它從未觸發。

我們不得不調試所有的init()函數,發現一個隊友在一個“我不記得的包”中使用瞭init()函數,從一個大文件加載瞭大量數據到內存中,這導致花費瞭很多時間追蹤一個微小的問題。

2. 使用全局變量

這與使用單例模式時可能出現的問題類似,特別是當全局變量是復雜的,包含映射、切片或指針時。

  • 競態條件(Race condition):當使用全局變量時,多個goroutine同時訪問全局變量會導致意外行為。這在Go語言中是一個很大的問題。

  • 難以測試:使用全局變量會使你的項目更具狀態,這意味著當你開始單元測試/集成測試時,全局變量必須與運行main()或生產環境時相同。

  • 不夠模塊化且難以重用:難以組織和封裝數據,因為任何包或模塊都可以訪問它們。這可能會導致代碼不夠模塊化,更難以理解,因為很難確定數據來自哪裡以及如何使用它們。

通常建議封裝您的包,使其可以在不影響其他包的情況下移動。使用全局變量可能會使您的代碼更緊密耦合,更難以修改或重用。

3. 忽略錯誤消息

錯誤是Go編程的內在部分,處理它們以優雅的方式確保在發生錯誤時不會發生意外情況非常重要。

忽略錯誤消息的方法是使用“_”符號,這樣做會丟棄函數返回的錯誤值,可能會導致意外行為。

檢查錯誤並適當地處理它們以防止程序發生崩潰和崩潰非常重要。

// sample 1
func main() {
    var x interface{} = "hello"
    s := x.(int) // panic: interface conversion: interface {} is string, not int
    fmt.Println(s)
}
// sample 2
func main() {
    var x interface{} = "hello"
    s, _ := x.(int) // safe but DON'T
    fmt.Println(s)
}

忽略錯誤處理會導致生產代碼中出現重大問題,因為這可能使得識別和修復錯誤變得困難。始終檢查錯誤並適當地解決它們對於確保代碼的順暢運行非常重要。

4. GOTO語句 — 跳進陷阱

不僅在Go語言中,許多語言都認為使用“goto”語句是一種不良實踐,因為它會使代碼更難理解和維護。

原因是“goto”語句忽略瞭代碼流程,使得在不引入錯誤的情況下難以理解代碼的不同部分之間的依賴關系。

這可能會使得在運行時推理程序的狀態變得困難,使得調試和測試變得更加困難。

使用“goto”可能會導致錯誤數量增加,使得更難以識別問題的根本原因。

5. 不使用Defer和Recover

利用“defer”和“recover”的主要原因是為瞭防止恐慌。“defer”甚至可以在發生恐慌時執行,而“recover”可以捕捉到恐慌,允許更加控制地處理意外情況。

這種使用方式被認為是不好的代碼實踐:

func readFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
    // Do something with the file
    file.Close() // <--- DONT
}

我們使用defer這種方式,這樣即使readFile()函數出現panic,文件也會被關閉。此外,這樣做也容易記住在open()函數之後立即放置關閉函數。

func readFile(filename string) {
    file, err := os.Open(filename)
    if err != nil {
        log.Fatal(err)
    }
    defer file.Close()
    // Do something with the file
    ...
}

6. 使用太多次的context.Background()

在 Go 中,上下文是最強大的功能之一。當正確使用時,它可以作為提供程序、樹流和流控制器。

在進行外部調用(例如數據庫、HTTP 等)時,使用context.Background()context.ToDo()設置截止時間或超時非常重要。如果沒有設置,當用戶過多且外部服務沒有及時響應時,可能會導致您的應用程序出現瓶頸。

我通常會編寫一個上下文池的實用函數,其中包括 3 個函數來創建或包裝具有超時的新上下文:快速(0.5 秒)、中等(3 秒)、慢速(10 秒)。這樣,我就不必一直依賴context.Background(),並且可以輕松地為每個調用設置適當的超時時間。

const FastTimeout = 500 * time.Millisecond
func WrapCustomContext(ctx context.Context, dur time.Duration) (context.Context, context.CancelFunc) {
    return context.WithTimeout(context.Background(), dur)
}
func GenFastContext() (context.Context, context.CancelFunc) {
    return WrapCustomContext(context.Background(), FastTimeout)
}
func WrapFastContext(ctx context.Context) (context.Context, context.CancelFunc) {
    return WrapCustomContext(ctx, FastTimeout)
}

以上就是從錯誤中學習改正Go語言五個壞習慣提高編程技巧的詳細內容,更多關於Go語言五個編程習慣的資料請關註WalkonNet其它相關文章!

推薦閱讀: