Pointer

概述

参考:

在 Go 语言中,Pointer(指针) 可以有两种含义:

  • 通过 & 符号获取一个变量的内存地址,即指针。通常使用十六进制数表示。这种方式称为指针引用
  • 指针也可以表示一种数据类型。可以声明一个指针类型的变量,用以存储内存地址。

一个指针变量可以指向任何一个值的内存地址。这个内存地址,在 32 位机器上占用 4 个字节,在 64 位机器上占用 8 个字节,并且与其所指向的值得的大小无关。 在 Go 语言中,不能进行指针运算。

指针的引用

每一个变量都有指针,我们可以通过 & 符号引用该变量的指针,也就是获取该变量的内存地址。

我们平时说引用指针,并不是引用指针类型的变量,指针类型的变量也是一种变量,正常使用变量名称即可引用。

这里说要说的指针的引用是指引用一个变量的内存地址,即变量的指针 **格式:**在变量名称前加上 & 符号,即可获取该变量的内存地址,即该变量的指针。

&VarID

注意:若该变量的值为空,则该变量依然具有内存地址:

var a string
fmt.Println(&a)

这将会输出:0xc000010250

指针变量的声明

格式:*数据类型 的组合书写,即代表指针类型:

var VarID *TYPE

这里需要注意的是,当一个指针变量被声明后,它的值为nil,但是这个变量本身是具有指针的

func main() {
    // 声明一个 `字符串指针` 类型的变量
 var VarID *string
 fmt.Println(VarID)
    fmt.Println(&VarID)
}

输出结果:

<nil>
0xc000014088

一个指针类型的变量可以保存内存地址,同时自己也具有内存地址。

指针变量的赋值

格式:

var ptr *string
stringVar = "pointer string"
ptr = &stringVar

虽然一个指针类型的变量的值是类似 0xc000010250 这样的内存地址,但是若我们声明了一个字符串指针类型的变量,那么给该变量赋值时,也要使用保存字符串的内存地址;如果使用保存其他类型(比如 int)的内存地址,将会报错:

normalVar := 5
var ptr *string
ptr = &normalVar // 这里将会报错:cannot use &normalVar (value of type *int) as type *string in assignment

因为 *string*int 是两个不同的类型。

指针变量的解除引用

在上面指针变量的赋值中,我们在最后使用指针变量的 Dereferences(解除引用),以获取具体的值。 格式:

*PointerTypeVarID

在某个指针类型的变量前面添加 * 则会解除指针引用,从而获取指针变量中的值,所指向的内存空间中的值。 注意:若指针变量未实例化(即指针变量为 nil),那么解除引用时将会报错:panic: runtime error: invalid memory address or nil pointer dereference。主要错误在于 nil pointer dereference(空指针接触引用)

指针变量的实例化

通过 var 关键字声明的指针变量的默认值为 nil,无法通过解引用的方式为该内存空间填入值,或者获取内存空间的值,因为内存空间不存在,是 nil。 此时可以通过 new() 函数,在声明的指针变量的同时实例化该指针,并为指针变量的值赋予一个值。

ptrNew := new(string)
*ptrNew = "a" // 由于此时 ptrNew 具有内存地址,所以可以直接通过解引用的方式为 ptrNew 内存地址空间内赋予值
fmt.Println(*ptrNew) // 将会输出:a

var 与 new() 的区别

主要区别在于系统是否会为指针变量初始化一个值,var 不会, new() 会 若一个指针变量没有被初始化一个值,那么解除引用将会失败,因为指针变量没有值,也就是说这个指针变量内没有内存地址,此时接触引用将会失败,也就是 nil pointer dereference

var ptr *string
*ptr = "5"
// 只声明未实例化,上面两行报错:panic: runtime error: invalid memory address or nil pointer dereference

ptr := new(string)
*ptr = "5"
// 这两行不报错

TODO

这是什么语法?(*stringValue)(p)

func newStringValue(val string, p *string) *stringValue {
 *p = val
 return (*stringValue)(p)
}

简单示例

func main() {
 normalVar := 5
 // 通过 & 符号取得变量 a 的内存地址,即指向 a 的指针
 fmt.Println("变量 a 的内存地址,即指针为:", &normalVar)

 // 声明一个指针类型的变量
 var ptr *string // 这是一个字符串指针类型的变量
 fmt.Println("刚刚声明的指针没有任何内存地址,默认值为 nil:", ptr)

 // 指针赋值
 // 错误示例:不可以使用 *int 类型给 *string 类型赋值,虽然都是内存地址,但是不可以这么做
 // ptr = &normalVar
 // 正确示例:
 newPtr := strconv.Itoa(normalVar)
 ptr = &newPtr
 fmt.Println("为指针类型变量赋予一个内存地址后,获取该内存地址内保存的值:", *ptr)
}

输出结果:

变量 a 的内存地址,即指针为: 0xc0000ba000
刚刚声明的指针没有任何内存地址,默认值为 nil: <nil>
为指针类型变量赋予一个内存地址后,获取该内存地址内保存的值: 5

通过指针改变变量的值

由于指针的特殊性,我们可以在任何地方修改一个局部变量的值。这就是人们常说的“值传递”和“引用传递”。

