89.Go使用channel优雅的收集多协程的结果

文章目录

  • 一、基础写法:引入sync.WaitGroup基本使用
  • 二、进阶写法:使用结构体收集结果和错误
  • 三、进阶写法二:使用select收集结果和错误

一、基础写法:引入sync.WaitGroup基本使用

在日常工作中,使用协程提高处理效率是非常常见的技巧,这时候我们需要同时执行多个协程,然后再根据其结果再进行处理,这时候收集多个协程的值就非常关键。

相信稍有经验的程序员都能写出如下代码

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    ch := make(chan int)

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            fmt.Printf("Goroutine %d is working\n", id)
            ch <- id // Send a signal to the channel when done
        }(i)
    }

    // Wait for all Goroutines to signal completion
    go func() {
        wg.Wait()
        close(ch) // Close the channel when all Goroutines are done
    }()

    for id := range ch {
        fmt.Printf("Received signal from Goroutine %d\n", id)
    }

    fmt.Println("All Goroutines have finished.")
}

在这个示例中,我们有三个 Goroutines 执行工作,并往一个通道ch中发送信息。而另一个独立的 Goroutine 则监听通道,使用 sync.WaitGroup 来等待所有 Goroutines 完成,以知道每个 Goroutine 何时完成其工作从而关闭ch

当然,大家更常见的写法可能是下面这样,区别在于sync.WaitGroupclose操作不单独开启协程,而是同步等待。

package main

import (
    "fmt"
    "sync"
)

func worker(id int, resultChan chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    result := id * 2
    resultChan <- result // Send the result to the channel
}

func main() {
    var wg sync.WaitGroup
    resultChan := make(chan int, 3)

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(i, resultChan, &wg)
    }

    wg.Wait() // Wait for all workers to finish
    close(resultChan) // Close the channel when all results are sent

    for result := range resultChan {
        fmt.Printf("Received result: %d\n", result)
    }
}

在这个示例中,三个工作 Goroutines 计算结果并将它们发送到一个通道。主 Goroutine 等待所有工作者完成,关闭通道,然后从通道中读取和处理结果。

这些示例说明了如何使用通道同步在 Go 中的各种并发编程场景中协调和同步 Goroutines。通道为 Goroutines 之间提供了一个强大的机制,使得编写行为可预测和可靠的并发程序变得更加容易。

二、进阶写法:使用结构体收集结果和错误

假如有这样一个场景,我们需要对一批用户中的每个用户执行某个操作,比如获取用户当月积分,需要所有用户都执行成功才算成功,有一个执行失败就应该整体返回错误(有点类似事务了,但实际场景不太一样,事务是多个操作要么都成功,要么都失败,而我这里例举的操作,是对同一批主体执行同一个操作)

// 定义结构体,用于保存每个主体的执行结果
type bizResult struct {
	ExecFailed bool
	ErrMsg     string
	Value      int64
}

func GetUserScore(userId int64) (score int64, err error) {
	// 假设涉及到一些查DB以及RPC调用等,可能还涉及到一些复杂计算,这为了简便直接写死100
	res := int64(100)
	return res,nil
}

func GetUserScores(userIds []int64) (scores int64, err error) {
	var totalScores int64
	bizResultChan := make(chan *bizResult, len(userIds))
	var wg sync.WaitGroup
	for _, userId := range userIds {
		wg.Add(1)
		go func(userId int64) {
			defer wg.Done()
			score, err := GetUserScore(userId)
			if err != nil {
				bizResultChan <- &bizResult{
					ExecFailed: true,
					ErrMsg:     fmt.Sprintf("get user score failed, userId: %v, err: %v", userId, err),
				}
				return
			}
			bizResultChan <- &bizResult{
				ExecFailed: false,
				Value:      score,
			}
		}(userId)
	}
	
	wg.Wait()
	close(bizResultChan)
	for brc := range bizResultChan {
		if brc == nil {
			continue
		}
		if brc.ExecFailed {
			msg := fmt.Sprintf("[GetUserScores] get GetUserScore failed, err: %v", brc.ErrMsg)
			return 0,fmt.Errorf(msg)
		}
		totalScores += brc.Value
	}
	return totalScores,nil
}

