Go指针使用注意事项

嵌入式技术

1371人已加入

描述

 

 

与C语言一样,Go语言中同样有指针,通过指针,我们可以只传递变量的内存地址,而不是传递整个变量,这在一定程度上可以节省内存的占用,但凡事有利有弊,Go指针在使用也有一些注意点,稍不留神就会踩坑,下面就让我们一起来细嗦下。

 

1.指针类型的变量

 

在Golang中,我们可以通过取地址符号& 得到变量的地址,而这个新的变量就是一个指针类型的变量,指针变量与普通变量的区别在于,它存的是内存地址,而不是实际的值。

 

C语言

如果是普通类型的指针变量(比如 int ),是无法直接对其赋值的,必须通过 * 取值符号才行。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
func main() { num := 1 numP := &num  //numP = 2 // 报错:(type untyped int) cannot be represented by the type *int *numP = 2}

		

但结构体却比较特殊,在日常开发中,我们经常看到一个结构体指针的内部变量仍然可以被赋值,比如下面这个例子,这是为什么呢?

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
type Test struct { Num int}
// 直接赋值和指针赋值func main() { test := Test{Num: 1} test.Num = 3 fmt.Println("v1", test) // 3
 testP := &test testP.Num = 4           // 结构体指针可以赋值 fmt.Println("v2", test) // 4}

		

这是因为结构体本身是一个连续的内存,通过 testP.Num ,本质上拿到的是一个普通变量,并不是一个指针变量,所以可以直接赋值。

 

C语言

那slice、map、channel这些又该怎么理解呢?为什么不用取地址符号也能打印它们的地址?比如下面的例子

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func main() { nums := []int{1, 2, 3} fmt.Printf("%p
", nums)     // 0xc0000160c0 fmt.Printf("%p
", &nums[0]) // 0xc0000160c0
 maps := map[string]string{"aa": "bb"} fmt.Printf("%p
", maps) // 0xc000076180
 ch := make(chan int, 0) fmt.Printf("%p
", ch) // 0xc00006c060}

		

这是因为,它们本身就是指针类型!只不过Go内部为了书写的方便,并没有要求我们在前面加上 符号。

 

在Golang的运行时内部,创建slice的时候其实返回的就是一个指针:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 源码  runtime/slice.go// 返回值是:unsafe.Pointerfunc makeslice(et *_type, len, cap int) unsafe.Pointer { mem, overflow := math.MulUintptr(et.size, uintptr(cap)) if overflow || mem > maxAlloc || len < 0 || len > cap {  // NOTE: Produce a 'len out of range' error instead of a  // 'cap out of range' error when someone does make([]T, bignumber).  // 'cap out of range' is true too, but since the cap is only being  // supplied implicitly, saying len is clearer.  // See golang.org/issue/4085.  mem, overflow := math.MulUintptr(et.size, uintptr(len))  if overflow || mem > maxAlloc || len < 0 {   panicmakeslicelen()  }  panicmakeslicecap() }
 return mallocgc(mem, et, true)}

		

而且返回的指针地址其实就是slice第一个元素的地址(上面的例子也体现了),当然如果slice是一个nil,则返回的是 0x0 的地址。slice在参数传递的时候其实拷贝的指针的地址,底层数据是共用的,所以对其修改也会影响到函数外的slice,在下面也会讲到。

 

map和slice其实也是类似的,在在Golang的运行时内部,创建map的时候其实返回的就是一个hchan指针:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 源码  runtime/chan.go// 返回值是:*hchanfunc makechan(t *chantype, size int) *hchan { elem := t.elem
 // compiler checks this but be safe. if elem.size >= 1<<16 {  throw("makechan: invalid channel element type") } ... return c}

		

最后,为什么 fmt.Printf 函数能够直接打印slice、map的地址,除了上面的原因,还有一个原因是其内部也做了特殊处理:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 第一层源码func Printf(format string, a ...interface{}) (n int, err error) { return Fprintf(os.Stdout, format, a...)}
// 第二层源码func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) { p := newPrinter() p.doPrintf(format, a)  // 核心 n, err = w.Write(p.buf) p.free() return}
// 第三层源码func (p *pp) doPrintf(format string, a []interface{}) {  ... default:   // Fast path for common case of ascii lower case simple verbs   // without precision or width or argument indices.   if 'a' <= c && c <= 'z' && argNum < len(a) {    ...    p.printArg(a[argNum], rune(c))   // 核心是这里    argNum++    i++    continue formatLoop   }   // Format is more complex than simple flags and a verb or is malformed.   break simpleFormat  }
}
// 第四层源码func (p *pp) printArg(arg interface{}, verb rune) { p.arg = arg p.value = reflect.Value{}  ... case 'p':  p.fmtPointer(reflect.ValueOf(arg), 'p')  return } ...}
// 最后了func (p *pp) fmtPointer(value reflect.Value, verb rune) { var u uintptr switch value.Kind() {  // 这里对这些特殊类型直接获取了其地址 case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Slice, reflect.UnsafePointer:  u = value.Pointer() default:  p.badVerb(verb)  return }  ...}

		

2.Go只有值传递,没有引用传递

 

值传递和引用传递相信大家都比较了解,在函数的调用过程中,如果是值传递,则在传递过程中,其实就是将参数的值复制一份传递到函数中,如果在函数内对其修改,并不会影响函数外面的参数值,而引用传递则相反。

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
type User struct { Name string Age  int}
// 引用传递func setNameV1(user *User) { user.Name = "test_v1"}
// 值传递func setNameV2(user User) { user.Name = "test_v2"}
func main() { u := User{Name: "init"} fmt.Println("init", u)  // init {init 0}
 up := &u setNameV1(up) fmt.Println("v1", u) // v1 {test_v1 0}
 setNameV2(u) fmt.Println("v2", u) // v2 {test_v1 0}}

		

但在Golang中,这所谓的“引用传递”其实本质上是值传递,因为这时候也发生了拷贝,只不过这时拷贝的是指针,而不是变量的值,所以“Golang的引用传递其实是引用的拷贝”。

 

可以通过以下代码验证:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
type User struct { Name string Age  int}
// 注意这里有个误区,我一开始看 user(v1)打印后的地址和一开始(init)是一致的,从而以为这是引用传递// 其实这里的user应该看做一个指针变量,我们需要对比的是它的地址,所以还要再取一次地址func setNameV1(user *User) { fmt.Printf("v1: %p
", user)  // 0xc0000a4018  与 init的地址一致 fmt.Printf("v1_p: %p
", &user) // 0xc0000ac020 user.Name = "test_v1"}
// 值传递func setNameV2(user User) { fmt.Printf("v2_p: %p
", &user) //0xc0000a4030 user.Name = "test_v2"}
func main() { u := User{Name: "init"}
 up := &u fmt.Printf("init: %p 
", up) //0xc0000a4018 setNameV1(up) setNameV2(u)}

		

注:slice、map等本质也是如此。

 

3.for range与指针

 

for range是在Golang中用于遍历元素,当它与指针结合时,稍不留神就会踩坑,这里有一段经典代码:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
type User struct { Name string Age  int}
func main() { userList := []User {  User{Name: "aa", Age: 1},  User{Name: "bb", Age: 1}, }
 var newUser []*User for _, u := range userList {  newUser = append(newUser, &u) }
 // 第一次:bb // 第二次:bb for _, nu := range newUser {  fmt.Printf("%+v", nu.Name) }}

		

按照正常的理解,应该第一次输出aa,第二次输出bb,但实际上两次都输出了bb,这是因为 for range 的时候,变量u实际上只初始化了一次(每次遍历的时候u都会被重新赋值,但是地址不变),导致每次append的时候,添加的都是同一个内存地址,所以最终指向的都是最后一个值bb。

 

我们可以通过打印指针地址来验证:


		
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func main() { userList := []User {  User{Name: "aa", Age: 1},  User{Name: "bb", Age: 1}, }
 var newUser []*User for _, u := range userList {  fmt.Printf("point: %p
", &u)  fmt.Printf("val: %s
", u.Name)  newUser = append(newUser, &u) }}
// 最终输出结果如下:point: 0xc00000c030val: aapoint: 0xc00000c030val: bb
 类似的错误在Goroutine也经常发生:
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
// 这里要注意下,理论上这里都应该输出10的,但有可能出现执行到7或者其他值的时候就输出了,所以实际上这里不完全都输出10func main() { for i := 0; i < 10; i++ {  go func(idx *int) {   fmt.Println("go: ", *idx)  }(&i) } time.Sleep(5 * time.Second)}

		

4.闭包与指针

 

什么是闭包,一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

 

当闭包与指针进行结合时,如果闭包里面是一个指针变量,则外部变量的改变,也会影响到该闭包,起到意想不到的效果,让我们继续在举几个例子进行说明:

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func incr1(x *int) func() { return func() {  *x = *x + 1   // 这里是一个指针  fmt.Printf("incr point x = %d
", *x) }}func incr2(x int) func() { return func() {  x = x + 1  fmt.Printf("incr normal x = %d
", x) }}
func main() { x := 1 i1 := incr1(&x) i2 := incr2(x) i1() // point x = 2 i2() // normal x = 2 i1() // point x = 3 i2() // normal x = 3
 x = 100 i1() // point x = 101  // 闭包1的指针变量受外部影响,被重置为100,并继续递增 i2() // normal x = 4 i1() // point x = 102 i2() // normal x = 5}

		

5.指针与内存逃逸

 

内存逃逸的场景有很多,这里只讨论由指针引发的内存逃逸。理想情况下,肯定是尽量减少内存逃逸,因为这意味着GC(垃圾回收)的压力会减小,程序也会运行得更快。不过,使用指针又能减少内存的占用,所以这本质是内存和GC的权衡,需要合理使用。

 

下面是指针引发的内存逃逸的三种场景(欢迎大家补充~)

 

第一种场景:函数返回局部变量的指针

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
type Escape struct { Num1  int Str1  *string Slice []int}
// 返回局部变量的指针func NewEscape() *Escape { return &Escape{}   // &Escape{} escapes to heap}
func main() { e := &Escape{Num1: 0}}

		

第二种场景:被已经逃逸的变量引用的指针

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func main() { e := NewEscape() e.SetNum1(10)
 name := "aa" // e.Str1 中,e是已经逃逸的变量, &name是被引用的指针 e.Str1 = &name  // moved to heap: name}

		

第三种场景:被指针类型的slice、map和chan引用的指针

  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
  •  
func main() { e := NewEscape() e.SetNum1(10)
 name := "aa" e.Str1 = &name
 // 指针类型的slice arr := make([]*int, 2)  n := 10  // moved to heap: n arr[0] = &n // 被引用的指针}

		

欢迎大家继续补充指针的其他注意事项~

  审核编辑:汤梓红

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分