指针
# 指针
指针是一个简单的变量,它保存一个值在内存中的存储位置。每个变量都被存储在一个或多个连续的内存位置,称为地址。
var x int32 = 10
var y bool = true
pointerX := &x
pointerY := &y
var pointerZ *string
2
3
4
5
指针的零值是 nil。nil 也是slice、map、函数、channel、interface的零值。这些类型都是指针实现的。nil是一个未定型的标识符,表示某些类型没有值。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&
字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int
、*int64
、*string
等。
Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,我们只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
&
是地址运算符。它位于值类型之前,并返回存储该值的内存位置的地址。*
是间接寻址运算符。它位于指针类型的变量之前,并返回所指向的值。这称为解引用。
func main() {
a := 10
b := &a
fmt.Printf("a:%d ptr:%p\n", a, &a) // a:10 ptr:0xc00001a078
fmt.Printf("b:%p type:%T\n", b, b) // b:0xc00001a078 type:*int
fmt.Println(&b) // 0xc00000e018
}
2
3
4
5
6
7
取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值。
指针传值示例:
func modify1(x int) {
x = 100
}
func modify2(x *int) {
*x = 100
}
func main() {
a := 10
modify1(a)
fmt.Println(a) // 10
modify2(&a)
fmt.Println(a) // 100
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# new和make
func main() {
var a *int
*a = 100
fmt.Println(*a)
var b map[string]int
b["Mike"] = 100
fmt.Println(b)
}
// panic: runtime error: invalid memory address or nil pointer dereference
2
3
4
5
6
7
8
9
10
11
执行上面的代码会引发panic,为什么呢? 在Go语言中对于引用类型的变量,我们在使用的时候不仅要声明它,还要为它分配内存空间,否则我们的值就没办法存储。而对于值类型的声明不需要分配内存空间,是因为它们在声明的时候已经默认分配好了内存空间。要分配内存,就引出来今天的new和make。 Go语言中new和make是内建的两个函数,主要用来分配内存。
# new
new是一个内置的函数,它的函数签名如下:
func new(Type) *Type
其中,
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
new函数不太常用,使用new函数得到的是一个类型的指针,并且该指针对应的值为该类型的零值。举个例子:
func main() {
a := new(int)
b := new(bool)
fmt.Printf("%T\n", a) // *int
fmt.Printf("%T\n", b) // *bool
fmt.Println(*a) // 0
fmt.Println(*b) // false
}
2
3
4
5
6
7
8
# make
make也是用于内存分配的,区别于new,它只用于slice、map以及channel的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。
make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
# new与make的区别
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- 而new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向类型的指针。
Go是一种传值调用的语言,传递给函数的值是副本。对于基本类型、结构体和数组等非指针类型,这意味着被调用的函数不能修改原始值。
尽管当一个指针被传递给一个函数时,该函数会得到该指针的一个副本。但由于指针仍然指向原始数据,所以原始数据可以被调用的函数修改。
这其中有几个相关的含义:
第一个含义:当把一个nil指针传递给一个函数时,不能将这个值改变为非nil的。如果已经对该指针赋值,只能重新赋值。虽然一开始让人困惑,但这是有道理的。因为内存位置是通过传值调用传递给函数的,我们不能改变内存地址,就像我们不能改变int参数的值一样。我们可以用下面的程序来证明这一点:
func failedUpdate(g *int) {
x := 10
g = &x
}
func main() {
var f * int // f is nil
failedUpdate(f)
fmt.Println(f) // print nil
}
2
3
4
5
6
7
8
9
10
我们在main中以一个nil变量f开始。当调用failedUpdate时,将f的值(即nil)复制到名为g的参数中,这意味着g也被设置为nil。然后在failedUpdate中声明一个新的变量x,值为10。接下来将failedUpdate中的g改为指向x。这并没有改变main中的f,当我们退出failedUpdate并返回main时,f仍然是nil。
第二个含义是,如果你希望对指针参数进行的修改在退出函数时依然有效(即函数修改了指针的值),则必须对指针解引用并设置该值。如果仅仅改变了指针(对指针进行赋值),那么只是改变了指针副本,而不是原指针。解引用将新的值放在原始指针和副本指针都指向的内存位置上。下面是一个简短的程序,展示了它是如何工作的:
func failedUpdate(px *int) {
x2 := 20
px = &x2
}
func update(px *int) {
*px = 20
}
func main() {
x := 10
failedUpdate(&x)
fmt.Println(x) // 10
update(&x)
fmt.Println(x) // 20
}
2
3
4
5
6
7
8
9
10
11
12
13
14
💡 指针也是一个变量
如果结构体足够大,使用结构体的指针作为输入参数或返回值可以提高性能。对于所有数据大小来说,将指针传入函数的时间是恒定的,大约是1ns。这不难理解,因为指针的大小对所有数据类型都是一样的。当数据变大时,将值传入函数需要更长的时间。一旦数据达到10MB左右,就需要大约1ms。
在绝大多数情况下,使用指针和数值之间的差异不会大幅影响程序的性能。但是如果你要在函数之间传递兆字节甚至更大的数据,还是考虑使用指针,即使这些数据是不可改变的。