Go Wiki: 程式碼審查:Go 併發

本頁面是對 Go 程式碼審查註釋 列表的補充。此列表的目的是幫助在審查 Go 程式碼時發現併發相關的錯誤。

您也可以只通讀一遍此列表,以重新整理您的記憶,並確保您瞭解所有這些併發陷阱。

⚠️ 本頁面由社群編寫和維護。其中包含有爭議的資訊,可能具有誤導性或不正確。


同步不足和競態條件

測試

可伸縮性

時間


同步不足和競態條件

# RC.1. **HTTP 處理函式是否可以安全地從多個 goroutine 同時呼叫?** 很容易忽略 HTTP 處理函式應該是執行緒安全的,因為它們通常不在專案程式碼的任何地方被顯式呼叫,而僅從 HTTP 伺服器的內部呼叫。

# RC.2. 是否存在**未受互斥鎖保護的欄位或變數訪問**,而該欄位或變數是原始型別或非顯式執行緒安全的型別(如 atomic.Value),同時該欄位可以從併發的 goroutine 中更新?即使是對原始變數進行讀取操作,如果不進行同步也是不安全的,因為存在非原子硬體寫入和潛在的記憶體可見性問題。

另請參閱 典型資料競態:未受保護的原始變數

# RC.3. **執行緒安全型別的*方法*是否不返回受保護結構的指標?** 這是一個微妙的錯誤,會導致前一項中描述的未受保護的訪問問題。示例

type Counters struct {
    mu   sync.Mutex
    vals map[Key]*Counter
}

func (c *Counters) Add(k Key, amount int) {
    c.mu.Lock()
    defer c.mu.Unlock()
    count, ok := c.vals[k]
    if !ok {
        count = &Counter{sum: 0, num: 0}
        c.vals[k] = count
    }
    count.sum += amount
    count.n += 1
}

func (c *Counters) GetCounter(k Key) *Counter {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.vals[k] // BUG! Returns a pointer to the structure which must be protected
}

一種可能的解決方案是在 GetCounter() 中返回該結構的副本,而不是指標。

type Counters struct {
    mu   sync.Mutex
    vals map[Key]Counter // Note that now we are storing the Counters directly, not pointers.
}

...

func (c *Counters) GetCounter(k Key) Counter {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.vals[k]
}

# RC.4. 如果有多個 goroutine 可以更新 sync.Map,**您是否不呼叫 m.Store()m.Delete() 來依賴於之前 m.Load() 呼叫的成功?** 換句話說,以下程式碼是有競態的

var m sync.Map

// Can be called concurrently from multiple goroutines
func DoSomething(k Key, v Value) {
    existing, ok := m.Load(k)
    if !ok {
        m.Store(k, v) // RACE CONDITION - two goroutines can execute this in parallel
        ... some other logic, assuming the value in `k` is now `v` in the map
    }
    ...
}

在某些情況下,這種競態條件可能是良性的:例如,Load()Store() 呼叫之間的邏輯計算要快取到 map 中的值,而該計算始終返回相同的結果且沒有副作用。

⚠️ **潛在的誤導性資訊**。“競態條件”可以指邏輯錯誤,例如這個例子,它可以是良性的。但這個短語也常用於指違反記憶體模型,這永遠不是良性的。

如果競態條件不是良性的,請使用 sync.Map.LoadOrStore()LoadAndDelete() 方法來修復它。

可伸縮性

# Sc.1. **故意建立一個零容量的通道(channel)是否是故意的**,例如 make(chan *Foo)?向零容量通道傳送訊息的 goroutine 會一直阻塞,直到另一個 goroutine 接收到該訊息。在 make() 呼叫中省略容量可能只是一個錯誤,它會限制程式碼的可伸縮性,並且單元測試很可能找不到這種錯誤。

