10 個提高生產力的 Go 小技巧

最近 Phuong Le 大佬針對日常開發 Go 項目時,總結了一些好用的 Go 小技巧。

看了後,感覺對於剛入門 Go 的同學有一定的學習價值。可以挑好的學。應用到自己項目裏。以下內容分享給大家。

在開發 Go 生產項目時,我發現自己經常重複編寫代碼和使用某些技術,直到後來回顧自己的工作時才意識到這一點。

下面是從總結經驗中挑選的一些有用的代碼片段,希望對大家有所幫助。

  1. 計時技巧

如果你對跟蹤函數的執行時間感興趣,或者在排查問題時需要使用。

可以在 Go 中可以使用 defer 關鍵字,只需一行代碼即可實現一個非常簡單、高效的技巧。

你只需要一個 TrackTime 函數:

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()) // <-- 就是這裏

  time.Sleep(500 * time.Millisecond)
}

// elapsed: 501.11125ms

1.5 兩階段 Defer

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)
}
  1. 預先分配切片

我們在編寫程序時,可以有意識的預先分配或映射切片,可以顯著提高我們的 Go 程序的性能。

如下例子:

// 而不是這樣
a := make([]int, 10)
a[0] = 1

// 這樣使用
b := make([]int, 0, 10)
b = append(b, 1)
  1. 鏈式調用

鏈式調用技術可以應用於函數(指針)接收者。

我們考慮一個具有兩個函數 AddAge 和 Rename 的 Person 結構體,這兩個函數可以用來修改 Person 的字面值。

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: 35}

  p.AddAge()
  p.Rename("煎魚")
}

或者,我們可以修改 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("腦子進煎魚了")
  1. Go 1.20 支持將切片解析爲數組或數組指針

當我們需要將切片轉換爲固定大小的數組時,我們不能像這樣直接賦值:

a := []int{0, 1, 2, 3, 4, 5}
var b [3]int = a[0:3]

// cannot use a[0:3] (value of type []int) as [3]int value in variable
// declaration compiler(IncompatibleAssign)

爲了將切片轉換爲數組,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]
}
  1. 使用 _ import 進行包初始化

在庫中,你可能會看到像這樣帶有下劃線 _ 的 import 語句:

import (
  _ "google.golang.org/genproto/googleapis/api/annotations"
)

這將會執行包的初始化代碼(init 函數),不會爲它創建包的名稱引用。

功能上來講,這允許你在運行代碼之前初始化包,註冊連接並執行其他任務。

這是一個例子,以便於我們更好地理解它的工作原理:

// 下劃線包
package underscore

func init() {
    // 初始化代碼
}

這種方式允許我們在不直接使用包的情況下,執行包的初始化代碼。

這在需要進行一些設置或註冊操作時非常有用

  1. 使用點 . 操作符導入包

點(.)操作符可以用來使導入包的導出標識符在不必指定包名的情況下可用,這對懶惰的開發者來說是一個有用的捷徑。

這在處理項目中的長包名稱(例如 externalmodeldoingsomethinglonglib)時特別有用。

爲了演示,這裏有一個簡短的示例:

package main

import (
  "fmt"
  . "math"
)

func main() {
  fmt.Println(Pi) // 3.141592653589793
  fmt.Println(Sin(Pi / 2)) // 1
}
  1. Go 1.20 起可以將多個錯誤包裝成一個錯誤

Go 1.20 引入了錯誤包的新特性(小修小補),包括對多個錯誤包裝的支持和對 errors.Iserrors.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 函數關聯追加。

這樣就不需要自己手動管理數組。大大簡化了錯誤處理過程。

  1. 編譯時檢查接口的技巧

假設有一個名爲 Buffer 的接口,其中包含一個 Write() 函數。此外,還有一個名爲 StringBuffer 的結構體實現了這個接口。

但是,如果你打錯了字,寫的是 Writeee(),而不是 Write() 呢?

type Buffer interface {
  Write([]byte) (n int, err error)
}

type StringBuffer struct{}

func (s *StringBuffer) Writeee([]byte) (n int, err error) {
  return 0, nil
}

在運行之前,您無法檢查 StringBuffer 是否正確實現了 Buffer 接口。

通過使用下面這個技巧,編譯器會通過 IDE 錯誤信息提醒您:

var _ Buffer = (*StringBuffer)(nil)

// cannot use (*StringBuffer)(nil) (value of type *StringBuffer)
// as Buffer value in variable declaration: *StringBuffer
// does not implement Buffer (missing method Write)
  1. 三元運算符

Go 不像許多其他編程語言那樣有內置對三元運算符的支持。

Python:

min = a if a < b else b

C#:

min = x < y ? x : y

Go 在 1.18 中引入了泛型功能,現在我們可以創建一個實用工具,只需一行代碼即可實現類似於三元表達式的功能:

// our utility
func Ter[T any](cond bool, a, b T "T any") T {
  if cond {
    return a
  }

  return b
}

func main() {
  fmt.Println(Ter(true, 1, 2)) // 1
  fmt.Println(Ter(false, 1, 2)) // 2
}
  1. 驗證接口是否真的爲 nil 的方法

即使接口的值爲 nil,也不一定意味着接口本身就是 nil。這可能會導致 Go 程序中出現意想不到的錯誤。

知道如何檢查接口是否爲 nil 是很重要的。

func main() {
  var x interface{}
  var y *int = nil
  x = y

  if x != nil {
    fmt.Println("x != nil") // <-- actual
  } else {
    fmt.Println("x == nil")
  }

  fmt.Println(x)
}

// x != nil
// <nil>

我們如何確定 interface{} 值是否爲空?

通過下述方法可以實現這一個訴求:

func IsNil(x interface{}) bool {
  if x == nil {
    return true
  }

  return reflect.ValueOf(x).IsNil()
}

總結

這些開發技巧不限具體的分類,對於大家在日常開發中能有一些 tips 的作用。

平時我經常看到有同學爲了統計函數執行時間,就一條條打日誌,打開始和結束時間。顯得比較繁瑣。

大家可以結合起來,在平時開發時,也可以及時總結這類方法論。會比較有幫助!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/GG3QbKQz3wYKFPdmJjWtuA