Golang中闭包与Goroutine的结合使用

Golang中闭包与Goroutine的结合使用 我是Go语言的新手,带着许多其他语言的思维习惯。使用下面的代码,程序运行正确,count 的值是5。但如果我使用 go Even(arr)count 就变成了0。我原以为 Even(arr) 会形成一个闭包,捕获 count 变量,这样在goroutine(闭包)中处理时仍然可以访问并修改 count 的值到5。

我哪里理解错了?

var count = 0

func Even(arr []int) {
	for _, item := range arr {
		if item%2 == 0 {
			count++
		}
	}
}

func main() {
	arr := makeRange(1, 100000)
	Even(arr)
	fmt.Println(count)
}

更多关于Golang中闭包与Goroutine的结合使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

3 回复

感谢您花时间查看这个问题。

我特意尝试破坏这段代码,以展示共享变量可能引发的问题。

我之前不了解 WaitGroup 这个概念。感谢您介绍它。起初我很困惑为什么全局变量 count 总是零。现在我明白了。这是因为主函数在 goroutine 有机会更新全局变量之前就结束了。因此它保持为 0。

通过引入 WaitGroup,我能够向自己证明,共享全局变量的功能确实会出问题。正如我所预料的,count 是一个不稳定的值 🙂

func main() {
	arr := makeRange(1, 10000)
	firstHalf := arr[0 : len(arr)/2]
	secondHalf := arr[len(arr)/2:]

	wg := sync.WaitGroup{}
	wg.Add(2)

	go func() {
		Even(firstHalf)
		wg.Done()
	}()
	go func() {
		Even(secondHalf)
		wg.Done()
	}()

	wg.Wait()

	fmt.Println(count)

}

更多关于Golang中闭包与Goroutine的结合使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


首先,你的 Even 函数并没有对 count 形成闭包。count 是一个全局变量,mainEven 函数都可以访问它。

其次,虽然我理解你了解其他语言,因此我猜你只是想学习如何在 Go 中实现并发,但以下是我会如何实现你的程序:

我认为正确的方法是修改 Even 函数,使其不再接收切片并更新全局 count 变量,而是应该改成这样:

func Even(arr []int) int {
	count := 0
	for _, item := range arr {
		if item%2 == 0 {
			count++
		}
	}
	return count
}

然后你在 main 函数中这样使用它:

func main() {
	arr := makeRange(1, 100000)
	count := Even(arr)
	fmt.Println(count)
}

如果不做这个改动,假设你有另一个 goroutine 也在运行,并且使用你的 Even 函数来统计偶数值的数量,那么它们将共享同一个 count 变量,并且都会返回错误的值。以这种新方式编写函数,可以安全地并发使用,并且为我们设置了一个稍微不同的例子,我想用它来演示使用 goroutine 的动机:

go 关键字启动一个 goroutine 但不会等待它完成。当你想运行一个函数,然后在不等待(或者等待)该函数完成的情况下做其他事情时,你会想要使用 goroutine。在你的例子中,因为你只是统计偶数值的数量,然后只有在得到计数后才想打印出来,所以在另一个 goroutine 中运行 Even 不会带来任何好处(无论是性能上还是学习上)。不过,通过我上面所做的改动,你可以在多个 goroutine 中运行多个 Even 函数:

import "sync"

func main() {
    arr := makeRange(1, 100000)

    wg := sync.WaitGroup{}
    wg.Add(2)

    firstHalf, secondHalf := 0, 0

    go func() {
        firstHalf = Even(arr[:50000])
        wg.Done()
    }()

    go func() {
        secondHalf = Even(arr[50000:])
        wg.Done()
    }()

    wg.Wait()

    fmt.Println(firstHalf + secondHalf)
}

这个实现在两个 goroutine 中执行 Even 函数,并使用一个 sync.WaitGroup 来等待 goroutine 完成对切片两半部分中偶数的计数。

你的理解基本正确,闭包确实会捕获外部变量。但问题在于并发执行时,主goroutine和Even goroutine之间存在竞态条件

当你使用go Even(arr)时:

  1. 主goroutine继续执行,立即打印count
  2. Even goroutine在后台执行计数
  3. 两者同时访问count变量,没有同步机制

关键点:

  • 闭包确实捕获了count变量
  • 但goroutine是并发执行的,主goroutine可能在Even goroutine完成前就打印了结果

示例代码展示问题:

package main

import (
	"fmt"
	"time"
)

var count = 0

func Even(arr []int) {
	for _, item := range arr {
		if item%2 == 0 {
			count++
		}
	}
}

func makeRange(min, max int) []int {
	arr := make([]int, max-min+1)
	for i := range arr {
		arr[i] = min + i
	}
	return arr
}

func main() {
	arr := makeRange(1, 10)
	
	// 使用goroutine
	go Even(arr)
	
	// 主goroutine立即执行
	fmt.Println("Count before goroutine completes:", count) // 可能输出0
	
	// 等待goroutine完成
	time.Sleep(100 * time.Millisecond)
	fmt.Println("Count after waiting:", count) // 应该输出5
}

要正确同步,可以使用sync.WaitGroup

package main

import (
	"fmt"
	"sync"
)

var count = 0
var mu sync.Mutex

func Even(arr []int, wg *sync.WaitGroup) {
	defer wg.Done()
	for _, item := range arr {
		if item%2 == 0 {
			mu.Lock()
			count++
			mu.Unlock()
		}
	}
}

func main() {
	arr := makeRange(1, 10)
	var wg sync.WaitGroup
	
	wg.Add(1)
	go Even(arr, &wg)
	
	wg.Wait() // 等待goroutine完成
	fmt.Println(count) // 输出5
}

闭包版本:

func main() {
	arr := makeRange(1, 10)
	var wg sync.WaitGroup
	var mu sync.Mutex
	count := 0
	
	wg.Add(1)
	go func() {
		defer wg.Done()
		for _, item := range arr {
			if item%2 == 0 {
				mu.Lock()
				count++
				mu.Unlock()
			}
		}
	}()
	
	wg.Wait()
	fmt.Println(count) // 输出5
}

核心问题不是闭包机制,而是并发执行时缺乏同步。闭包正确捕获了变量,但你需要确保在读取结果前goroutine已经完成计算。

回到顶部