⚠️ **誤導性資訊**。緩衝通道本身並不比無緩衝通道更能提高“可伸縮性”。但是,緩衝通道很容易掩蓋死鎖和其他基本設計錯誤,而這些錯誤在使用無緩衝通道時會立即顯現。

# Sc.2. 與純粹的 sync.Mutex 相比,使用 RWMutex 進行鎖定會產生額外的開銷,此外,Go 中 RWMutex 的當前實現可能存在一些 可伸縮性問題。除非情況非常明確(例如,使用 RWMutex 同步許多每次持續數百毫秒或更長時間的只讀操作,並且需要獨佔鎖的寫入操作很少發生),**否則應該有一些基準測試證明 RWMutex 確實有助於提高效能。** 一個典型的例子,其中 RWMutex 肯定弊大於利,就是對結構體中變數的簡單保護。

type Box struct {
    mu sync.RWMutex // DON'T DO THIS -- use a simple Mutex instead.
    x  int
}

func (b *Box) Get() int {
    b.mu.RLock()
    defer b.mu.RUnlock()
    return b.x
}

func (b *Box) Set(x int) {
    b.mu.Lock()
    defer b.mu.Unlock()
    b.x = x
}

時間

# Tm.1. **是否使用 defer tick.Stop() 停止 time.Ticker?** 當使用該 tick 的函式在迴圈中返回時,不停止 tick 會導致記憶體洩漏。

# Tm.2. **是否使用 Equal() 方法而不是簡單的 == 來比較 time.Time 結構體?** 引用 time.Time 的文件

請注意,Go 的 == 運算子不僅比較時間點,還比較 Location 和單調時鐘讀數。因此,在確保所有值都設定了相同的 Location(可以透過使用 UTC()Local() 方法實現)並且已剝離單調時鐘讀數(透過設定 t = t.Round(0))之前,不應將 Time 值用作 map 或資料庫的鍵。總的來說,優先使用 t.Equal(u) 而不是 t == u,因為 t.Equal() 使用最準確的可比性,並正確處理只有其引數之一具有單調時鐘讀數的情況。

# Tm.3. **在呼叫 time.Since(t) 之前,是否*沒有*從 t 中剝離單調分量?** 這是前一項的後果。如果在將 time.Time 結構體傳遞給 time.Since() 函式之前剝離了單調分量(透過呼叫 UTC()Local()In()Round()Truncate()AddDate()),則 time.Since() 的結果在極少數情況下(例如,如果系統時間在原始獲取開始時間與呼叫 time.Since() 之間透過 NTP 同步)**可能會為負數**。如果*沒有*剝離單調分量,time.Since() 將始終返回正持續時間。

# Tm.4. **如果您想透過 t.Before(u) 比較*系統時間*,是否會從引數中剝離單調分量**,例如透過 u.Round(0)?這是與 Tm.2 相關的另一個問題。有時,您需要專門僅按其中儲存的系統時間來比較兩個 time.Time 結構體。例如,當將一個 Time 結構體儲存到磁碟或透過網路傳送時,您可能需要這樣做。例如,設想某種遙測代理會定期將遙測指標連同時間一起推送到某個遠端系統。

var latestSentTime time.Time

func pushMetricPeriodically(ctx context.Context) {
    t := time.NewTicker(time.Second)
    defer t.Stop()
    for {
        select {
        case <-ctx.Done: return
        case <-t.C:
            newTime := time.Now().Round(0) // Strip monotonic component to compare system time only
            // Check that the new time is later to avoid messing up the telemetry if the system time
            // is set backwards on an NTP sync.
            if latestSentTime.Before(newTime) {
                sendOverNetwork(NewDataPoint(newTime, metric()))
                latestSentTime = newTime
            }
        }
    }
}

如果不呼叫 Round(0),即剝離單調分量,此程式碼將是錯誤的。

閱讀列表

Go 程式碼審查註釋:Go 程式碼審查的清單,非併發特定。

Go 併發

併發,但不限於 Go


此內容是 Go Wiki 的一部分。