一、选择go的原因
作为一个后端开发,日常工作中接触最多的两门语言就是php和go了。无可否认,php确实是最好的语言(手动狗头哈哈),写起来真的很舒爽,没有任何心智负担,字符串和整型压根就不用区分,开发速度真的是比go快很多。现在工作中也还是有一些老项目在使用php,但21年之后的新项目基本上就都是用go了。那为什么php那么香,还要转战使用go呢,下面就给大家讲解一下我们新项目从php转go的原因,有几个比较重要的点:
1、php不能满足我们的高并发业务,这是最主要的原因了,(ps:我这里所说的php是指官方的php-fpm模式下的开发,是一个请求一个进程的那种模式,而不是类似于swoole常驻进程的那种。那么为什么不去使用swoole呢,当然也是有的,但swoole毕竟太小众了,且之前有很多bug,使用起来心智负担太高了),而我们部门所负责的是直播业务,每天都和高并发打交道啊,所以只能将目光转向了并发小王子go的怀抱。
2、go语言当时在市面上很火,像腾讯、百度、滴滴、好未来这些大厂都在陆陆续续地从php转向go,这也是一个讯号吧,跟着大佬们走总不会错。
3、go语言的简单简洁,相比较于java,上手是很快的(但真正学好还是没那么容易的),我当时就学了两个礼拜左右语法就跟着一起写项目了。
二、go解决的并发问题
说到并发,是go最基本的功能了,但是在传统的php中是比较困难的,如果不借助其它一些扩展的话,是做不到并发的。举个场景:每个用户进入直播间,都要获取很多信息,有版本服务信息、直播基础信息、用户信息、直播关联权益信息、直播间信息统计等等。如果是php的写法,就得按照下面串行的流程去做,这个接口耗时就是所有操作的时间之和,严重影响用户体验啊。
但如果换成go去做这件事,那就非常清爽了,这个用户请求耗时就只需要时间最长的那个操作耗时,如下图:
那么我们如何用去实现这个并发逻辑呢?
方法1:使用sync.waitgroup
//请求入口 func main() { var ( versiondetail, livedetail, userdetail, equitydetail, statisticsdetail int ) ctx := context.background() gonoerr(ctx, func() { versiondetail = 1 //版本服务信息 time.sleep(1 * time.second) fmt.println("执行第一个任务") }, func() { livedetail = 2 //直播基础信息 time.sleep(2 * time.second) fmt.println("执行第二个任务") }, func() { userdetail = 3 //用户信息 time.sleep(3 * time.second) fmt.println("执行第三个任务") }, func() { equitydetail = 4 //直播关联权益信息 time.sleep(4 * time.second) fmt.println("执行第四个任务") }, func() { statisticsdetail = 5 //直播间信息统计 time.sleep(5 * time.second) fmt.println("执行第五个任务") }) fmt.println(versiondetail, livedetail, userdetail, equitydetail, statisticsdetail) } //并发方法 func gonoerr(ctx context.context, functions ...func()) { var wg sync.waitgroup for _, f := range functions { wg.add(1) // 每个函数启动一个协程 go func(function func()) { function() wg.done() }(f) } // 等待执行完 wg.wait() }
方法2:使用errgroup库
//请求入口 func main() { var ( versiondetail, livedetail, userdetail, equitydetail, statisticsdetail int err error ) ctx := context.background() err = goerr(ctx, func() error { versiondetail = 1 //版本服务信息 time.sleep(1 * time.second) fmt.println("执行第一个任务") return nil //返回实际执行的错误 }, func() error { livedetail = 2 //直播基础信息 time.sleep(2 * time.second) fmt.println("执行第二个任务") return nil //返回实际执行的错误 }, func() error { userdetail = 3 //用户信息 time.sleep(3 * time.second) fmt.println("执行第三个任务") return nil //返回实际执行的错误 }, func() error { equitydetail = 4 //直播关联权益信息 time.sleep(4 * time.second) fmt.println("执行第四个任务") return nil //返回实际执行的错误 }, func() error { statisticsdetail = 5 //直播间信息统计 time.sleep(5 * time.second) fmt.println("执行第五个任务") return nil //返回实际执行的错误 }) if err != nil { fmt.println(err) return } fmt.println(versiondetail, livedetail, userdetail, equitydetail, statisticsdetail) } func goerr(ctx context.context, functions ...func() error) error { var eg errgroup.group for i := range functions { f := functions[i] //请注意这里的写法,下面有讲解 eg.go(func() (err error) { err = f() if err != nil { //记日志 } return err }) } // 等待执行完 return eg.wait() }
上面就是使用errgroup库的并发执行任务的方法,可以直接拿来使用,errgroup这是go官方提供的一个同步扩展库,可以很好地将⼀个通⽤的⽗任务拆成⼏个⼩任务并发执⾏。
上面有一点需要特别注意的写法,就是下面这段代码的写法,写法1:
for i := range functions { f := functions[i] eg.go(func() (err error) { err = f()
也可以这样写,写法2:
for _, f := range functions { fs := f eg.go(func() (err error) { err = fs()
但如果这样写就会有问题,写法3:
for _, f := range functions { eg.go(func() (err error) { err = f()
你们可以改一下,实际跑一下。会发现 (写法3) 会出现类似这样的错误结果
正确预期的结果(写法1、写法2)应该是这样的
这是因为在 go 语言中,当使用闭包(匿名函数)时,如果闭包引用了外部的变量,闭包实际上会捕获这些变量的引用。在循环中创建闭包时,如果直接将循环变量作为闭包的参数或在闭包中引用该变量,会导致所有生成的闭包都引用相同的变量,即最后一次迭代的值。
为了避免这个问题,常见的做法是在循环内部创建一个新的变量,将循环变量的值赋给这个新变量,然后在闭包中引用该新变量。这样,每次循环迭代都会创建一个新的变量,闭包捕获的是不同的变量引用,而不是相同变量的引用。
在给定的代码中,fs := f 就是为了创建一个新的变量 f,并将循环变量 f 的值赋给它。这样,在闭包中就可以安全地引用这个新变量 f,而不会受到循环迭代的影响。这个技巧非常有用,可以在循环中创建多个独立的闭包,并确保它们捕获的是预期的变量值,而不会受到循环迭代的干扰。
当然,还有一些第三方库也实现了上面的并发分组操作,大家感兴趣的可以去上看看,但功能和实现基本都大同小异。以上就是go并发的基础,将一个父任务拆分成多个子任务去执行,提高程序的并发度,节省程序耗时。我们平时在工作中,两种方法都可以直接拿来使用,可以说这两个go并发方法几乎贯穿了我的go职业生涯,也是最基础最实用的并发操作方法。