golang中的errgroup
0.1、索引
https://waterflow.link/articles/1665239900004
1、串行执行
假如我们需要查询一个课件列表,其中有课件的信息,还有课件创建者的信息,和课件的缩略图信息。但是此时我们已经对服务做了拆分,假设有课件服务和用户服务还有文件服务。
我们通常的做法是,当我们查询课件列表时,我们首先调用课件服务,比如查询10条课件记录,然后获取到课件的创建人ID,课件的缩略图ID;再通过这些创建人ID去用户服务查询用户信息,通过缩略图ID去文件服务查询文件信息;然后再写到这10条课件记录中返回给前端。
像下面这样:
package main
import (
"fmt"
"time"
)
type Courseware struct {
Id int64
Name string
Code string
CreateId int64
CreateName string
CoverId int64
CoverPath string
}
type User struct {
Id int64
Name string
}
type File struct {
Id int64
Path string
}
var coursewares []Courseware
var users map[int64]User
var files map[int64]File
var err error
func main() {
// 查询课件
coursewares, err = CoursewareList()
if err != nil {
fmt.Println("获取课件错误")
return
}
// 获取用户ID、文件ID
userIds := make([]int64, 0)
fileIds := make([]int64, 0)
for _, courseware := range coursewares {
userIds = append(userIds, courseware.CreateId)
fileIds = append(fileIds, courseware.CoverId)
}
// 批量获取用户信息
users, err = UserMap(userIds)
if err != nil {
fmt.Println("获取用户错误")
return
}
// 批量获取文件信息
files, err = FileMap(fileIds)
if err != nil {
fmt.Println("获取文件错误")
return
}
// 填充
for i, courseware := range coursewares {
if user, ok := users[courseware.CreateId]; ok {
coursewares[i].CreateName = user.Name
}
if file, ok := files[courseware.CoverId]; ok {
coursewares[i].CoverPath = file.Path
}
}
fmt.Println(coursewares)
}
func UserMap(ids []int64) (map[int64]User, error) {
time.Sleep(3 * time.Second) // 模拟数据库请求
return map[int64]User{
1: {Id: 1, Name: "liu"},
2: {Id: 2, Name: "kang"},
}, nil
}
func FileMap(ids []int64) (map[int64]File, error) {
time.Sleep(3 * time.Second) // 模拟数据库请求
return map[int64]File{
1: {Id: 1, Path: "/a/b/c.jpg"},
2: {Id: 2, Path: "/a/b/c/d.jpg"},
}, nil
}
func CoursewareList() ([]Courseware, error) {
time.Sleep(3 * time.Second)
return []Courseware{
{Id: 1, Name: "课件1", Code: "CW1", CreateId: 1, CreateName: "", CoverId: 1, CoverPath: ""},
{Id: 2, Name: "课件2", Code: "CW2", CreateId: 2, CreateName: "", CoverId: 2, CoverPath: ""},
}, nil
}
2、并发执行
但我们获取课件之后,填充用户信息和文件信息是可以并行执行的,我们可以修改获取用户和文件的代码,把他们放到协程里面,这样就可以并行执行了:
...
// 此处放到协程里
go func() {
// 批量获取用户信息
users, err = UserMap(userIds)
if err != nil {
fmt.Println("获取用户错误")
return
}
}()
// 此处放到协程里
go func() {
// 批量获取文件信息
files, err = FileMap(fileIds)
if err != nil {
fmt.Println("获取文件错误")
return
}
}()
...
但是当你执行的时候你会发现这样是有问题的,因为下面的填充数据的代码有可能会在这两个协程执行完成之前去执行。也就是说最终的数据有可能没有填充用户信息和文件信息。那怎么办呢?这是我们就可以使用golang的waitgroup了,主要作用就是协程的编排。
我们可以等2个协程都执行完成再去走下面的填充逻辑
我们继续修改代码成下面的样子
...
// 初始化一个sync.WaitGroup
var wg sync.WaitGroup
func main() {
// 查询课件
...
// 获取用户ID、文件ID
...
// 此处放到协程里
wg.Add(1) // 计数器+1
go func() {
defer wg.Done() // 计数器-1
// 批量获取用户信息
users, err = UserMap(userIds)
if err != nil {
fmt.Println("获取用户错误")
return
}
}()
// 此处放到协程里
wg.Add(1) // 计数器+1
go func() {
defer wg.Done() // 计数器-1
// 批量获取文件信息
files, err = FileMap(fileIds)
if err != nil {
fmt.Println("获取文件错误")
return
}
}()
// 阻塞等待计数器小于等于0
wg.Wait()
// 填充
for i, courseware := range coursewares {
if user, ok := users[courseware.CreateId]; ok {
coursewares[i].CreateName = user.Name
}
if file, ok := files[courseware.CoverId]; ok {
coursewares[i].CoverPath = file.Path
}
}
fmt.Println(coursewares)
}
...
我们初始化一个sync.WaitGroup,调用wg.Add(1)给计数器加一,调用wg.Done()计数器减一,wg.Wait()阻塞等待直到计数器小于等于0,结束阻塞,继续往下执行。
3、errgroup
但是我们现在又有这样的需求,我们希望如果获取用户或者获取文件有任何一方报错了,直接抛错,不再组装数据。
我们可以像下面这样写
...
var goErr error
var wg sync.WaitGroup
...
func main() {
...
// 此处放到协程里
wg.Add(1)
go func() {
defer wg.Done()
// 批量获取用户信息
users, err = UserMap(userIds)
if err != nil {
goErr = err
fmt.Println("获取用户错误:", err)
return
}
}()
// 此处放到协程里
wg.Add(1)
go func() {
defer wg.Done()
// 批量获取文件信息
files, err = FileMap(fileIds)
if err != nil {
goErr = err
fmt.Println("获取文件错误:", err)
return
}
}()
wg.Wait()
if goErr != nil {
fmt.Println("goroutine err:", err)
return
}
...
}
...
把错误放在goErr中,结束阻塞后判断协程调用是否抛错。
那golang里面有没有类似这样的实现呢?答案是有的,那就是errgroup。其实和我们上面的方法差不多,但是errgroup包做了一层结构体的封装,也不需要在每个协程里面判断error传给errGo了。
下面是errgroup的实现
package main
import (
"errors"
"fmt"
"golang.org/x/sync/errgroup"
"time"
)
type Courseware struct {
Id int64
Name string
Code string
CreateId int64
CreateName string
CoverId int64
CoverPath string
}
type User struct {
Id int64
Name string
}
type File struct {
Id int64
Path string
}
var coursewares []Courseware
var users map[int64]User
var files map[int64]File
var err error
// 定义一个errgroup
var eg errgroup.Group
func main() {
// 查询课件
coursewares, err = CoursewareList()
if err != nil {
fmt.Println("获取课件错误:", err)
return
}
// 获取用户ID、文件ID
userIds := make([]int64, 0)
fileIds := make([]int64, 0)
for _, courseware := range coursewares {
userIds = append(userIds, courseware.CreateId)
fileIds = append(fileIds, courseware.CoverId)
}
// 此处放到协程里
eg.Go(func() error {
// 批量获取用户信息
users, err = UserMap(userIds)
if err != nil {
fmt.Println("获取用户错误:", err)
return err
}
return nil
})
// 此处放到协程里
eg.Go(func() error {
// 批量获取文件信息
files, err = FileMap(fileIds)
if err != nil {
fmt.Println("获取文件错误:", err)
return err
}
return nil
})
// 判断group中是否有报错
if goErr := eg.Wait(); goErr != nil {
fmt.Println("goroutine err:", err)
return
}
// 填充
for i, courseware := range coursewares {
if user, ok := users[courseware.CreateId]; ok {
coursewares[i].CreateName = user.Name
}
if file, ok := files[courseware.CoverId]; ok {
coursewares[i].CoverPath = file.Path
}
}
fmt.Println(coursewares)
}
func UserMap(ids []int64) (map[int64]User, error) {
time.Sleep(3 * time.Second)
return map[int64]User{
1: {Id: 1, Name: "liu"},
2: {Id: 2, Name: "kang"},
}, errors.New("sql err")
}
func FileMap(ids []int64) (map[int64]File, error) {
time.Sleep(3 * time.Second)
return map[int64]File{
1: {Id: 1, Path: "/a/b/c.jpg"},
2: {Id: 2, Path: "/a/b/c/d.jpg"},
}, nil
}
func CoursewareList() ([]Courseware, error) {
time.Sleep(3 * time.Second)
return []Courseware{
{Id: 1, Name: "课件1", Code: "CW1", CreateId: 1, CreateName: "", CoverId: 1, CoverPath: ""},
{Id: 2, Name: "课件2", Code: "CW2", CreateId: 2, CreateName: "", CoverId: 2, CoverPath: ""},
}, nil
}
当然,errgroup中也有针对上下文的errgroup.WithContext函数,如果我们想控制请求接口的时间,用这个是最合适不过的。如果请求超时会返回一个关闭上下文的报错,像下面这样
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
"time"
)
type Courseware struct {
Id int64
Name string
Code string
CreateId int64
CreateName string
CoverId int64
CoverPath string
}
type User struct {
Id int64
Name string
}
type File struct {
Id int64
Path string
}
var coursewares []Courseware
var users map[int64]User
var files map[int64]File
var err error
func main() {
// 查询课件
...
// 获取用户ID、文件ID
...
// 定义一个带超时时间的上下文,1秒钟超时
ctx, cancelFunc := context.WithTimeout(context.Background(), 1*time.Second)
defer cancelFunc()
// 定义一个带上下文的errgroup,使用上面带有超时时间的上下文
eg, ctx := errgroup.WithContext(ctx)
// 此处放到协程里
eg.Go(func() error {
// 批量获取用户信息
users, err = UserMap(ctx, userIds)
if err != nil {
fmt.Println("获取用户错误:", err)
return err
}
return nil
})
// 此处放到协程里
eg.Go(func() error {
// 批量获取文件信息
files, err = FileMap(ctx, fileIds)
if err != nil {
fmt.Println("获取文件错误:", err)
return err
}
return nil
})
if goErr := eg.Wait(); goErr != nil {
fmt.Println("goroutine err:", err)
return
}
// 填充
for i, courseware := range coursewares {
if user, ok := users[courseware.CreateId]; ok {
coursewares[i].CreateName = user.Name
}
if file, ok := files[courseware.CoverId]; ok {
coursewares[i].CoverPath = file.Path
}
}
fmt.Println(coursewares)
}
func UserMap(ctx context.Context, ids []int64) (map[int64]User, error) {
result := make(chan map[int64]User)
go func() {
time.Sleep(2 * time.Second) // 假装请求超过1秒钟
result <- map[int64]User{
1: {Id: 1, Name: "liu"},
2: {Id: 2, Name: "kang"},
}
}()
select {
case <-ctx.Done(): // 如果上下文结束直接返回错误信息
return nil, ctx.Err()
case res := <-result: // 返回正确结果
return res, nil
}
}
func FileMap(ctx context.Context, ids []int64) (map[int64]File, error) {
return map[int64]File{
1: {Id: 1, Path: "/a/b/c.jpg"},
2: {Id: 2, Path: "/a/b/c/d.jpg"},
}, nil
}
func CoursewareList() ([]Courseware, error) {
time.Sleep(3 * time.Second)
return []Courseware{
{Id: 1, Name: "课件1", Code: "CW1", CreateId: 1, CreateName: "", CoverId: 1, CoverPath: ""},
{Id: 2, Name: "课件2", Code: "CW2", CreateId: 2, CreateName: "", CoverId: 2, CoverPath: ""},
}, nil
}
执行上面的代码:
go run waitgroup.go
获取用户错误: context deadline exceeded
goroutine err: context deadline exceeded
golang中的errgroup的更多相关文章
- golang中的race检测
golang中的race检测 由于golang中的go是非常方便的,加上函数又非常容易隐藏go. 所以很多时候,当我们写出一个程序的时候,我们并不知道这个程序在并发情况下会不会出现什么问题. 所以在本 ...
- 基础知识 - Golang 中的正则表达式
------------------------------------------------------------ Golang中的正则表达式 ------------------------- ...
- golang中的reflect包用法
最近在写一个自动生成api文档的功能,用到了reflect包来给结构体赋值,给空数组新增一个元素,这样只要定义一个input结构体和一个output的结构体,并填写一些相关tag信息,就能使用程序来生 ...
- Golang中的坑二
Golang中的坑二 for ...range 最近两周用Golang做项目,编写web服务,两周时间写了大概五千行代码(业务代码加单元测试用例代码).用Go的感觉很爽,编码效率高,运行效率也不错,用 ...
- Golang 中的坑 一
Golang 中的坑 短变量声明 Short variable declarations 考虑如下代码: package main import ( "errors" " ...
- google的grpc在golang中的使用
GRPC是google开源的一个高性能.跨语言的RPC框架,基于HTTP2协议,基于protobuf 3.x,基于Netty 4.x. 前面写过一篇golang标准库的rpc包的用法,这篇文章接着讲一 ...
- Golang中Struct与DB中表字段通过反射自动映射 - sqlmapper
Golang中操作数据库已经有现成的库"database/sql"可以用,但是"database/sql"只提供了最基础的操作接口: 对数据库中一张表的增删改查 ...
- Golang中WaitGroup使用的一点坑
Golang中WaitGroup使用的一点坑 Golang 中的 WaitGroup 一直是同步 goroutine 的推荐实践.自己用了两年多也没遇到过什么问题.直到一天午睡后,同事扔过来一段奇怪的 ...
- Golang中使用lua进行扩展
前言 最近在项目中需要使用lua进行扩展,发现github上有一个用golang编写的lua虚拟机,名字叫做gopher-lua.使用后发现还不错,借此分享给大家. 数据类型 lua中的数据类型与go ...
随机推荐
- Spring源码 11 IOC refresh方法6
参考源 https://www.bilibili.com/video/BV1tR4y1F75R?spm_id_from=333.337.search-card.all.click https://ww ...
- 一文带你弄懂 JVM 三色标记算法!
大家好,我是树哥. 最近和一个朋友聊天,他问了我 JVM 的三色标记算法.我脑袋一愣发现竟然完全不知道!于是我带着疑问去网上看了几天的资料,终于搞清楚啥事三色标记算法,它是用来干嘛的,以及它和 CMS ...
- Java NIO全面详解(看这篇就够了)
很多技术框架都使用NIO技术,学习和掌握Java NIO技术对于高性能.高并发网络的应用是非常关键的@mikechen NIO简介 NIO 中的 N 可以理解为 Non-blocking,不单纯是 N ...
- Taurus.MVC 微服务框架 入门开发教程:项目部署:3、微服务应用程序版本升级:全站升级和局部模块升级。
系列目录: 本系列分为项目集成.项目部署.架构演进三个方向,后续会根据情况调整文章目录. 本系列第一篇:Taurus.MVC V3.0.3 微服务开源框架发布:让.NET 架构在大并发的演进过程更简单 ...
- ipi发送阻塞导致crash
3.10的内核, 在子进程退出的时候,发送信号通知父进程,此时是持有父进程的sighand中的spinlock的,然后父进程和该子进程不在一个核上,发送ipi的reschedule中断给对应的核, 但 ...
- JZM 的套题(暴力,DP)
J Z M \rm JZM JZM 即将奔赴 N O I \rm NOI NOI 考场,为了让同学们赶上自己的千分之一水平,刻意出了两道水题给同学们练练. 1.曾经的代码 J Z M \rm JZM ...
- Python入门系列(二)语法风格
python缩进 Python使用缩进来表示代码块,例如 if 5 > 2: print("Five is greater than two!") 如果跳过缩进,Python ...
- 字节跳动端智能工程链路 Pitaya 的架构设计
Client AI 是字节跳动产研架构下属的端智能团队,负责端智能 AI 框架和平台的建设,也负责模型和算法的研发,为字节跳动开拓端上智能新场景.本文介绍的 Pitaya 是由字节跳动的 Client ...
- 02_Linux基础-文件系统层次结构-提示符-进程-命令格式-隐藏文件-通配符-绝对相对路径-{1..100}-ls-mkdir-其他基础命令
02_Linux基础-ls-mkdir-cd-pwd-man-useradd-su-rm-tree-tab-passwd-w-ssh-touch-date-stat-cp-mv-du-文件系统层次结构 ...
- yield功能分析
下面是示例 输出结果: starting... 4 ******************** res: None 4