Skip to main content

Command Palette

Search for a command to run...

Eng Ver. Common mistakes with for loops in Go

Updated
6 min read

For loop + Goroutine + Closure

The program provided has the following code:

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
            fmt.Println(v)
            done <- true
        }()
    }

    // wait for all goroutines to complete before exiting
    for _ = range values {
        <-done
    }
}

At first glance, one would expect the output to be "a, b, c". However, due to the unpredictable execution of Goroutines, they might all end up printing "c" instead of iterating through each value. This happens because Goroutines might execute after the next loop iteration, and the value of "v" might change by then.

Furthermore, the loop variable "v" in Go's for loop is shared, thus they all point to the same memory address. To prove this, there's another code segment:

func main() {
    done := make(chan bool)

    values := []string{"a", "b", "c"}
    for _, v := range values {
        go func() {
            fmt.Printf("value=%s, addr=%p\n", v, &v)
            done <- true
        }()
    }

    // wait for all goroutines to complete before exiting
    for _ = range values {
        <-done
    }
}

/*
value=c, addr=0xc000014070
value=c, addr=0xc000014070
value=c, addr=0xc000014070
*/

There are several solutions to this issue:

  1. Make a copy of the value during each iteration.

     func main() {
         done := make(chan bool)
    
         values := []string{"a", "b", "c"}
         for _, v := range values {
             v1 := v
             go func() {
                 fmt.Println(v1)
                 done <- true
             }()
         }
    
         // wait for all goroutines to complete before exiting
         for _ = range values {
             <-done
         }
     }
    
  2. Pass the value as a parameter to the closure function, thus creating a copy of the value.

     func main() {
         done := make(chan bool)
    
         values := []string{"a", "b", "c"}
         for _, v := range values {
             go func(v string) {
                 fmt.Println(v)
                 done <- true
             }(v)
         }
    
         // wait for all goroutines to complete before exiting
         for _ = range values {
             <-done
         }
     }
    

Go Tool Vet

This is a common interview question. Luckily, Go provides a static code analysis tool called "go vet". The tool checks for various issues, including the one with loop variables in nested functions. By running "go vet", one can identify potential issues like this.

To list the available checks, run "go tool vet help":
* asmdecl      report mismatches between assembly files and Go declarations
* assign       check for useless assignments
* atomic       check for common mistakes using the sync/atomic package
* bools        check for common mistakes involving boolean operators
* buildtag     check that +build tags are well-formed and correctly located
* cgocall      detect some violations of the cgo pointer passing rules
* composites   check for unkeyed composite literals
* copylocks    check for locks erroneously passed by value
* httpresponse check for mistakes using HTTP responses
* loopclosure  check references to loop variables from within nested functions
* lostcancel   check cancel func returned by context.WithCancel is called
* nilfunc      check for useless comparisons between functions and nil
* printf       check consistency of Printf format strings and arguments
* shift        check for shifts that equal or exceed the width of the integer
* slog         check for incorrect arguments to log/slog functions
* stdmethods   check signature of methods of well-known interfaces
* structtag    check that struct field tags conform to reflect.StructTag.Get
* tests        check for common mistaken usages of tests and examples
* unmarshal    report passing non-pointer or non-interface values to unmarshal
* unreachable  check for unreachable code
* unsafeptr    check for invalid conversions of uintptr to unsafe.Pointer
* unusedresult check for unused results of calls to some functions

You can view the usage instructions with "go tool vet help". Let me use "go tool vet" to scan and see.

go vet main.go
or
go vet -loopclosure main.go

/*
# command-line-arguments
./main.go:11:38: loop variable v captured by func literal
./main.go:11:42: loop variable v captured by func literal
*/

Seeing the message "loop variable v captured by func literal" alerts you that in line 11, the loop variable "v" is shared and is being used by the closure function. Once we modify the code using the aforementioned methods, running "go vet" will no longer display the mentioned warning.

Alright, the problem in the previous example was relatively easy to spot. Next, let's explore a more subtle code pattern where this pitfall is easy to overlook.

For loop + Address Operator &

Interestingly, even without closure functions, one can encounter similar issues. For instance: Reference

package main

import "fmt"

func main() {
    var out []*int
    for i := 0; i < 3; i++ {
        out = append(out, &i)
    }
    fmt.Println("Values:", *out[0], *out[1], *out[2])
    fmt.Println("Addresses:", out[0], out[1], out[2])
}

/*
Values: 3 3 3
Addresses: 0xc0000120e8 0xc0000120e8 0xc0000120e
*/

Huh, there's no closure function this time, yet the problem persists? Let's give "go vet" a try! go vet main.go

