指针的概念
指针是存储另一个变量的内存地址的变量。
变量是一种使用方便的占位符,用于引用计算机内存地址。
一个指针变量可以指向任何一个值的内存地址。
在上面的图中,变量b的值为156,存储在内存地址0x1040a124
。变量a持有b的地址,现在a被认为指向b。
区别于C/C++中的指针,Go语言中的指针不能进行偏移和运算,是安全指针。
要搞明白Go语言中的指针需要先知道3个概念:指针地址、指针类型和指针取值。
Go语言中的指针不能进行偏移和运算,因此Go语言中的指针操作非常简单,只需要记住两个符号:&
(取地址)和*
(根据地址取值)。
声明指针
声明指针,*T是指针变量的类型,它指向T类型的值。
1 | go复制代码var var_name *var-type |
var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。
1 | go复制代码var ip *int /* 指向整型*/ |
示例代码:
1 | go复制代码func main() { |
运行结果:
1 | go复制代码a 变量的地址是: 20818a220 |
示例代码:
1 | go复制代码type name int8 |
运行结果:
1 | go复制代码false 1 11 |
未初始化的变量自动赋上初始值
1 | go复制代码type name int8 |
运行结果:
1 | go复制代码false 1 2 &{1 false 2} 1 0xc042068018 1 |
获取指针地址在指针变量前加&的方式
指针地址和指针类型
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&
字符放在变量前面对变量进行“取地址”操作。 Go语言中的值类型(int、float、bool、string、array、struct)都有对应的指针类型,如:*int
、*int64
、*string
等。
取变量指针的语法如下:
1 | go复制代码ptr := &v // v的类型为T 取v的地址 其实就是把v的地址引用给了ptr,此时v和ptr指向了同一块内存地址,任一变量值的修改都会影响另一个变量的值 |
其中:
v
:代表被取地址的变量,类型为T
ptr
:用于接收地址的变量,ptr的类型就为*T
,称做T的指针类型。*代表指针。
1 | go复制代码func main() { |
运行结果:
1 | less复制代码变量a的地址: 0x20818a220 |
b := &a
的图示:
指针取值
在对普通变量使用&操作符取地址后会获得这个变量的指针,然后可以对指针使用*操作,也就是指针取值,代码如下。
1 | go复制代码func main() { |
输出如下:
1 | go复制代码type of b:*int |
总结: 取地址操作符&
和取值操作符*
是一对互补操作符,&
取出地址,*
根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
- 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
- 指针变量的值是指针地址。
- 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
使用指针传递函数的参数
1 | go复制代码func change(val *int) { |
运行结果
1 | go复制代码value of a before function call is 58 |
不要将一个指向数组的指针传递给函数。使用切片。
假设想对函数内的数组进行一些修改,并且对调用者可以看到函数内的数组所做的更改。一种方法是将一个指向数组的指针传递给函数。
1 | go复制代码func modify(arr *[3]int) { |
运行结果
1 | csharp复制代码[90 90 91] |
示例代码:
1 | go复制代码func modify(arr *[3]int) { |
运行结果
1 | csharp复制代码[90 90 91] |
虽然将指针传递给一个数组作为函数的参数并对其进行修改,但这并不是实现这一目标的惯用方法。切片是首选:
1 | go复制代码func modify(sls []int) { |
运行结果:
1 | csharp复制代码[90 90 91] |
Go不支持指针算法。
1 | go复制代码func main() { |
指针数组
有一种情况,我们可能需要保存数组,这样就需要使用到指针。
1 | go复制代码const MAX int = 3 |
指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。
1 | go复制代码func main() { |
指针作为函数参数
1 | go复制代码package main |
空指针
Go 空指针 当一个指针被定义后没有分配到任何变量时,它的值为 nil。 nil 指针也称为空指针。 nil在概念上和其它语言的null、None、nil、NULL一样,都指代零值或空值。 一个指针变量通常缩写为 ptr。
空指针判断:
1 | go复制代码if(ptr != nil) /* ptr 不是空指针 */ |
1 | go复制代码func main() { |
运行这些代码,会看到如下错误信息:
1 | go复制代码panic: runtime error: invalid memory address or nil pointer dereference |
这是因为指针类型的变量如果没有分配内存,就默认是零值 nil,它没有指向的内存,所以无法使用,强行使用就会得到以上 nil 指针错误。
指针使用
- 指针可以修改指向数据的值;
- 在变量赋值,参数传值的时候可以节省内存。
注意事项
- 不要对 map、slice、channel 这类引用类型使用指针;
- 如果需要修改方法接收者内部的数据或者状态时,需要使用指针;
- 如果需要修改参数的值或者内部数据时,也需要使用指针类型的参数;
- 如果是比较大的结构体,每次参数传递或者调用方法都要内存拷贝,内存占用多,这时候可以考虑使用指针;
- 像 int、bool 这样的小数据类型没必要使用指针;
- 如果需要并发安全,则尽可能地不要使用指针,使用指针一定要保证并发安全;
- 指针最好不要嵌套,也就是不要使用一个指向指针的指针,虽然 Go 语言允许这么做,但是这会使代码变得异常复杂。
new 和 make
我们知道对于值类型来说,即使只声明一个变量,没有对其初始化,该变量也会有分配好的内存。
1 | go复制代码func main() { |
结构体也是值类型,比如 var wg sync.WaitGroup
声明的变量 wg ,不进行初始化也可以直接使用,Go 语言自动分配了内存,所以可以直接使用,不会报 nil 异常。
于是可以得到结论:如果要对一个变量赋值,这个变量必须有对应的分配好的内存,这样才可以对这块内存操作,完成赋值的目的。
其实不止赋值操作,对于指针变量,如果没有分配内存,取值操作一样会报 nil 异常,因为没有可以操作的内存。
所以一个变量必须要经过声明、内存分配才能赋值,才可以在声明的时候进行初始化。指针类型在声明的时候,Go 语言并没有自动分配内存,所以不能对其进行赋值操作,这和值类型不一样。map 和 chan 也一样,因为它们本质上也是指针类型。
要分配内存,就引出来了内置函数new()和make()。 Go语言中new和make是内建的两个函数,主要用来分配内存。
new
new是一个内置的函数,它的函数签名如下:
1 | go复制代码// The new built-in function allocates memory. The first argument is a type, |
其中,
- Type表示类型,new函数只接受一个参数,这个参数是一个类型
- *Type表示类型指针,new函数返回一个指向该类型内存地址的指针。
它的作用就是根据传入的类型申请一块内存,然后返回指向这块内存的指针,指针指向的数据就是该类型的零值:
1 | go复制代码func main() { |
通过 new 函数分配内存并返回指向该内存的指针后,就可以通过该指针对这块内存进行赋值、取值等操作。
make
make
也是用于内存分配的,区别于new
,它只用于slice
、map
以及chan
的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
1 | go复制代码func make(t Type, size ...IntegerType) Type |
在使用 make 函数创建 map 的时候,其实调用的是 makemap
函数:
1 | go复制代码// makemap implements Go map creation for make(map[k]v, hint). |
makemap
函数返回的是 *hmap
类型,而 hmap
是一个结构体,它的定义如下所示:
1 | go复制代码// A header for a Go map. |
可以看到, map 关键字其实非常复杂,它包含 map 的大小 count、存储桶 buckets 等。要想使用这样的 hmap,不是简单地通过 new 函数返回一个 *hmap 就可以,还需要对其进行初始化,这就是 make 函数要做的事情:
1 | go复制代码m:=make(map[string]int,10) |
其实 make 函数就是 map 类型的工厂函数,它可以根据传递它的 K-V 键值对类型,创建不同类型的 map,同时可以初始化 map 的大小。
make 函数不只是 map 类型的工厂函数,还是 chan、slice 的工厂函数。它同时可以用于 slice、chan 和 map 这三种类型的初始化。
make函数是无可替代的,在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。
new与make的区别
- 二者都是用来做内存分配的。
- make只用于slice、map以及channel的初始化,返回的还是这三个引用类型本身;
- new用于类型的内存分配,并且内存对应的值为类型零值,返回的是指向对应类型零值的指针。
本文转载自: 掘金