We often encounter some time-consuming tasks, and then need to get the results of the task processing for further processing. In Go, the first thing that comes to mind is the way of goroutine and wait group WG to accelerate the efficiency of concurrent processing. While it’s easy enough to write concurrent programs in GO, some people often fall into the trap without noticing. This article uses a simple example to tease out several stomping pits.
Simple example
In business scenarios, we often need to split multiple tasks or one task, and then do complex logic processing separately. Suppose we have 1 to 10 numbers representing each of these 10 tasks, and then we need to multiply each of these 10 numbers by 2 to represent logical processing.
A pit point
Guess what the following program outputs?
var (
wg = sync.WaitGroup{}
nums = []int{1.2.3.4.5.6.7.8.9.10})func task(num int) int {
return num * 2
}
func main(a) {
wg.Add(len(nums))
for _, num := range nums {
go func(a) {
defer wg.Done()
res := task(num)
fmt.Println(res)
}()
}
wg.Wait()
}
Copy the code
If your conclusion is “output 2,4,6,8,10,12,14,16,18,20 in uncertain order”, so congratulations you into the pit! The actual result of running the code is:
20 20 20 20 20 20 20 20 20Copy the code
Because by the time the goroutine in the for loop is actually run, the loop has finished executing, and the num value is the last value after the loop, 20. The solution to this problem is simple, as it is in many languages, to have each go func() hold its num value by itself in a closure.
This most basic pit, although can run, but the editor will often have a hint
Correct code is as follows, can not determine the order of the output 2,4,6,8,10,12,14,16,18,20:
var (
wg = sync.WaitGroup{}
nums = []int{1.2.3.4.5.6.7.8.9.10})func task(num int) int {
return num * 2
}
func main(a) {
wg.Add(len(nums))
for _, num := range nums {
go func(num int) {
defer wg.Done()
res := task(num)
fmt.Println(res)
}(num)
}
wg.Wait()
}
Copy the code
Pit point 2
In the example above, some people with strict memory management requirements might automatically change it to this:
var (
wg = sync.WaitGroup{}
nums = []int{1.2.3.4.5.6.7.8.9.10})func task(num int) int {
return num * 2
}
func main(a) {
wg.Add(len(nums))
for _, num := range nums {
go func(num *int) {
defer wg.Done()
res := task(*num)
fmt.Println(res)
}(&num)
}
wg.Wait()
}
Copy the code
So what’s going to be the output? Not necessarily, but most values are 20:
20 20 20 20 20 8 20 20 20 20 20Copy the code
With closures, why not pass Pointers? Because of closures, go func() holds the same memory address, that is, &num refers to the same memory address in the for loop, but the value stored in that address is constantly changing in the for, and by the time the goroutine actually executes, the &num value has almost always been changed to the last value, 20. So you can’t pass a pointer at this point.
Now, some people might say, is it silly, a simple int, why would you pass a pointer? This could go into a pit. In real life, no one would do this, but the example in this article is too simple. What if num was not int? When writing code, it’s often a complex structure, such as a model defined in an ORM. I’m sure someone will pass &model!
Point three
Following the example above, if we want to save the result after processing, it is natural to write the following code:
var (
wg = sync.WaitGroup{}
nums = []int{1.2.3.4.5.6.7.8.9.10}
results []int
)
func task(num int) int {
return num * 2
}
func main(a) {
wg.Add(len(nums))
for _, num := range nums {
go func(num int) {
defer wg.Done()
res := task(num)
results = append(results, res)
}(num)
}
wg.Wait()
fmt.Println(results)
}
Copy the code
What is the results output? If you to the conclusion that the results contained in the order of 2,4,6,8,10,12,14,16,18,20, so congratulations you into the pit again! Normally, len(results) would have a value of 10, but running the above code a few more times shows that len(results) is almost always less than 10. Because in GO, the slice slice type is non-concurrent safe, which means that multiple values are inserted at the same time in a location in results, resulting in data loss. The solution can be through locking:
var (
wg = sync.WaitGroup{}
nums = []int{1.2.3.4.5.6.7.8.9.10}
results []int
lock = sync.RWMutex{}
)
func task(num int) int {
return num * 2
}
func main(a) {
wg.Add(len(nums))
for _, num := range nums {
go func(num int) {
defer wg.Done()
res := task(num)
lock.Lock()
results = append(results, res)
lock.Unlock()
}(num)
}
wg.Wait()
fmt.Println(results)
}
Copy the code
Similar to the slice type, the MAP type in GO is also non-concurrent safe and can be replaced with sync. map in concurrent scenarios.
summary
Just like that single-celled creature, the more simple it is, the more annoying it is! The above three step pit point is very simple, most people may be with me the same mentality: “so simple problem I will not enter the pit!” . However, I think I have written the go code is very skilled, but did not want to be crazy hit face in the business code recently, waste a lot of time! Probably in terms of focusing on the code itself, I think very few people fall into the pit. This makes sense, but in reality, when our thinking is always focused on how complex business logic organizes the code implementation, we tend to lose sight of these details in the long, long lines of code. This is the commandment.