Nothing came up, huh? The results clearly aren't as expected, but "go vet" seems to think everything's fine! (Come on, go vet!)

In reality, the "i" in the for loop for each iteration is pointing to the same shared variable. To make matters worse, we're taking the address of &i and appending it to the "out" pointer slice. By the time the last iteration happens, "i" truly points to the value 3. Since they all share the same memory address, that's why the output is consistently 3.

Let's take a look at the publicly documented issue at "publicly documented issue at Lets Encrypt" and discuss a segment of the code inside.

// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a
// protobuf authorizations map
func authz2ModelMapToPB(m map[string]authz2Model) (*sapb.Authorizations, error) {
    resp := &sapb.Authorizations{}
    for k, v := range m {
        // Make a copy of k because it will be reassigned with each loop.
        kCopy := k
        authzPB, err := modelToAuthzPB(&v)
        if err != nil {
            return nil, err
        }
        resp.Authz = append(resp.Authz, &sapb.Authorizations_MapElement{
            Domain: &kCopy,
            Authz: authzPB,
        })
    }
    return resp, nil
}

In line 7, a copy of "k" is made. This is because, if it wasn't copied here, the "k" at this moment would also be a shared variable. Using &k in line 13 would result in the aforementioned unexpected error.

Go 1.22 is finally addressing this common pitfall, as mentioned in "Fixing For Loops in Go 1.22".

But 1.22 hasn't been officially released yet, right? No worries, as mentioned in the article, 1.21 provides a preview of this new feature. All you have to do is enable it with the GOEXPERIMENT=loopvar flag.

GOEXPERIMENT=loopvar  go run main.go

/*
Values: 0 1 2
Addresses: 0xc0000120e8 0xc000012110 0xc000012118
*/

Perfect!

Summary

To sum up, In versions prior to Go 1.21, whenever a for loop takes the address of an iterating variable or when combining a for loop with a closure, it's crucial for code reviews to alert each other. Leveraging tools like "go vet" during Continuous Integration (CI) or using Git hooks like "husky" can help in detecting these basic errors. However, I believe Go 1.22 is a version well worth upgrading to, given how often this pitfall is encountered.

References

Fixing For Loops in Go 1.22

What happens with closures running as goroutines?

Let's Encrypt: CAA Rechecking bug

CommonMistakes

335 views

More from this blog

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read162
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

工程師的 Claude Code 實戰指南:從零開始到高效開發

工程師的 Claude Code 實戰指南:從零開始到高效開發 本文整合 Anthropic 官方 Best Practices 與社群實戰 Tips,帶你由淺入深掌握 Claude Code。 什麼是 Claude Code?為什麼值得學? 如果你還在用「複製程式碼貼到 ChatGPT,再複製答案貼回去」的工作流程,Claude Code 會讓你大開眼界。 Claude Code 是 Anthropic 推出的命令列工具,它直接活在你的 terminal 裡,能夠讀懂你的整個 codeb...

Feb 18, 20265 min read72
工程師的 Claude Code 實戰指南:從零開始到高效開發

System Design Interview Ch 12 Digital Wallet

確立問題與設計範疇 角色對話內容 面試者我們應該只關注兩個數位錢包之間的餘額轉帳操作嗎?我們是否需要擔心其他功能? 面試官讓我們只關注餘額轉帳操作。 面試者該系統需要支援多少 TPS(每秒交易次數)? 面試官讓我們假設是 1,000,000 TPS (每秒 100 萬次交易)。 面試者數位錢包對正確性有嚴格的要求。我們可以假設事務保證 就足夠了嗎? 面試官聽起來不錯。 面試者我們需要證明正確性嗎? 面試官這是一個很好的問題。正確性(Correctness)通常只有在交...

Feb 2, 202610 min read190
System Design Interview Ch 12 Digital Wallet

Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦

在現代 AI 輔助開發中,我們不僅需要 AI 寫程式,更需要它懂規則、記性好,並且能自動處理那些繁瑣的雜事。透過 Claude Code Hooks 機制,我們可以介入 AI 的思考與執行迴圈,實現真正的「人機協作自動化」。 一、 動機與痛點:為什麼你需要介入 AI 的生命週期? 在預設狀態下,Claude Code 雖然強大,但它是「被動」且「無狀態」的,這導致了開發者常遇到以下痛點: 記憶重置 (Session Amnesia): 痛點:每次重啟終端機,AI 就像失憶一樣。 解法:你...

Jan 24, 20266 min read441
Claude Code 利用 Event-Driven Hooks 打造自動化開發大腦
M

MicroFIRE

71 posts