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. 针对多个球体的World类

    World类其他都一样的,就修改build函数就行了,以后测试所有代码,都是基于两个或多个球体的,不再重复阐述. void World::build() { vp.set_hres(200); vp. ...

  2. Redis 09 基数

    参考源 https://www.bilibili.com/video/BV1S54y1R7SB?spm_id_from=333.999.0.0 版本 本文章基于 Redis 6.2.6 概述 Redi ...

  3. 浅谈MySQL的sql_mode

    SQL mode 今天我们来分享一下MySQL的SQL mode , 这也是我们比较容易忽略的一点,我们在一开始安装数据库的时候其实就要先考虑要保留哪些SQL mode,去除哪些,合理的配置能够减少很 ...

  4. [Android开发学iOS系列] 工具篇: Xcode使用和快捷键

    [Android开发学iOS系列] 工具篇: Xcode使用和快捷键 工欲善其事必先利其器. 编辑 Cmd + N: 新建文件 Option + Cmd + N: 新建文件夹 Cmd + / : 注释 ...

  5. 试用 ModVB(一):安装教程+使用 JSON 常量和 JSON 模式匹配

    前排提醒:阅读此文章并充分尝试 ModVB 的新语法需要较长的时间.对于程序员而言,如果你工作时不用 VB,则最好避免在上班时间看,以免被领导认为你在长时间摸鱼. 什么是 ModVB ModVB 是一 ...

  6. k8s数据持久化,pv和pvc

    k8s中为什么使用存储: k8s中的副本控制器保证了pod的始终存储,却保证不了pod中的数据.只有启动一个新pod的,之前pod中的数据会随着容器的删掉而丢失! pv和pvc的概念: Persist ...

  7. 【Traefik二次开发】中间件 Middleware 开发

    本篇只讨论HTTP中间件 中间件定义 https://doc.traefik.io/traefik/middlewares/overview/ Attached to the routers, pie ...

  8. 【Android 逆向】ARM switch 逆向

    #include <stdio.h> int switch1(int a, int b, int i){ switch (i){ case 1: return a + b; break; ...

  9. Bert不完全手册8. 预训练不要停!Continue Pretraining

    paper: Don't stop Pretraining: Adapt Language Models to Domains and Tasks GitHub: https://github.com ...

  10. 使用containerlab搭建cilium BGP环境解析

    使用 Containerlab + Kind 快速部署 Cilium BGP 环境一文中使用Containerlab和Cilium实现了模拟环境下的Cilium BGP网络.它使用Containerl ...