go语言解决并发问题小结

 更新时间:2024年05月20日 10:17:41   作者:snail_lie  
并发是GO最基本的功能了,但是在传统的PHP中是比较困难的,如果不借助其它一些扩展的话,是做不到并发的,这篇文章主要介绍了go语言如何解决并发问题,需要的朋友可以参考下

一、选择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,而不会受到循环迭代的影响。这个技巧非常有用,可以在循环中创建多个独立的闭包,并确保它们捕获的是预期的变量值,而不会受到循环迭代的干扰

当然,还有一些第三方库也实现了上面的并发分组操作,大家感兴趣的可以去GitHub上看看,但功能和实现基本都大同小异。以上就是GO并发的基础,将一个父任务拆分成多个子任务去执行,提高程序的并发度,节省程序耗时。我们平时在工作中,两种方法都可以直接拿来使用,可以说这两个GO并发方法几乎贯穿了我的GO职业生涯,也是最基础最实用的并发操作方法

到此这篇关于go语言如何解决并发问题的文章就介绍到这了,更多相关go语言并发内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Golang接口型函数使用小结

    Golang接口型函数使用小结

    接口函数指的是用函数实现接口,这样在调用的时候就会非常简便,这种方式适用于只有一个函数的接口,这里以迭代一个map为例,演示这一实现的技巧,对Golang接口型函数使用知识感兴趣的朋友一起看看吧
    2022-06-06
  • GO语言字符串处理Strings包的函数使用示例讲解

    GO语言字符串处理Strings包的函数使用示例讲解

    这篇文章主要为大家介绍了GO语言字符串处理Strings包的函数使用示例讲解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步早日升职加薪
    2022-04-04
  • Go中的函数选项模式(Functional Options Pattern)详解

    Go中的函数选项模式(Functional Options Pattern)详解

    在 Go 语言中,函数选项模式是一种优雅的设计模式,用于处理函数的可选参数,本文将对其进行讲解,准备好了吗,快跟随着本文一探究竟吧
    2023-06-06
  • 利用Go语言实现在终端绘制小兔子

    利用Go语言实现在终端绘制小兔子

    这篇文章主要为大家详细介绍了如何利用Go语言实现在终端绘制小兔子来给大家拜个早年,文中的示例代码讲解详细,感兴趣的小伙伴可以了解一下
    2023-01-01
  • 一文详细谈谈GoLang的panic和error

    一文详细谈谈GoLang的panic和error

    说是初识,并不是说第一次使用error和panic包,而是第一次特地去了解golang中的这两个机制,下面这篇文章主要给大家介绍了关于如何通过一文详细谈谈GoLang中panic和error的相关资料,需要的朋友可以参考下
    2022-12-12
  • Golang使用archive/zip包实现ZIP压缩与解压

    Golang使用archive/zip包实现ZIP压缩与解压

    Golang 中的 archive/zip 包用于处理 ZIP 格式的压缩文件,提供了一系列用于创建、读取和解压缩 ZIP 格式文件的函数和类型,使用起来非常方便,下面就跟随小编一起了解一下具体使用方法吧
    2023-08-08
  • go 语言字符类型 byte 与 rune案例详解

    go 语言字符类型 byte 与 rune案例详解

    这篇文章主要介绍了go 语言字符类型 byte 与 rune案例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-03-03
  • Go语言建议多使用切片少使用数组原理探究

    Go语言建议多使用切片少使用数组原理探究

    这篇文章主要为大家介绍了Go语言建议多使用切片少使用数组原理探究,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • 浅谈Go切片的值修改是否会覆盖数组的值 

    浅谈Go切片的值修改是否会覆盖数组的值 

    本文主要介绍了浅谈Go切片的值修改是否会覆盖数组的值,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下 
    2022-02-02
  • 为什么GO不支持循环引用

    为什么GO不支持循环引用

    这篇文章主要介绍的是为什么GO不支持循环引用,学习 Go 语言的开发者越来越多了,很多小伙伴在使用时,就会遇到种种不理解的问题,其中一点就是包的循环引用的报错,下main文章我们一起来看看学习原因
    2021-10-10

最新评论