Golang语言陷阱

1、多值赋值
  • 多值赋值语义
    多值赋值看似简化了代码,但相互引用会让人产生困惑。多值赋值包含两层语义
    (1)对左侧操作数中的表达式、索引值进行计算和确定,首先确定左侧的操作数地址,然后对右侧的赋值表达式进行计算,如果发现右侧的表达式计算引用了左侧的变量,则创建左侧变量的临时变量进行值拷贝,最后完成计算。
    (2)从左到右依次赋值。
package main

import "fmt"

func main(){
	x := []int{1,2,3}
	i := 0
	x[i],i = 2,x[i]  //temp = x[0] x[0] = 2 i = temp i->1
	fmt.Println(i,x)
}
  • 执行结果:1 [2,2,3]
  • 结果分析:先计算赋值语句(=)左右两侧 x[i] 中索引 i 的值,此时i=0,两个被赋值的变量是 i 和 x[0],两个赋值变量分别为2、x[0]。由于x[0]是左边的操作数,所以编译器会创建一个临时变量temp,即temp = x[0],然后从左到右依次执行赋值操作x[0] = 2 i = temp。
  • 通过这种方式可以很好的写一个交换两个数值的程序
package main

import "fmt"

func main(){
	a,b := 1,2
	fmt.Println("a = ",a," b = ",b)
	a,b = b,a   //temp = a,a = b,b = temp
	fmt.Println("a = ",a," b = ",b)
}
2、range复用临时变量
  • 先上一段代码
package main

import "sync"

func main(){
	wg := sync.WaitGroup{}
	si := []int{1,2,3,4,5,6,7,8,9,10}
	for i := range si {
		wg.Add(1)
		go func(){
			println(i)
			wg.Done()
		}()
	}
	wg.Wait()
}
  • 运行结果
9
9
9
9
9
9
9
9
9
9

程序没有达到我们的预期,而是全部打印9。有两点原因导致的:
(1)for range下的迭代变量 i 的值是共用的。
(2)main函数所在的goroutine和后续启动的goroutine存在竞争关系。

  • 正确的写法是使用函数参数做一次数据复制,而不是闭包。
package main

import "sync"
 
func main(){
	wg := sync.WaitGroup{}
	si := []int{1,2,3,4,5,6,7,8,9,10}
	for i := range si {
		wg.Add(1)
		//这里有一个实参到形参的值拷贝
		go func(a int){
			println(a)
			wg.Done()
		}(i)
	}
	wg.Wait()
}
  • 执行结果:可以按照预期输出
  • 总结:
    其实for range复用迭代变量不能说是一个缺陷,而是Go的设计者为了性能而选择的一种设计方案。因为大多情况下for循环块里的代码是在同一个goroutine里运行的,为了避免空间的浪费和GC的压力,复用了range迭代时的临时变量。所以注意:在for循环下调用并发时要复制迭代变量后再使用,不要直接使用for的迭代变量。
3、defer陷阱
  • 第一个副作用是对返回值的影响
  • 第二个副作用是对性能的影响
    (1)defer和函数返回值:defer中如果引用了函数的返回值,则因引用形式的不同会导致不同的结果
package main

func f1()(r int){
	defer func (){
		r++
	}()
	return 0
}
func f2()(r int){
	t := 5
	defer func(){
		t = t + 5
	}()
	return t
}
func f3()(r int){
	defer func(r int){
		r = r + 5
	}(r)
	return 1
}
func main(){
	println("f1=",f1()) //f1=1
	println("f2=",f2()) //f2=5
	println("f3=",f3()) //f3=1
}

f1、f2、f3三个函数的共同点就是它们都是带命名返回值的函数,函数返回值都是变量r。

  • 函数调用方负责开辟栈空间,包括形参和返回值的空间。
  • 有名的函数返回值相当于函数的局部变量,被初始化为类型的零值。
    (1)分析f1函数,defer语句后面的匿名函数是对函数返回值r的闭包引用,f1的逻辑如下:
    ① r 是函数的有名返回值,分配在栈上,其地址又被称为返回值所在栈区。首先r先初始化为0。
    ② “return 0”会复制到返回值栈区,返回值 r 被赋值为0。
    ③ 执行defer语句,由于匿名函数对返回值 r 是闭包引用,所以 r++ 执行后,函数返回值被修改为1。
    ④ defer 语句执行完后RET返回,此时函数的返回值仍然为 1 。
  • f1 的程序指令如下:
    在这里插入图片描述
    (2)f2的逻辑如下:
    ① 函数返回值 r 被初始化为 0。
    ② 创建局部变量 t ,并赋值为5。
    ③ 执行return t,将t的值 5 复制到返回值 r 所在的栈区。
    ④ defer语句后面的匿名函数是对局部变量 t 的闭包引用,t 被赋值为10。但是并不影响到 r。
    ⑤ 函数返回,此时函数返回值栈区上的值仍然是5。
  • f2的程序指令如下:
    在这里插入图片描述
    (3)f3的逻辑如下:
    ① 函数返回值 r 被初始化为 0。
    ② 执行return 1,将1复制到 r 所在的栈区,此时 r = 1。
    ③ 执行defer,defer后面的匿名函数是对函数返回值 r 的闭包引用,此时使用的传参机制。在注册defer函数时将返回值 r 作为实参传进去,由于函数调用是值拷贝,所以defer函数执行后只是形参值变为5,对实参没有任何的影响。
    ④ 函数返回,此时返回栈区上的值是 1。
  • f3的程序执行指令如下:r1为返回值,r2为匿名函数形参
    在这里插入图片描述
    综上所述:对于在defer的函数中返回有命名的函数返回值有三个步骤:
    (1)执行return的值拷贝,将return语句返回的值复制到函数返回值栈区。(如果只有一个return,不带任何变量或值,则此步骤不做任何动作)
    (2)执行defer语句,多个defer按照FIFO顺序执行。
    (3)执行调整RET指令。
    由此看出在defer中修改函数返回值不是一个明智的编程方法,在实际编程中应尽可能避免这种情况。还有一种彻底避免这种情况的发生的方法,就是在定义函数时使用不带有返回值名的形式。通过这种方法defer就不能直接引用返回值的栈区,也就避免了返回值被修改的问题。
