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 ...
随机推荐
- B端产品需求分析与优先级判断
需求分析是产品经理工作中的重要一部分,而对B端产品经理来说,因为业务的特殊性,所以需求分析更考验产品经理的基础能力比如还原场景中业务调研的能力.需求价值分析中对价值的界定等. B端厂商的产品需求多数来 ...
- 数据结构与算法【Java】02---链表
前言 数据 data 结构(structure)是一门 研究组织数据方式的学科,有了编程语言也就有了数据结构.学好数据结构才可以编写出更加漂亮,更加有效率的代码. 要学习好数据结构就要多多考虑如何将生 ...
- Luogu3398 仓鼠找sugar (LCA)
第一发lg[]没开够RE了,下了数据本地一直停止运行,还以为是dfs死了,绝望一交,A了... 判断\(x\)是否在路径\(s-t\)上,只需满足 \(dep_{x} >= dep_{LCA(s ...
- java-Servlet生命周期与jsp
容器如何处理请求资源路径? 比如,在浏览器地址栏输入:ip地址路径/abc.html. Step1.默认访问的是一个servlet,会从web.xml中查找有没有匹配的servlet. ...
- 用Socket套接字发送和接收文件(中间用数组存取)
创建服务端: public class TcpFileServer { public static void main(String[] args) throws Exception { //1创建S ...
- 痞子衡嵌入式:MCUXpresso IDE下设置代码编译优化等级的几种方法
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是MCUXpresso IDE下设置代码编译优化等级的几种方法. 最近公司芯片设计团队正在开发一款全新的基于 Cortex-M33 内核的 ...
- Windows API 学习
Windows API学习 以下都是我个人一些理解,笔者不太了解windows开发,如有错误请告知,非常感谢,一切以microsoft官方文档为准. https://docs.microsoft.co ...
- Linux有趣命令
通外网 下载使用阿里云镜像源:wget -O /etc/yum.repos.d/CentOS-Base.repo https://mirrors.aliyun.com/repo/Centos-7.re ...
- STC15 串口通信
串口1选择定时器2产生波特率 串口1相关寄存器 1.选择串口1所放的管脚 2.串口1配置步骤 3.选择串口工作模式 4.确定定时器2工作速度 代码配置 void Uart1_Tim2_Config(i ...
- KingbaseES 中可以通过构造一个聚集函数来实现mysql的any_value功能。
示例 创建函数 create function coalesce_("anyelement","anyelement") returns "anyel ...