go使用注意点
1、需要注意sync.WaitGroup
是一个结构体,进行参数传递的时候要传递指针。
2、Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
在Go语言的函数中return
语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。
defer经典案例
阅读下面的代码,写出最后的打印结果。
1 | func f1() int { |
defer面试题
1 | func calc(index string, a, b int) int { |
1 | type temp struct{} |
1 | func main() { |
1 | func main() { |
3、
在做对外接口的数据对接和转换时,我们经常需要对 JSON 数据进行处理。
如下代码:
1 | type T struct { |
输出结果是空,原因是 JSON 的输出,只会输出公开(导出)字段,也就是结构体T里面的字段name和age首字母必须为大写。
4、
程序运行期间funcB
中引发了panic
导致程序崩溃,异常退出了。这个时候我们就可以通过recover
将程序恢复回来,继续往后执行。
1 | func funcA() { |
注意:
recover()
必须搭配defer
使用。defer
一定要在可能引发panic
的语句之前定义。panic 只会对当前 Goroutine 的 defer 有效
1
2
3
4
5
6
7
8
9
10
11
12
13func main() {
defer func() {
if err := recover(); err !=nil{
fmt.Println(err)
}
}()
go func() {
fmt.Println("======begin work======")
panic("nil pointer exception")
}()
time.Sleep(time.Second*100)
fmt.Println("======after work======")
}以上的panic是在另一个goroutine中定义的,所以对main中的defer无法cover住这个异常。
5、
在 Go 中,循环迭代器变量是一个单一的变量,在每个循环迭代中取不同的值。这如果使用不当,可能会导致非预期的行为。
如下代码:
1 | func main() { |
输出结果:
1 | Values: 3 3 3 |
原因是:在每次迭代中,我们将 i 的地址追加到 out 切片中,但由于它是同一个变量,我们实际上追加的是相同的地址,该地址最终包含分配给 i 的最后一个值。
6、for…range…拷贝副本
1 | func main() { |
输出结果如下:
1 | k:zhou v:&{wang 22} |
原因:for range每次产生的k,v都是一个值拷贝,不是stus值对应的引用。
详解:for中,第一次循环,变量stu被赋值,指向”zhou”,然后放到map中保存,第二次循环,变量stu的被重新赋值,改为指向“li”,这时map中的指向也跟着改变。
解决方法:
1 | for _, stu := range stus{ |
或者map存储student的实体,而不是指针,如map[string]student,m[stu.Name] = stu,把stu复制了一份到map中。
例子2:
在循环迭代器变量上使用 goroutine
1 | values := []int{1, 2, 3, 4, 5} |
输出结果如下:
1 | 5 |
原因:在闭包执行的匿名函数中,val的值由于在匿名函数中找不到,就去到函数外面找,由于外层的val值是切片values的一个临时变量,在for…range…执行时,不断被赋予新的值,所以,匿名函数中的val也会跟着改变。
解决方法:
1 | values := []int{1, 2, 3, 4, 5} |
7、常量
iota
是go语言的常量计数器,只能在常量的表达式中使用。
**iota
在const关键字出现时将被重置为0。const中每新增一行常量声明将使iota
计数一次(iota可理解为const语句块中的行索引)。 **
1 | const ( |
8、float不支持按位或操作
1 | func main() { |
float不支持按位或操作,编译不通过
9、
1 | var nums1 []interface{} |
打印结果:
1 | []interface {} |
原因:使用append,会把nums2当成一个空接口类型加到空接口切片nums1中。
拓展:把apend这句改成一下
1 | nums3 := append(nums3, nums2...) |
会报错:
1 | cannot use nums2 (variable of type []int) as type []interface{} in argument to append |
原因:nums3 := append(nums3, nums2...)
本质是把[]int
类型转换成[]interface{}
类型,go不支持这样的转换。
10、
1 | A. var x = nil |
nil 只能赋值给指针、chan、func、interface、map 或 slice 类型的变量
11、
1 | m := [...]int{ |
输出:
1 | 100 |
原因:定义并赋值一个int数组,使用...
会根据初始值的个数自行推断数组的长度,而字符a、b、c对应的ASCII的值为97,98,99,所以数组m的最大值为m[‘c’],即m[99]。
12、
- 没有支持前缀自增自减的运算语句,也就是不允许 ++a。
- 运算符 ++ 和 – 只能作为一个语句来使用,不可以作为表达式被赋值给其它的变量使用。例如:
b := a++
是不允许的。
13、
go中所有的函数传参都是值传递,对于基础值类型在传参中使用深拷贝,实际上对于指针、slice(切片)、map(映射)、channel(管道)、函数、接口,它们都是引用类型(如切片底层也是一个指针),使用的是浅拷贝,即指针拷贝了一个副本,指向的内存地址依然是原数据。
14、使用channel实现互斥锁
1 | type Mutex struct { |
15、
go中使用map[string]struct{}
来模拟一个C++的set
16、
全局变量的作用域是整个包,局部变量的作用域是该变量所在的花括号内
1 | var t int |
输出:
1 | init: 2 |
原因:init中的t是用:=生成的,所以t是局部变量,在init函数中覆盖了全局变量t。全局变量t并没有被赋值,它还是原来的0值。
修改为:
1 | var t int |
17、
实现接口时有下面的约束:
- 如果定义的是 (Type)Method,则该类型会隐式的声明一个 (*Type)Method;
- 如果定义的是 (*Type)Method ,则不会隐式什么一个 (Type)Method。
https://mp.weixin.qq.com/s/_duDs0oHc_z_p--3OoIfVw
18、
在使用copy函数时,只要注意两个切片中的最小长度就行了。copy函数只会将min(len(dstslice), len(srcslice))个元素拷贝到dstslice中,有时候会是0个,所以这个要特别注意的。
19、
1 | var y = 5.2 |
1 | const a = 7.0 |
1 | const ( |
1 | a := 40 |
对于变量而言,如果没有显示指定数据类型,编译器会根据赋值自动推导出确定的数据类型,变量不会根据上下文转换其类型。整数的默认类型是
int
,浮点数的默认类型是float64
对于常量而言,如果没有显示指定数据类型,编译器同样会推导出一个数据类型,但是没有显示指定数据类型的常量在代码上下文里可以根据需要隐式转化为需要的数据类型进行计算。
Go不允许不同的数据类型做运算。当变量和没有显示指定数据类型的常量混合在一起运算时,如果常量转化成变量的类型不会损失精度,那常量会自动转化为变量的数据类型参与运算。如果常量转化成变量的类型会损失精度,那就会编译报错。