小艾的自留地

Stay foolish, Stay hungry

区别于 C/C++ 的指针,Go 语言的指针不能进行偏移和运算,因此是安全指针。

指针

一个变量在内存中,可以分为两部分:编址(变量的地址)和具体的值。

在概念上,Go 语言的指针和 C 语言一样,当一个变量的值存储的值是其他变量的地址,那么它就是一个指针变量。

所以也可以说指针的本质就是地址

操作指针

前面提到过,因为 Go 语言的指针不能进行偏移和运算,因此指针的使用场景就只有传递了,只需要记住两个操作符即可:

  • &:取址符,也称为引用,通过该操作符可以获取一个变量的地址值。
  • *:取值符,也称为解引用,通过该操作符可以获取一个地址对应的值。

指针的类型

每个变量在内存中都有一个属于自己的地址,不同类型的数据,都可以拥有自己的指针,比如:

  • int => *int,也叫整型指针
  • string => *string,也叫字符串指针
  • struct => *struct,也叫结构体指针

无论是什么类型,占用的内存都一样(32位4个字节, 64位8个字节)。

指针在内存中的表示

按照惯例先来看一段示例代码:

1
2
3
4
5
6
7
8
9
10
func main() {
x := 10
p := &x

// 打印变量 a 的值和变量 a 的地址
fmt.Printf("x: %d, ptr: %p, T: %T \n", x, &x, x)
// 打印变量 b 的值和变量 b 的地址,因为变量b,本身是一个整型指针,不需要再次取址
fmt.Printf("p: %d, ptr: %p , T: %T \n", *p, p, p)
fmt.Println(&p)
}

执行示例代码,输出以下结果:

1
2
3
x: 10, ptr: 0xc0000b2008, T: int 
p: 10, ptr: 0xc0000b2008 , T: *int
0xc0000ac018

在上面的示例代码中,变量 x 通过取址符,获取到自己的编址并赋值给变量 p,因此变量 p 就是一个指针变量,其类型为整型,也可以说变量 p 就是一个整型指针。

因为变量 p 是一个指针变量,所以它也拥有自己的地址,而它的值则是变量 x 的地址。

下面用一张图来解释它们之间的关系:

& 符号的作用是获取变量的地址,* 符号的作用是通过变量的地址获取对应的值。

指针作为函数参数和返回值

指针传递的场景还包括作为函数参数和返回值,下面一一来看下,这两种场景下,都有哪些特点。

作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type T struct {
a int
}

func F(t *T) {
t.a = 11
}

func main() {
var t1 T
println(t1.a)
F(t1)
println(t1.a)
}

运行上面的示例代码,会发现编译失败了:

cannot use t1 (variable of type T) as type *T in argument to F

因为函数 F 接收一个指针作为参数,但是传进去的 t1 并不是一个结构体指针,而是一个结构体,类型不一致,所以导致编译失败。

要解决这个问题很简单,只需要保证形参和实参的类型一致即可,使用 & 取址符将 t1 的地址作为参数传入:

1
F(&t1)

再次运行示例代码:

1
2
0
11

总结:

  • 形参与实参的类型必须一致,否则会编译失败
  • 对形参进行任何修改都会影响到实参

作为返回值

一个问题

方法的 receiver 参数这篇笔记中,当时遇到了一个 Go编译器做指针自动转换的场景,有了上面的基础做铺垫就不难理解了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type T struct {
a int
}

func (t T) M1() {
t.a = 10
}

func (t *T) M2() {
t.a = 11
}

func main() {
var t1 T
println(t1.a) // 0
t1.M1()
println(t1.a) // 0
t1.M2()
println(t1.a) // 11

var t2 = &T{}
println(t2.a) // 0
t2.M1()
println(t2.a) // 0
t2.M2()
println(t2.a) // 11
}

t1 作为一个结构体(非指针),理论上是不能直接调用 receiver 参数类型 *T(指针)的 M2,Go 编译器在背后做了自动转换,使用 & 取址符将t1 变成了结构体指针,也就是 (&t1).M2()

同理,t2 作为一个结构体指针(指针),理论上是不能直接调用 receiver 参数类型 T(非指针)的 M1,同样是Go 编译器在背后做了自动转换,使用 * 取值符将t2 变成了结构体,也就是 (*t2).M1()

评论