Go 語言中嘗試延遲執行一個函數

Go 編程語言提供了豐富的特性,使得像 Google 這樣的大型公司能夠高效地進行軟件開發。它爲許多雲服務提供商和分佈式服務的底層基礎設施提供支持,同時保持了簡單易學的特點。

在 Go 中,我們可以根據需要使用指針類型和值類型。在本文中,我們將探討一個有趣的使用場景。

函數調用中的切片和映射傳遞

通常,在 Go 中調用函數時,切片(slice)和映射(map)並不是通過指針傳遞的,而是通過值傳遞的。以下是一個示例:

func Sum(items []int) int {  
  var res int  
  for _, v := range items {  
    res += v  
  }  
  return res  
}

在上述代碼中,函數Sum通過值傳遞參數items。我們可以通過以下代碼驗證這一點:

package main

import "fmt"

func main() {  
  items := []int{1, 2, 3, 4}  
  Sum(items)  
  fmt.Println(items)  
}

func Sum(items []int) int {  
  var res int  
  for _, v := range items {  
    res += v  
  }  
  items = []int{}  
  return res  
}

運行結果如下:

[1 2 3 4]

可以注意到,即使在Sum函數內部重新初始化了items,傳入的items並沒有發生變化。

實際上,在我個人的實踐中,只有在極少數情況下會使用指向切片的指針。接下來,我們將描述一個這樣的場景。

使用指針解決延遲調用中的問題

假設我們需要從存儲中加載一些數據,對它們進行處理,並在處理完成後刪除這些數據。以下是一個示例:

type Fetcher struct {}

func (f *Fetcher) Load(ctx context.Context) ([]int, error) {
  // 從存儲中加載數據
  ...
}

func (f *Fetcher) Delete(ctx context.Context, values []int) error {
  // 刪除數據
  ...
}

現在,我們可以使用Fetcher加載數據,並在處理完成後釋放它們:

func Process(ctx context.Context, f *Fetcher) error {  
  values, err := f.Load(ctx)  
  if err != nil {  
    return fmt.Errorf("failed to load values: %v", err)  
  }  
  
  // 使用完數據後刪除它們
  // 這是一個bug
  defer f.Delete(ctx, values)

  for _, v := range values {  
    // 進行一些處理,例如通過RPC發送數據
    log.InfoContextf(ctx, "Processing value: %v", v)  
    if err := SendForProcessingOverRPC(ctx, v); err != nil {  
      return err  
    }  
  }

  return nil // 一切正常,無錯誤
}

在上述代碼中,我們引入了一個 bug:即使某些數據未被成功處理,所有數據仍會被刪除。例如,如果SendForProcessingOverRPC函數在處理某個值時失敗,剩餘的數據將不會被處理,但它們仍然會被刪除。

我們希望能夠僅刪除那些已成功處理的數據,這樣才能避免上述問題。

修復嘗試:跟蹤已處理的數據

我們可以通過跟蹤已成功處理的數據來解決這個問題。例如:

func Process(ctx context.Context, f *Fetcher) error {  
  values, err := f.Load(ctx)  
  if err != nil {  
    return fmt.Errorf("failed to load values: %v", err)  
  }  
  
  var toDelete []int  
  // 使用完數據後刪除它們
  // 這仍然是一個bug
  defer f.Delete(ctx, toDelete)

  for _, v := range values {  
    // 進行一些處理,例如通過RPC發送數據
    log.InfoContextf(ctx, "Processing value: %v", v)  
    if err := SendForProcessingOverRPC(ctx, v); err != nil {  
      return err  
    }  

    toDelete = append(toDelete, v)  
  }

  return nil // 一切正常,無錯誤
}

在這個版本中,我們使用toDelete變量跟蹤已成功處理的數據,並將其傳遞給f.Delete函數。然而,這裏仍然存在一個 bug:實際上,沒有任何數據會被刪除。

原因在於,defer f.Delete(ctx, toDelete)捕獲的是toDelete變量的初始值(即空切片)。因此,當延遲調用發生時,toDelete仍然是空的。

解決方案:使用指向切片的指針

如果我們能夠捕獲toDelete的指針,就可以實現預期的效果:

func Process(ctx context.Context, f *Fetcher) error {  
  values, err := f.Load(ctx)  
  if err != nil {  
    return fmt.Errorf("failed to load values: %v", err)  
  }  
  
  var toDelete []int  
  // 使用完數據後刪除它們
  // 現在可以正常工作
  defer func(items *[]int) {  
    f.Delete(ctx, *items)  
  }(&toDelete)

  for _, v := range values {  
    // 進行一些處理,例如通過RPC發送數據
    log.InfoContextf(ctx, "Processing value: %v", v)  
    if err := SendForProcessingOverRPC(ctx, v); err != nil {  
      return err  
    }  

    toDelete = append(toDelete, v)  
  }

  return nil // 一切正常,無錯誤
}

在這個版本中,延遲調用的匿名函數接收了toDelete的指針,因此隨着切片內容的更新,延遲函數也能夠訪問到最新的值。

總結

通過對切片和延遲調用函數的探索,我們揭示了 Go 內存模型中的一個細微之處。儘管 Go 在大多數情況下能夠無縫地處理內存管理,但在某些場景下,理解切片的值傳遞和指針使用的影響至關重要。

通過認識切片機制、延遲執行和閉包之間的相互作用,我們可以避免意料之外的行爲,從而編寫更健壯、更可靠的 Go 程序。雖然在本例中使用指針解決了問題,但優先考慮清晰且可維護的代碼,例如返回切片或批量處理數據,也可以進一步提升代碼質量。

掌握這些細節將使你能夠更有效地利用 Go 的高效性和表達能力,尤其是在開發大規模應用時。繼續深入探索 Go 語言的奧祕,編寫出卓越的代碼吧!

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