golang使用坑两处

今天的文章给大家看看两处golang使用中存在的坑,了解这两处坑,能够防止一些隐蔽性比较大的bug出现。闲话少叙,上代码

package main

import (
	"errors"
	"fmt"
)

func main() {
	var err error

	defer func(err error) {
		fmt.Printf("inner %v\n", &err)
		fmt.Println("inner", err)
	}(err)

	err = errors.New("error here")

	fmt.Printf("outer %v\n", &err)
	fmt.Println("outer", err)
}

上面代码结构经常用在函数执行结束后需要进行后处理的时候,表面看上面的代码貌似没有问题,你希望在函数执行结束后,inner打印出err的信息,那么现在执行看下结果

outer 0xc0000881e0
outer error here
inner 0xc000088200
inner <nil>

What?怎么inner err是nil?而且存放nil的地址和outer err的地址也完全不同。如果我们这里代码逻辑是判断inner err不为nil时打印log,那么很遗憾,即使outter err不是nil,你都看不到log了。先别着急分析原因,我把代码稍微修改,再来看看。

package main

import (
	"errors"
	"fmt"
)

func main() {
	var err error

	defer func(err *error) {
		fmt.Printf("inner %v\n", err)
		fmt.Println("inner", *err)
	}(&err)

	err = errors.New("error here")

	fmt.Printf("outer %v\n", &err)
	fmt.Println("outer", err)
}

修改的地方就是匿名函数传递error指针而不是变量,相关的inner打印做一下修改,这次运行结果

outer 0xc00003a1f0
outer error here
inner 0xc00003a1f0
inner error here

完全符合要求了。那么问题来了,为什么上面那段代码inner err就是nil呢?原因在于golang的传参机制,golang给函数传参,不管什么类型的变量,传递的都是变量值的拷贝,注意,是变量值的拷贝,如果变量不是指针,拷贝的就是变量的值,如果变量是指针变量,指针变量的值就是变量的地址值,所以拷贝指针变量的值,就是拷贝变量的地址值。

第一段代码,传递的是error类型的变量err,所以,我们一开始只是声明了err变量,error是一个interface,其零值是nil,也就是我们传递给匿名函数时候的err变量值是nil,golang将nil值拷贝进匿名函数的局部变量inner err,所以打印inner err地址与outter err不同,而且inner err值是nil,这完全是正确的行为。

第二段代码则不同,匿名函数传递了指针,也就是将outter err的地址值做了拷贝赋值给了匿名函数的局部变量,inner err实际保存了变量的地址值,所以,err的值会随着outer err的变化而变化。

当然我们还有最后一种代码写法来回避这个问题

package main

import (
	"errors"
	"fmt"
)

func main() {
	var err error

	defer func() {
		fmt.Printf("inner %v\n", &err)
		fmt.Println("inner", err)
	}()

	err = errors.New("error here")

	fmt.Printf("outer %v\n", &err)
	fmt.Println("outer", err)
}

匿名函数不使用参数,这样,err都是外部定义的err,不存在二义性了。下面看另外一个坑,问题原因和上面的坑一样

package main

import (
	"fmt"
	"sync"
)

func main() {
	aSlice := []int{1, 2, 3, 4}

	wg := sync.WaitGroup{}

	wg.Add(len(aSlice))

	for _, num := range aSlice {
		go func() {
			fmt.Println(num)
			fmt.Printf("%v\n", &num)
			wg.Done()
		}()
	}

	wg.Wait()
}

注意看中间的for循环部分,这样的方式采用的是上面坑中最后一种方式,匿名函数没有传参,你以为这次正确了?你以为打印会是1到4?运行下看看再说

4
4
0xc0000120e0
0xc0000120e0
4
0xc0000120e0
4
0xc0000120e0

抛开打印的异步问题,我们关注内容,竟然全是4,而且num的地址完全一样,多运行几次,运气好,你可能会遇到结果里面零星可见的3(我是看不到1和2),这又是怎么回事?