三、进阶写法二:使用select收集结果和错误

首先,假设我们有一个需求:接口调用时,接收到一个包含十个元素的列表。我们希望并发执行这十个任务,每个任务都会返回执行的结果和可能的异常。最后,我们要将返回的结果整合到一个切片列表中,然后一并返回。

为了实现这个需求,我们首先定义了一个结构体 Order 用于表示任务的信息:

type Order struct {
    Name string `json:"name"`
    Id   int    `json:"id"`
}

然后,我们决定并发执行十个任务,因此初始化了两个通道,一个用于接收任务的结果,另一个用于接收异常:

taskNum := 10
orderCh := make(chan Order, taskNum) // 用于接收返回的结果
errCh := make(chan error, taskNum)   // 用于接收返回的异常

接下来,我们创建了任务执行函数:

func processTask(task Task) {
    // 执行任务的逻辑
}

然后,我们启动了十个协程来执行这些任务,并使用 sync.WaitGroup 来等待它们完成:

var wg sync.WaitGroup

for i := 0; i < taskNum; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // 任务的执行逻辑
        processTask(task)// 注意这里task指参数,是伪代码
    }()
}

// 等待所有任务协程完成
wg.Wait()

接着,我们使用 for-select 结构从结果通道中接收执行结果:

orderList := make([]Order, taskNum)

for i := 0; i < taskNum; i++ {
    select {
    case order, ok := <-orderCh:
        if ok {
            orderList = append(orderList, order)
        }
    case err := <-errCh:
        if err != nil {
            return err // 在发现错误时,根据需求选择是继续执行还是返回错误
        }
    default:
        fmt.Println("done")
    }
}

最后,我们关闭了通道,表示不再发送任务:

close(orderCh)
close(errCh)

如果我们需要控制每个任务的执行时间,我们可以使用定时器来解决超时问题:

timeoutTime := time.Second * 3  // 超时时间
taskTimer := time.NewTimer(timeoutTime) // 初始化定时器

for i := 0; i < taskNum; i++ {
    select {
    case <-taskTimer.C:
        err := errors.New("task timeout")
        return err
    // 其他 case 分支处理任务的执行和结果接收
    }
    // 每次执行都需要重置定时器
    taskTimer.Reset(timeoutTime)
}

在协程内处理 panic 问题是很重要的,我们在协程内使用 defer 来捕获 panic

for i := 0; i < taskNum; i++ {
    wg.Add(1)
    go func() {
        defer func() {
            wg.Done()
            if r := recover(); r != nil {
                err := errors.New(fmt.Sprintf("System panic: %v", r))
                errCh <- err
                return
            }
        }()
        // 任务的执行逻辑
        processTask(task)// 注意这里task指参数,是伪代码
    }()
}

最后,如果我们需要保持任务执行结果的顺序,我们可以定义一个带序号的结构体,并通过带序号的通道接收结果:

type OrderWithSeq struct {
    Seq       int
    OrderItem Order
}

orderCh := make(chan OrderWithSeq, taskNum) // 用于接收带序号的结构体

在任务执行时,我们加入序号信息:

for i := 0; i < taskNum; i++ {
    i := i
    wg.Add(1)
    go func(i int) {
        defer wg.Done()
        // 任务的执行逻辑
        // 注意这里task指参数,是伪代码
        // res指返回值,也是伪代码,实际processTask应该还会返回err
        res := processTask(task)
        orderCh <- OrderWithSeq{
            Seq:       i,
            OrderItem: res,
        }
    }(i)
}

最后,在结果接收时,按照带序号的结构体进行排序:

orderSeqList := make([]OrderWithSeq, taskNum)

for i := 0; i < taskNum; i++ {
    select {
    case order, ok := <-orderCh:
        if ok {
            orderList = append(orderSeqList, order)
        }
    // 其他 case 分支处理异常等
    }
}

// 按原始顺序进行排序
sort.Sort(BySeq(orderSeqList))

这就是一个完整的批量执行任务的通用模板,注意它是伪代码,主要提供的是思路,根据实际需求和场景,我们需要调整其中的一些部分。

你可能感兴趣的:(go,golang,开发语言,后端)