package main

func f4() int {
	r := 0
	defer func(){
		r++
	}()
	return r
}
func f5() int {
	r := 0
	defer func(i int){
		i++
	}(r)
	return 0
}
func main(){
	println("f4=",f4()) //f4=0
	println("f5=",f5()) //f5=0
}

可以看到在函数返回值没有名字的前提下,不管defer如何操作,都不会改变函数的return的值。

4、切片
  • nil切片和空切片
    make([]int,0)与var a []int创建的切片是有区别的,前者的切片指针有分配,后者的切片指针为0。
package main

import "fmt"
import "reflect"
import "unsafe"

func main(){
	var a []int
	b := make([]int,0)
	if a == nil {
		fmt.Println("a is nil")
	} else {
		fmt.Println("a is not nil")
	}
	//虽然b的底层数组大小为0,但是切片不为nil
	if b == nil {
		fmt.Println("b is nil")
	} else {
		fmt.Println("b is not nil")
	}
	//使用反射reflect中的SliceHeader来获取切片运行时的数据结构
	as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	fmt.Printf("len=%d,cap=%d,type=%d\n",len(a),cap(a),as.Data)
	fmt.Printf("len=%d,cap=%d,type=%d\n",len(b),cap(b),bs.Data)
}

运行结果

a is nil
b is not nil
len=0,cap=0,type=0
len=0,cap=0,type=5537704

因此:var a []int 创建的切片是一个nil的切片(底层没有数组,指针指向nil)
在这里插入图片描述
make([]int,0)创建的是一个空切片(切片指针非空,但是指向的数组为空)
在这里插入图片描述

  • 多个切片引用同一个底层数组引发的混乱
    切片可以由数组创建,一个底层数组可以创建多个切片,这些切片共享同一个底层数组。使用append函数扩展切片的过程中可能会修改底层数组的元素,间接的影响到其他切片的值,也可能发生数组复制重建。
package main

import "fmt"
import "reflect"
import "unsafe"

func main(){
	a := []int{0,1,2,3,4,5,6}
	b := a[0:4] //0,1,2,3
	as := (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs := (*reflect.SliceHeader)(unsafe.Pointer(&b))
	//a,b共享底层数组
	fmt.Printf("a=v%,len=%d,cap=%d,type=%d\n",a,len(a),cap(a),as.Data)
	fmt.Printf("b=v%,len=%d,cap=%d,type=%d\n",b,len(b),cap(b),bs.Data)
	b = append(b,10,11,12) //0,1,2,3,10,11,12
	//a,b继续共享底层数组,修改b会影响共享的底层数组,间接影响a
	fmt.Printf("a=v%,len=%d,cap=%d\n",a,len(a),cap(a))
	fmt.Printf("b=v%,len=%d,cap=%d\n",b,len(b),cap(b))
	//len(b) = 7底层数组容量是7,此时需要重新分配数组,将原来数组中的元素复制到新的数组中,然后将新数组的首地址返回
	b = append(b,13,14)
	
	as = (*reflect.SliceHeader)(unsafe.Pointer(&a))
	bs = (*reflect.SliceHeader)(unsafe.Pointer(&b))
	//可以看到a和b指向的底层数组已经不同了
	fmt.Printf("a=v%,len=%d,cap=%d,type=%d\n",a,len(a),cap(a),as.Data)
	fmt.Printf("b=v%,len=%d,cap=%d,type=%d\n",b,len(b),cap(b),bs.Data)
}

运行结果:

a=[0,1,2,3,4,5,6],len=7,cap=7,type=842350575680
b=[0,1,2,3],len=4,cap=7,type=842350575680
a=[0,1,2,3,10,11,12],len=7,cap=7
b=[0,1,2,3,10,11,12],len=7,cap=7
a=[0,1,2,3,10,11,12],len=7,cap=7,type=842350575680
b=[0,1,2,3,10,11,12,13,14],len=9,cap=14,type=842350788720
  • 总结:多个切片共享同一个底层数组,其中一个切片的append操作可能会引发如下两种情况:
  • (1)append追加的元素没有超过底层数组的容量,此时append操作会直接操作共享的底层数组,如果其他切片有引用的数组被覆盖的元素,则会导致其他切片的值也隐式的发生变化。
  • (2)append追加的元素加上原来的元素超过了底层数组的容量,此时append操作会重新申请新数组,并将原来的数组元素复制到新的数组,并返回新数组的首地址。
    所以在使用切片的过程中尽量避免多个切片共享同一个底层数组,可以使用copy函数进行显式的复制。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值