还是上面的思路,golang中for循环中创建的变量num,在整个for循环过程中是同一个变量,只是变量的值会变化。我们匿名协程直接使用num这个变量,随着外面num值在变化,协程只会打印它运行时刻的num值,大部分时候,协程得到调度是在main协程wait的时候。之所以偶尔会有3,那是因为赶巧num在3时,main协程不知道什么原因被抢占了,匿名协程得到了调度。

修改这个问题也很简单,代码如下:

package main

import (
	"fmt"
	"sync"
)

func main() {
	aSlice := []int{1, 2, 3, 4}

	wg := sync.WaitGroup{}

	wg.Add(len(aSlice))

	for _, num := range aSlice {
		go func(num int) {
			fmt.Println(num)
			fmt.Printf("%v\n", &num)
			wg.Done()
		}(num)
	}

	wg.Wait()
}

将num的值通过参数值拷贝实时传递给匿名函数,这样,匿名函数中的num只是记录了外部num的值,引用上已经脱离了关系,自然运行也就正常了。

从这两个坑我们应该看到,我们不可以生搬照抄某一种解决方案,对于第一个坑的解决方案却正好是第二个坑的问题原因,我们应该时刻对我们的代码怀有警惕心,切不可有之前这样的代码就没有问题,这次也不应该出问题的想法,对代码的仁慈和同情就是对自己的残忍,代码总会在我们不经意间给我们挖坑。对于语言机制的了解,才是用好一门编程语言,写出高质量代码的根本手段。

 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Golang使用教程可以按照以下步骤进行学习: 1. 确保环境搭建完成:根据golang基础教程(一)中的指引,搭建好Golang的开发环境。 2. 了解开发规范及API:参考golang基础教程(二),学习Golang的开发规范和常用的API使用方法。 3. 学习变量与数据类型:阅读golang基础教程(三)和(四),掌握Golang中的变量声明和各种基本数据类型。 4. 掌握基本数据类型的转换:参考golang基础教程(五)和(六),学习Golang中基本数据类型的相互转换方法。 5. 理解指针的概念:阅读golang基础教程(七),了解指针Golang中的作用和使用方法。 6. 学习数组和切片:参考golang基础教程(八)和(九),掌握Golang中数组和切片的定义和操作。 7. 熟悉map的使用:阅读golang基础教程(十),学习Golang中map的定义和使用方法。 8. 理解结构体的概念:参考golang基础教程(十一),了解Golang中结构体的定义和使用。 9. 掌握方法的使用:阅读golang基础教程(十二),学习Golang中方法的定义和调用。 10. 了解继承的概念:参考golang基础教程(十三),了解Golang中的继承实现方式。 11. 掌握接口和多态的使用:阅读golang基础教程(十四),学习Golang中接口的定义和多态的实现。 12. 学习异常处理:参考golang基础教程(十五),了解Golang中异常处理的方式。 13. 熟悉文件操作:阅读golang基础教程(十六),学习Golang中文件的读写操作方法。 14. 理解goroutine和channel的使用:参考golang基础教程(十七),了解Golang中并发编程的基本概念和使用方法。 15. 了解Golang并发原理:参考golang基础教程(十八),学习Golang中并发编程的底层原理。 16. 掌握反射的使用:阅读golang基础教程(十九),了解Golang中反射的基本操作和使用场景。 17. 学习tcp网络编程:参考golang基础教程(二十),了解Golang中基于TCP协议的网络编程方法。 18. 进行单元测试:阅读golang基础教程(附录一),学习Golang中单元测试的编写和执行。 以上是一个基本的Golang使用教程的概述,按照这些步骤,你可以系统地学习和掌握Golang的基础知识和常用技巧。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [golang基础教程](https://blog.csdn.net/weixin_37910453/article/details/87276411)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [Golang基础教程](https://blog.csdn.net/a58125584s/article/details/124511834)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值