在開發生產項目的過程中,我注意到經常會發現自己在重復編寫代碼,使用某些技巧時沒有意識到,直到后來回顧工作時才意識到。
為了解決這個問題,我開發了一種解決方案,對我來說非常有幫助,我覺得對其他人也可能有用。
以下是一些從我的實用程序庫中隨機挑選的有用且多功能的代碼片段,沒有特定的分類或特定于系統的技巧。
1. 追蹤執行時間的技巧
如果你想追蹤 Go 中函數的執行時間,有一個簡單高效的技巧可以用一行代碼實現,使用 defer 關鍵字即可。你只需要一個 TrackTime 函數:
// Utility
func TrackTime(pre time.Time) time.Duration {
elapsed := time.Since(pre)
fmt.Println("elapsed:", elapsed)
return elapsed
}
func TestTrackTime(t *testing.T) {
defer TrackTime(time.Now()) // <--- THIS
time.Sleep(500 * time.Millisecond)
}
// 輸出:
// elapsed: 501.11125ms
1.5. 兩階段延遲執行
Go 的 defer 不僅僅是用于清理任務,還可以用于準備任務,考慮以下示例:
func setupTeardown() func() {
fmt.Println("Run initialization")
return func() {
fmt.Println("Run cleanup")
}
}
func mAIn() {
defer setupTeardown()() // <--------
fmt.Println("Main function called")
}
// 輸出:
// Run initialization
// Main function called
// Run cleanup
這種模式的美妙之處在于,只需一行代碼,你就可以完成諸如以下任務:
- 打開數據庫連接,然后關閉它。
- 設置模擬環境,然后拆除它。
- 獲取分布式鎖,然后釋放它。
- ...
"嗯,這似乎很聰明,但它在現實中有什么用處呢?"
還記得追蹤執行時間的技巧嗎?我們也可以這樣做:
func TrackTime() func() {
pre := time.Now()
return func() {
elapsed := time.Since(pre)
fmt.Println("elapsed:", elapsed)
}
}
func main() {
defer TrackTime()()
time.Sleep(500 * time.Millisecond)
}
注意!如果我連接到數據庫時出現錯誤怎么辦?
確實,像 defer TrackTime() 或 defer ConnectDB() 這樣的模式不會妥善處理錯誤。這種技巧最適合用于測試或者當你愿意冒著致命錯誤的風險時使用,參考下面這種面向測試的方法:
func TestSomething(t *testing.T) {
defer handleDBConnection(t)()
// ...
}
func handleDBConnection(t *testing.T) func() {
conn, err := connectDB()
if err != nil {
t.Fatal(err)
}
return func() {
fmt.Println("Closing connection", conn)
}
}
這樣,在測試期間可以處理數據庫連接的錯誤。
2. 預分配切片
根據文章《Go 性能提升技巧》中的見解,預分配切片或映射可以顯著提高 Go 程序的性能。
但是值得注意的是,如果我們不小心使用 Append 而不是索引(如 a[i]),這種方法有時可能導致錯誤。你知道嗎,我們可以在不指定數組長度(為零)的情況下使用預分配的切片,就像在上述文章中解釋的那樣?這使我們可以像使用 append 一樣使用預分配的切片:
// 與其
a := make([]int, 10)
a[0] = 1
// 不如這樣使用
b := make([]int, 0, 10)
b = append(b, 1)
3. 鏈式調用
鏈式調用技術可以應用于函數(指針)接收器。為了說明這一點,讓我們考慮一個 Person 結構,它有兩個函數 AddAge 和 Rename,用于對其進行修改。
type Person struct {
Name string
Age int
}
func (p *Person) AddAge() {
p.Age++
}
func (p *Person) Rename(name string) {
p.Name = name
}
如果你想給一個人增加年齡然后給他們改名字,常規的方法是:
func main() {
p := Person{Name: "Aiden", Age: 30}
p.AddAge()
p.Rename("Aiden 2")
}
或者,我們可以修改 AddAge 和 Rename 函數接收器,使其返回修改后的對象本身,即使它們通常不返回任何內容。
func (p *Person) AddAge() *Person {
p.Age++
return p
}
func (p *Person) Rename(name string) *Person {
p.Name = name
return p
}
通過返回修改后的對象本身,我們可以輕松地將多個函數接收器鏈在一起,而無需添加不必要的代碼行:
p = p.AddAge().Rename("Aiden 2")
4. Go 1.20 允許將切片解析為數組或數組指針
當我們需要將切片轉換為固定大小的數組時,不能直接賦值,例如:
a := []int{0, 1, 2, 3, 4, 5}
var b [3]int = a[0:3]
// 在變量聲明中不能將 a[0:3](類型為 []int 的值)賦值給 [3]int 類型的變量
// (不兼容的賦值)
為了將切片轉換為數組,Go 團隊在 Go 1.17 中更新了這個特性。隨著 Go 1.20 的發布,借助更方便的字面量,轉換過程變得更加簡單:
// Go 1.20
func Test(t *testing.T) {
a := []int{0, 1, 2, 3, 4, 5}
b := [3]int(a[0:3])
fmt.Println(b) // [0 1 2]
}
// Go 1.17
func TestM2e(t *testing.T) {
a := []int{0, 1, 2, 3, 4, 5}
b := *(*[3]int)(a[0:3])
fmt.Println(b) // [0 1 2]
}
只是一個快速提醒:你可以使用 a[:3] 替代 a[0:3]。我提到這一點是為了更清晰地說明。
5. 使用 "import _" 進行包初始化
有時,在庫中,你可能會遇到結合下劃線 (_) 的導入語句,如下所示:
import (
_ "google.golang.org/genproto/googleapis/api/annotations"
)
這將執行包的初始化代碼(init 函數),而無需為其創建名稱引用。這允許你在運行代碼之前初始化包、注冊連接和執行其他任務。
讓我們通過一個示例來更好地理解它的工作原理:
// 下劃線
package underscore
func init() {
fmt.Println("init called from underscore package")
}
// main
package main
import (
_ "lab/underscore"
)
func main() {}
// 輸出:init called from underscore package
6. 使用 "import ." 進行導入
在了解了如何使用下劃線進行導入后,讓我們看看如何更常見地使用點 (.) 運算符。
作為開發者,點 (.) 運算符可用于在不必指定包名的情況下使用導入包的導出標識符,這對于懶惰的開發者來說是一個有用的快捷方式。
很酷,對吧?這在處理項目中的長包名時特別有用,比如 externalmodel 或 doingsomethinglonglib。
為了演示,這里有一個簡單的例子:
package main
import (
"fmt"
. "math"
)
func main() {
fmt.Println(Pi) // 3.141592653589793
fmt.Println(Sin(Pi / 2)) // 1
}
7. Go 1.20 允許將多個錯誤合并為單個錯誤
Go 1.20 引入了對錯誤包的新功能,包括對多個錯誤的支持以及對 errors.Is 和 errors.As 的更改。
在 errors 中添加的一個新函數是 Join,我們將在下面詳細討論它:
var (
err1 = errors.New("Error 1st")
err2 = errors.New("Error 2nd")
)
func main() {
err := err1
err = errors.Join(err, err2)
fmt.Println(errors.Is(err, err1)) // true
fmt.Println(errors.Is(err, err2)) // true
}
如果有多個任務導致錯誤,你可以使用 Join 函數而不是手動管理數組。這簡化了錯誤處理過程。
8. 檢查接口是否為真正的 nil
即使接口持有的值為 nil,也不意味著接口本身為 nil。這可能導致 Go 程序中的意外錯誤。因此,重要的是要知道如何檢查接口是否為真正的 nil。
func main() {
var x interface{}
var y *int = nil
x = y
if x != nil {
fmt.Println("x != nil") // <-- 實際輸出
} else {
fmt.Println("x == nil")
}
fmt.Println(x)
}
// 輸出:
// x != nil
// <nil>
我們如何確定 interface{} 值是否為 nil 呢?幸運的是,有一個簡單的工具可以幫助我們實現這一點:
func IsNil(x interface{}) bool {
if x == nil {
return true
}
return reflect.ValueOf(x).IsNil()
}
9. 在 JSON 中解析 time.Duration
當解析 JSON 時,使用 time.Duration 可能是一個繁瑣的過程,因為它需要在一秒的后面添加 9 個零(即 1000000000)。為了簡化這個過程,我創建了一個名為 Duration 的新類型:
type Duration time.Duration
為了將字符串(如 "1s" 或 "20h5m")解析為 int64 類型的持續時間,我還為這個新類型實現了自定義的解析邏輯:
func (d *Duration) UnmarshalJSON(b []byte) error {
var s string
if err := json.Unmarshal(b, &s); err != nil {
return err
}
dur, err := time.ParseDuration(s)
if err != nil {
return err
}
*d = Duration(dur)
return nil
}
但是,需要注意的是,變量 'd' 不應為 nil,否則可能會導致編組錯誤。或者,你還可以在函數開頭對 'd' 進行檢查。
10. 避免裸參數
當處理具有多個參數的函數時,僅通過閱讀其用法來理解每個參數的含義可能會令人困惑。考慮以下示例:
printInfo("foo", true, true)
如果不檢查 printInfo 函數,那么第一個 'true' 和第二個 'true' 的含義是什么呢?當你有一個具有多個參數的函數時,僅通過閱讀其用法來理解參數的含義可能會令人困惑。
但是,我們可以使用注釋使代碼更易讀。例如:
// func printInfo(name string, isLocal, done bool)
printInfo("foo", true /* isLocal */, true /* done */)
有些 IDE 也支持這個功能,可以在函數調用建議中顯示注釋,但可能需要在設置中啟用。
以上是我分享的一些實用技巧,但我不想讓文章過長,難以跟進,因為這些技巧與特定主題無關,涵蓋了各種類別。
如果你覺得這些技巧有用,或有自己的見解要分享,請隨時留言。我重視你的反饋,并樂于在回應此文章時點贊或推薦你的想法。