在本教程中,我们将学习指针在 Go 中的工作方式,并了解 Go 指针与其他语言(如 C 和 C++)中的指针的区别。
本教程有以下几节。
什么是指针?
声明指针
指针的零值
使用 new 函数创建指针
取消引用指针
将指针传递给函数
从函数返回指针
不要将指向数组的指针作为函数的参数传递,改用切片
Go 不支持指针运算
什么是指针?
指针是存储另一个变量的内存地址的变量。
在上图中,变量 b
具有值 156
并存储在存储器地址 0x1040a124
处。变量 a
保存 b
的地址。现在 a
指向 b
。
声明指针
*T 是指针变量的类型,它指向类型 T 的值。
让我们编写一个声明指针的程序。
package main
import (
"fmt"
)
func main() {
b := 255
var a *int = &b
fmt.Printf("Type of a is %T\n", a)
fmt.Println("address of b is", a)
}
& 运算符用于获取变量的地址。在上面的程序第 9 行中,我们将 b 的地址赋值给类型为 *int 的 a。现在 a 指向 b,当我们输出 a
的值时,会打印出 b
的地址。这个程序输出
Type of a is *int
address of b is 0x1040a124
你可能会得到 b 的不同地址,因为 b 的位置可以在内存中的任何位置。
指针的零值
指针的零值是 nil
。
package main
import (
"fmt"
)
func main() {
a := 25
var b *int
if b == nil {
fmt.Println("b is", b)
b = &a
fmt.Println("b after initialization is", b)
}
}
b 在上面的程序中最初是空的,后来被分配到 a 的地址。这个程序输出
b is <nil>
b after initialisation is 0x1040a124
使用 new 函数创建指针
Go 还提供了一个方便的函数 new
来创建指针。new
函数接受一个 type 作为参数,返回一个新指针(值为这个 type 类型的零值)。
下面这个程序会让你明白得更加清楚。
package main
import (
"fmt"
)
func main() {
size := new(int)
fmt.Printf("Size value is %d, type is %T, address is %v\n", *size, size, size)
*size = 85
fmt.Println("New size value is", *size)
}
在上面的程序第 8 行中。我们使用 new
函数来创建一个int
类型的指针。这个函数将返回一个新分配地址的指针,指向类型 int
的零值。因此 size 类型为 int
,由于int
类型的零值为 0
,它的值为 0
。
以上程序将输出
Size value is 0, type is *int, address is 0x414020
New size value is 85
取消引用指针
取消引用指针意味着访问指针指向的变量的值。*a
是获取 a 指向变量的语法。
让我们看看下面这段代码。
package main
import (
"fmt"
)
func main() {
b := 255
a := &b
fmt.Println("address of b is", a)
fmt.Println("value of b is", *a)
}
在上面程序第 9 行中,我们访问指针 a 指向的变量,正如我们期望的那样,输出为 b。上面程序输出。
address of b is 0x1040a124
value of b is 255
让我们再写一个程序,使用指针改变 b 中的值。
package main
import (
"fmt"
)
func main() {
b := 255
a := &b
fmt.Println("address of b is", a)
fmt.Println("value of b is", *a)
*a++
fmt.Println("new value of b is", b)
}
上面程序的第 12 行,我们将 a 指向的值增加 1,这将改变 b 的值,因为 a 指向 b。因此,b 的值变为 256。程序的输出是
address of b is 0x1040a124
value of b is 255
new value of b is 256
将指针传递给函数
package main
import (
"fmt"
)
func change(val *int) {
*val = 55
}
func main() {
a := 58
fmt.Println("value of a before function call is",a)
b := &a
change(b)
fmt.Println("value of a after function call is", a)
}
在上面的程序第 15 行,我们将指针变量 b 传递给函数 change
,该变量包含 a 的地址。在 change
函数第 8 行中,使用 *val 来更改 a 的值。程序输出
value of a before function call is 58
value of a after function call is 55
从函数返回指针
函数返回局部变量的指针是完全合法的。 Go 编译器足够智能,它将在堆上分配此变量。
package main
import (
"fmt"
)
func hello() *int {
i := 5
return &i
}
func main() {
d := hello()
fmt.Println("Value of d", *d)
}
上面程序第 9 行中,我们从函数 hello
返回局部变量 i
的地址。 在 C 和 C++ 等编程语言中,**这段代码的行为是没有定义的,因为一旦函数 hello
返回,变量 i
就会超出作用域。 **但是在 Go 中,编译器执行转义分析并在堆上分配 i
。 因此,这个程序将输出,
Value of d 5
不要将指向数组的指针作为函数的参数传递,改用切片。
假设我们想对函数内部的数组做一些修改,并且对函数内部数组所做的修改对调用者可见。一种方法是将指向数组的指针作为函数的参数传递。
package main
import (
"fmt"
)
func modify(arr *[3]int) {
(*arr)[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
程序输出 [90 90 91]
在上面程序的第 13 行,我们将数组 a 的地址传给 modify 函数。modify 函数的第 8 行,我们对 arr 取消引用并将 90 分配给数组的第一个元素。这个程序输出 [90 90 91]
a[x] 是 (a)[x] 的简写。所以上面程序中的 (arr)[0] 可以用 arr[0] 代替。让我们用这种简写的语法重写上面的程序。
package main
import (
"fmt"
)
func modify(arr *[3]int) {
arr[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(&a)
fmt.Println(a)
}
程序依旧输出 [90 90 91]
虽然这种将数组指针作为参数传递给函数,并对其进行修改的方法是有效的,但在 Go中我们一般不推荐使用这种方法。我们一般用切片来做这个事情。
让我们使用切片重写上面这个程序
package main
import (
"fmt"
)
func modify(sls []int) {
sls[0] = 90
}
func main() {
a := [3]int{89, 90, 91}
modify(a[:])
fmt.Println(a)
}
在上面程序的第 13 行,我们将一个切片传递给 modify
函数。在 modify
函数中,切片的第一个元素被更改为 90
。该程序输出 [90 90 91]
。因此,不要把指针传递给数组,而是使用切片:)。这段代码更加简洁,并且在 Go 中更加推荐的这种做法:)。
Go 不支持指针运算
虽然指针运算在 C 和 C++ 等语言中都有,但在 Go 中却不支持指针运算。
package main
func main() {
b := [...]int{109, 110, 111}
p := &b
p++
}
上面的程序将抛出编译错误 main.go:6: invalid operation: p++ (non-numeric type *[3]int)
我在 github 中创建了一个项目,它涵盖了我们讨论过的所有内容。