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