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的更多相关文章

  1. golang中的race检测

    golang中的race检测 由于golang中的go是非常方便的,加上函数又非常容易隐藏go. 所以很多时候,当我们写出一个程序的时候,我们并不知道这个程序在并发情况下会不会出现什么问题. 所以在本 ...

  2. 基础知识 - Golang 中的正则表达式

    ------------------------------------------------------------ Golang中的正则表达式 ------------------------- ...

  3. golang中的reflect包用法

    最近在写一个自动生成api文档的功能,用到了reflect包来给结构体赋值,给空数组新增一个元素,这样只要定义一个input结构体和一个output的结构体,并填写一些相关tag信息,就能使用程序来生 ...

  4. Golang中的坑二

    Golang中的坑二 for ...range 最近两周用Golang做项目,编写web服务,两周时间写了大概五千行代码(业务代码加单元测试用例代码).用Go的感觉很爽,编码效率高,运行效率也不错,用 ...

  5. Golang 中的坑 一

    Golang 中的坑 短变量声明  Short variable declarations 考虑如下代码: package main import ( "errors" " ...

  6. google的grpc在golang中的使用

    GRPC是google开源的一个高性能.跨语言的RPC框架,基于HTTP2协议,基于protobuf 3.x,基于Netty 4.x. 前面写过一篇golang标准库的rpc包的用法,这篇文章接着讲一 ...

  7. Golang中Struct与DB中表字段通过反射自动映射 - sqlmapper

    Golang中操作数据库已经有现成的库"database/sql"可以用,但是"database/sql"只提供了最基础的操作接口: 对数据库中一张表的增删改查 ...

  8. Golang中WaitGroup使用的一点坑

    Golang中WaitGroup使用的一点坑 Golang 中的 WaitGroup 一直是同步 goroutine 的推荐实践.自己用了两年多也没遇到过什么问题.直到一天午睡后,同事扔过来一段奇怪的 ...

  9. Golang中使用lua进行扩展

    前言 最近在项目中需要使用lua进行扩展,发现github上有一个用golang编写的lua虚拟机,名字叫做gopher-lua.使用后发现还不错,借此分享给大家. 数据类型 lua中的数据类型与go ...

随机推荐

  1. react环境搭建及文件配置

    webpack简介 构建工具(基于Nodejs)node(v16)前端工程化. 环境搭建 创建一个空的package.json npm init webpack核心包(提供了API,插件) npm i ...

  2. 43%非常看好TypeScript…解读“2022前端开发者现状报告”

    摘要:近日,The Software House 发布了"2022前端开发者现状报告",笔者在此对报告内容进行解读,供大家参考. 本文分享自华为云社区<"2022前 ...

  3. 用好JAVA中的函数式接口,轻松从通用代码框架中剥离掉业务定制逻辑

    大家好,又见面了. 今天我们一起聊一聊JAVA中的函数式接口.那我们首先要知道啥是函数式接口.它和JAVA中普通的接口有啥区别?其实函数式接口也是一个Interface类,是一种比较特殊的接口类,这个 ...

  4. 主流前沿的开源监控和报警系统Prometheus+Grafana入门之旅

    Prometheus概述 定义 Prometheus 官网地址 https://prometheus.io/ Prometheus 官网文档地址 https://prometheus.io/docs/ ...

  5. 重看Java教学视频时的查漏补缺

    数据类型 1.基本数据类型:四类八种. 2.数据范围与字节数不一定相关.如float为4字节表示范围比long的8字节要大. 3.浮点数默认double类型,如要用float,需加F. 4.boole ...

  6. CAD二次开发---关于JoinEntity出现eNotApplicable的问题

    作者在使用JoinEntity时出现eNotApplicable的问题,查阅了Autodesk论坛的相关帖子,发现大多数人都有遇到这个问题,但没有找到合适的解决方法,可能原因是进行Join时两Curv ...

  7. 2022CISCN-satool

    2022CISCN-satool 打国赛的时候自己还并不了解LLVM PASS pwn,前几天正好学习了一下LLVM PASS pwn,于是就顺便来复现一下这道题目. 首先找到二进制文件的重写函数的主 ...

  8. 基于Anacoda搭建虚拟环境cudnn6.0+cuda8.0+python3.6+tensorflow-gpu1.4.0

    !一定要查准cudnn,cuda,tensorflow-gpu对应的版本号再进行安装,且本文一切安装均在虚拟环境中完成. 下文以笔者自己电脑为例,展开安装教程阐述(省略anaconda安装教程): 1 ...

  9. React的生命周期函数

    概述 在React中,生命周期函数指的是组件在某一个时刻会自动执行的函数 constructor 在类或组件创建的时候被自动执行,我们可以说它是生命周期函数,但它并不是React所特有的,所有的Es6 ...

  10. fastadmin后台分页设置显示方法

    ​ 1.参照日志列表的分页(后台代码都有) 2.修改默认分页配置,在初始化里面加上: pageList: [5,10,'all'], 3.显示列表: [$where, $sort, $order, $ ...