package main

import "fmt"

func zeroval(ival int) {
    ival = 0
}

func zeroptr(iptr *int) {
    *iptr = 0
}

func main() {
    i := 1
    fmt.Println("initial:", i)

    zeroval(i)
    fmt.Println("zeroval:", i)

    zeroptr(&i)
    fmt.Println("zeroptr:", i)

    fmt.Println("pointer:", &i)
}

运行结果

$ go run pointers.go
initial: 1
zeroval: 1
zeroptr: 0
pointer: 0x42131100

zeroptr() 函数中,我们将变量 i 的指针传递进去,此时在函数内修改,将会影响变量 i 的值

结构体与指针

TODO:

细嗦 Golang 的指针

原文链接:稀土掘金,细嗦 Golang 的指针

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

1.指针类型的变量

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

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

func main() {
 num := 1
 numP := &num


 *numP = 2
}

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

type Test struct {
 Num int
}


func main() {
 test := Test{Num: 1}
 test.Num = 3
 fmt.Println("v1", test)

 testP := &test
 testP.Num = 4
 fmt.Println("v2", test)
}

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

图二

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

func main() {
 nums := []int{1, 2, 3}
 fmt.Printf("%p\n", nums)     // 0xc0000160c0
 fmt.Printf("%p\n", &nums[0]) // 0xc0000160c0

 maps := map[string]string{"aa": "bb"}
 fmt.Printf("%p\n", maps) // 0xc000076180

 ch := make(chan int, 0)
 fmt.Printf("%p\n", ch) // 0xc00006c060
}

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

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



func makeslice(et *_type, len, cap int) unsafe.Pointer {
 mem, overflow := math.MulUintptr(et.size, uintptr(cap))
 if overflow || mem > maxAlloc || len < 0 || len > cap {





  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 指针:



func makechan(t *chantype, size int) *hchan {
 elem := t.elem


 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:


   if 'a' <= c && c <= 'z' && argNum < len(a) {
    ...
    p.printArg(a[argNum], rune(c))
    argNum++
    i++
    continue formatLoop
   }

   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)

 up := &u
 setNameV1(up)
 fmt.Println("v1", u)

 setNameV2(u)
 fmt.Println("v2", u)
}

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

图三

可以通过以下代码验证:

type User struct {
 Name string
 Age  int
}



func setNameV1(user *User) {
 fmt.Printf("v1: %p\n", user)
 fmt.Printf("v1_p: %p\n", &user)
 user.Name = "test_v1"
}


func setNameV2(user User) {
 fmt.Printf("v2_p: %p\n", &user)
 user.Name = "test_v2"
}

func main() {
 u := User{Name: "init"}

 up := &u
 fmt.Printf("init: %p \n", up)
 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)
 }

 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\n", &u)
  fmt.Printf("val: %s\n", u.Name)
  newUser = append(newUser, &u)
 }
}


point: 0xc00000c030
val: aa
point: 0xc00000c030
val: bb

两种解决方式

使用 for 代替 for...range

func CorrectUsageOfForAndPointer() {
 userList := []User{
  {Name: "aa", Age: 1},
  {Name: "bb", Age: 1},
 }

 var newUser []*User
 for i := 0; i < len(userList); i++ {
  newUser = append(newUser, &userList[i])
 }

 for _, nu := range newUser {
  fmt.Printf("%+v\n", nu.Name)
 }
}

创建一个 Struct,将数组包含在其中

type Users struct {
 Users []*User
}

type User struct {
 Name string
 Age  int
}

// 正确用法一
func CorrectUsageOfForrangeAndPointer() {
 usersList := Users{
  Users: []*User{
   {Name: "aa", Age: 1},
   {Name: "bb", Age: 1},
  },
 }

 var newUser []*User
 for _, u := range usersList.Users {
  newUser = append(newUser, u)
 }

 for _, nu := range newUser {
  fmt.Printf("%+v\n", nu.Name)
 }
}

类似的错误在Goroutine也经常发生:


func 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\n", *x)
 }
}
func incr2(x int) func() {
 return func() {
  x = x + 1
  fmt.Printf("incr normal x = %d\n", x)
 }
}

func main() {
 x := 1
 i1 := incr1(&x)
 i2 := incr2(x)
 i1()
 i2()
 i1()
 i2()

 x = 100
 i1()
 i2()
 i1()
 i2()
}

5.指针与内存逃逸

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

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

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

type Escape struct {
 Num1  int
 Str1  *string
 Slice []int
}


func NewEscape() *Escape {
 return &Escape{}
}

func main() {
 e := &Escape{Num1: 0}
}

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

func main() {
 e := NewEscape()
 e.SetNum1(10)

 name := "aa"

 e.Str1 = &name
}

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

func main() {
 e := NewEscape()
 e.SetNum1(10)

 name := "aa"
 e.Str1 = &name


 arr := make([]*int, 2)
 n := 10
 arr[0] = &n
}

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

参考

又吵起来了,Go 是传值还是传引用?

GO 语言变量逃逸分析


最后修改 June 18, 2024: linux pci, automation expect (c4538067)