在本教程中,我们将学习指针在 Go 中的工作方式,并了解 Go 指针与其他语言(如 C 和 C++)中的指针的区别。

本教程有以下几节。

  • 什么是指针?

  • 声明指针

  • 指针的零值

  • 使用 new 函数创建指针

  • 取消引用指针

  • 将指针传递给函数

  • 从函数返回指针

  • 不要将指向数组的指针作为函数的参数传递,改用切片

  • Go 不支持指针运算

什么是指针?

指针是存储另一个变量的内存地址的变量。

第十五部分:指针 - 图1

在上图中,变量 b 具有值 156 并存储在存储器地址 0x1040a124 处。变量 a 保存 b 的地址。现在 a 指向 b

声明指针

*T 是指针变量的类型,它指向类型 T 的值。

让我们编写一个声明指针的程序。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. b := 255
  7. var a *int = &b
  8. fmt.Printf("Type of a is %T\n", a)
  9. fmt.Println("address of b is", a)
  10. }

Run in playground

& 运算符用于获取变量的地址。在上面的程序第 9 行中,我们将 b 的地址赋值给类型为 *int 的 a。现在 a 指向 b,当我们输出 a 的值时,会打印出 b 的地址。这个程序输出

  1. Type of a is *int
  2. address of b is 0x1040a124

你可能会得到 b 的不同地址,因为 b 的位置可以在内存中的任何位置。

指针的零值

指针的零值是 nil

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. a := 25
  7. var b *int
  8. if b == nil {
  9. fmt.Println("b is", b)
  10. b = &a
  11. fmt.Println("b after initialization is", b)
  12. }
  13. }

Run in playground

b 在上面的程序中最初是空的,后来被分配到 a 的地址。这个程序输出

  1. b is <nil>
  2. b after initialisation is 0x1040a124

使用 new 函数创建指针

Go 还提供了一个方便的函数 new 来创建指针。new 函数接受一个 type 作为参数,返回一个新指针(值为这个 type 类型的零值)。

下面这个程序会让你明白得更加清楚。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. size := new(int)
  7. fmt.Printf("Size value is %d, type is %T, address is %v\n", *size, size, size)
  8. *size = 85
  9. fmt.Println("New size value is", *size)
  10. }

Run in playground

在上面的程序第 8 行中。我们使用 new 函数来创建一个int 类型的指针。这个函数将返回一个新分配地址的指针,指向类型 int 的零值。因此 size 类型为 int ,由于int 类型的零值为 0,它的值为 0

以上程序将输出


  1. Size value is 0, type is *int, address is 0x414020
  2. New size value is 85

取消引用指针

取消引用指针意味着访问指针指向的变量的值。*a 是获取 a 指向变量的语法。

让我们看看下面这段代码。


  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. b := 255
  7. a := &b
  8. fmt.Println("address of b is", a)
  9. fmt.Println("value of b is", *a)
  10. }

Run in playground

在上面程序第 9 行中,我们访问指针 a 指向的变量,正如我们期望的那样,输出为 b。上面程序输出。


  1. address of b is 0x1040a124
  2. value of b is 255

让我们再写一个程序,使用指针改变 b 中的值。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func main() {
  6. b := 255
  7. a := &b
  8. fmt.Println("address of b is", a)
  9. fmt.Println("value of b is", *a)
  10. *a++
  11. fmt.Println("new value of b is", b)
  12. }

上面程序的第 12 行,我们将 a 指向的值增加 1,这将改变 b 的值,因为 a 指向 b。因此,b 的值变为 256。程序的输出是

  1. address of b is 0x1040a124
  2. value of b is 255
  3. new value of b is 256


将指针传递给函数


  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func change(val *int) {
  6. *val = 55
  7. }
  8. func main() {
  9. a := 58
  10. fmt.Println("value of a before function call is",a)
  11. b := &a
  12. change(b)
  13. fmt.Println("value of a after function call is", a)
  14. }

Run in playground

在上面的程序第 15 行,我们将指针变量 b 传递给函数 change,该变量包含 a 的地址。在 change 函数第 8 行中,使用 *val 来更改 a 的值。程序输出

  1. value of a before function call is 58
  2. value of a after function call is 55

从函数返回指针

函数返回局部变量的指针是完全合法的。 Go 编译器足够智能,它将在堆上分配此变量。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func hello() *int {
  6. i := 5
  7. return &i
  8. }
  9. func main() {
  10. d := hello()
  11. fmt.Println("Value of d", *d)
  12. }

Run in playground

上面程序第 9 行中,我们从函数 hello 返回局部变量 i 的地址。 在 C 和 C++ 等编程语言中,**这段代码的行为是没有定义的,因为一旦函数 hello 返回,变量 i 就会超出作用域。 **但是在 Go 中,编译器执行转义分析并在堆上分配 i。 因此,这个程序将输出,


  1. Value of d 5

不要将指向数组的指针作为函数的参数传递,改用切片。

假设我们想对函数内部的数组做一些修改,并且对函数内部数组所做的修改对调用者可见。一种方法是将指向数组的指针作为函数的参数传递。


  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func modify(arr *[3]int) {
  6. (*arr)[0] = 90
  7. }
  8. func main() {
  9. a := [3]int{89, 90, 91}
  10. modify(&a)
  11. fmt.Println(a)
  12. }

Run in playground

程序输出 [90 90 91]

在上面程序的第 13 行,我们将数组 a 的地址传给 modify 函数。modify 函数的第 8 行,我们对 arr 取消引用并将 90 分配给数组的第一个元素。这个程序输出 [90 90 91]

a[x] 是 (a)[x] 的简写。所以上面程序中的 (arr)[0] 可以用 arr[0] 代替。让我们用这种简写的语法重写上面的程序。

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func modify(arr *[3]int) {
  6. arr[0] = 90
  7. }
  8. func main() {
  9. a := [3]int{89, 90, 91}
  10. modify(&a)
  11. fmt.Println(a)
  12. }

Run in playground

程序依旧输出 [90 90 91]

虽然这种将数组指针作为参数传递给函数,并对其进行修改的方法是有效的,但在 Go中我们一般不推荐使用这种方法。我们一般用切片来做这个事情。

让我们使用切片重写上面这个程序

  1. package main
  2. import (
  3. "fmt"
  4. )
  5. func modify(sls []int) {
  6. sls[0] = 90
  7. }
  8. func main() {
  9. a := [3]int{89, 90, 91}
  10. modify(a[:])
  11. fmt.Println(a)
  12. }

Run in playground

在上面程序的第 13 行,我们将一个切片传递给 modify 函数。在 modify 函数中,切片的第一个元素被更改为 90 。该程序输出 [90 90 91]因此,不要把指针传递给数组,而是使用切片:)。这段代码更加简洁,并且在 Go 中更加推荐的这种做法:)。


Go 不支持指针运算

虽然指针运算在 C 和 C++ 等语言中都有,但在 Go 中却不支持指针运算。

  1. package main
  2. func main() {
  3. b := [...]int{109, 110, 111}
  4. p := &b
  5. p++
  6. }

Run in playground

上面的程序将抛出编译错误 main.go:6: invalid operation: p++ (non-numeric type *[3]int)

我在 github 中创建了一个项目,它涵盖了我们讨论过的所有内容。

原文链接

https://golangbot.com/pointers/