Go开发中的一些注意事项
1、参数传递
Go中参数是如何传递的?
答案只有一个: Golang 中所有的类型传递都是通过值传递实现的,而不是引用传递,即使是指针的传递也是通过copy指针的方式进行。
对于一些包裹了底层数据的数据结构,其值传递的过程中,复制的也只是实例的指针,而不是底层数据所暴露出来的指针。
chan 和 map、slice 一样也是一个指针。注意传参的时候看是否会改变实参!
1.1 自动取引用、自动解引用
- 用指针类型的实参调用形参为值类型的方法(会进行“自动解引用”)
- 用值类型的实参调用形参为指针类型的方法(会进行“自动取引用”)
主要是作用在方法的接受者
type node struct {
Name string
}
func (n node) chg1() {
n.Name += ".say1" // 会提示: ineffective assignment to field node.Name (SA4005)go-staticcheck
}
func (n *node) chg2() {
n.Name += ".say2"
}
func main() {
n1 := node{Name: "n1"}
n1.chg1() // 不改变n1
fmt.Printf("n1: %s \n", n1.Name)
n1.chg2() // 实际为: (&n1).chg2() ,go自动取引用, 改变n1
fmt.Printf("n1: %s \n", n1.Name)
(&n1).chg2()
n2 := &node{Name: "n2"}
n2.chg1() // 实际为: (*n2).chg1() ,go自动解引用, 不改变n2
fmt.Printf("n2: %s \n", n2.Name)
n2.chg2() // 不改变n1
fmt.Printf("n2: %s \n", n2.Name)
(*n2).chg1()
}
方法的调用是否会对原值产生影响完全取决于该方法的形参是值类型还是指针类型。
2、内存逃逸
Go 语言有两部分内存空间:栈内存和堆内存。
函数中局部变量分配在栈内存中,栈内存由编译器自动分配和释放,开发者无法控制。
栈内存一般存储函数中的局部变量、参数等,函数创建的时候,这些内存会被自动创建;函数返回的时候,这些内存会被自动释放。
栈的分配和回收速度非常快。
全局变量,内存占用较大的局部变量,函数调用结束后不能立刻回收的局部变量都会存在堆里面。
堆内存的生命周期比栈内存要长。
变量在堆上的分配和回收都比在栈上开销大的多。同时对于 go 这种带 GC 的语言来说,会增加 gc 压力,同时也容易造成内存碎片。
func say() *string {
s := "123"
return &s
}
func main() {
s := say()
fmt.Printf("s: %s \n", *s)
}
以上的say()函数如果是c++来写是有问题的,局部变量s在函数结束后,在栈中就被回收了,返回的一个悬挂指针。
而在go中s会发生“内存逃逸”,转移到堆中,然后s的生命周期由go控制,会发生gc。
虽然go有gc,但gc会影响运行性能。
逃逸到堆内存的变量不能马上被回收,只能通过垃圾回收标记清除,增加了垃圾回收的压力,所以要尽可能地避免逃逸,让变量分配在栈内存上,这样函数返回时就可以回收资源,提升效率。
- 不要为了减少变量的值的拷贝而返回指针,逃逸的开销比值拷贝更大。
- 尽可能避免逃逸,因为栈内存效率更高,还不用 GC。比如小对象的传参,array 要比 slice效果好。
- 如果避免不了逃逸,还是在堆上分配了内存,那么对于频繁的内存申请操作,我们要学会重用内存,比如使用 sync.Pool。
内存逃逸发生时机
- 向 channel 发送指针数据
- 局部变量在函数调用结束后还被其他地方使用
- 在 slice 或 map 中存储指针
- 在 interface 类型上调用方法
- 切片扩容后长度太大,导致栈空间不足
3、“线程”安全
“线程”不安全的例子:
func main() {
c:=1
for i:=0;i<10;i++{
go func() {
c++
}()
}
time.Sleep(100)
fmt.Println(c)
}
```
注意虽然没有显示声明go,但是在一些包的使用中,实际已经用了多协程。