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 ...
随机推荐
- 在Ubuntu下编译安装GreatSQL
在Ubuntu下编译安装GreatSQL 本次介绍如何利用Docker构建Ubuntu环境,并将GreatSQL源码编译成二进制文件. 1.准备工作 先创建本次Docker的workdir为 /dat ...
- 一文详解 implementation api embed
最近使用 Android Studio 从事项目开发时,发现对 implementation.api.embed 的用法了解的不是很清楚,这里准备一篇文章对其使用场景或者说是使用方式进行一个总结. d ...
- mysql安装及修改密码
MySQL5.7更改密码时出现ERROR 1054 (42S22): Unknown column 'password' in 'field list' C:\Users\Administrator& ...
- 点击>>>解锁Apache Hadoop Meetup 2021!
" 10月16日,属于开源发烧友的狂欢日来啦! Apache Hadoop Meetup 2021 开源大数据行业交流盛会盛大开启!让我们相约北京,一起嗨翻初秋~ 在当今信息化时代,逐渐成熟 ...
- 283. 移动零--LeetCode__双指针
来源:力扣(LeetCode) 链接:https://leetcode.cn/problems/move-zeroes 著作权归领扣网络所有.商业转载请联系官方授权,非商业转载请注明出处. 思路 用一 ...
- 以太坊 layer2: optimism 源码学习(二) 提现原理
作者:林冠宏 / 指尖下的幽灵.转载者,请: 务必标明出处. 掘金:https://juejin.im/user/1785262612681997 博客:http://www.cnblogs.com/ ...
- KingbaseES R6 主备流复制集群创建级联复制案例
案例环境: 数据库: test=# select version(); version -------------------------------------------------------- ...
- Python 第二次实验
[1] (程序设计)三位水仙花数的计算."三位水仙花数"是指一个三位整数,其各位数字的3次方和等于该数本身.例如:ABC是一个"3位水仙花数",则:A的3次方+ ...
- Kubernetes后台数据库etcd:安装部署etcd集群,数据备份与恢复
目录 一.系统环境 二.前言 三.etcd数据库 3.1 概述 四.安装部署etcd单节点 4.1 环境介绍 4.2 配置节点的基本环境 4.3 安装部署etcd单节点 4.4 使用客户端访问etcd ...
- 内存溢出(OOM)分析
当JVM内存不足时,会抛出java.lang.OutOfMemoryError. 主要的OOM类型右: Java heap space:堆空间不足 GC overhead limit exceed ...