不像Java和.NET,Go语言为程序员提供了控制数据结构的指针的能力;但是,并不能进行指针运算。通过给予程序员基本内存布局,Go语言允许你控制特定集合的数据结构、分配的数量以及内存访问模式,这些对构建运行良好的系统是非常重要的:指针对于性能的影响是不言而喻的,而如果你想要做的是系统编程、操作系统或者网络应用,指针更是不可或缺的一部分。
指针(pointer)概念在Go语言中被拆分为两个核心概念:
类型指针,允许对这个指针类型的数据进行修改。传递数据使用指针,而无须拷贝数据。类型指针不能进行偏移和运算。
切片,由指向起始元素的原始指针、元素数量和容量组成。
受益于这样的约束和拆分,Go语言的指针类型变量拥有指针的高效访问,但又不会发生指针偏移,从而避免非法修改关键性数据问题。同时,垃圾回收也比较容易对不会发生偏移的指针进行检索和回收。
切片比原始指针具备更强大的特性,更为安全。切片发生越界时,运行时会报出宕机,并打出堆栈,而原始指针只会崩溃。
C/C++中的指针
说到C/C++中的指针,会让许多人“谈虎色变”,尤其对指针偏移、运算、转换都非常恐惧。
其实,指针是使C/C++语言有极高性能的根本,在操作大块数据和做偏移时方便又便捷。因此,操作系统依然使用C语言及指针特性进行编写。
C/C++中指针饱受诟病的根本原因是指针运算和内存释放。
C/C++语言中的裸指针可以自由偏移,甚至可以在某些情况下偏移进入操作系统核心区域。我们的计算机操作系统经常需要更新、修复漏洞的本质,是为解决指针越界访问所导致的“缓冲区溢出”。
要明白指针,需要知道几个概念:指针地址、指针类型和指针取值,下面将展开细说。
认识指针地址和指针类型
一个指针变量可以指向任何一个值的内存地址它指向那个值的内存地址,在32位机器上占用4个字节,在64位机器上占用8个字节,并且与它所指向的值的大小无关。当然,可以声明指针指向任何类型的值来表明它的原始性或结构性;你可以在指针类型前面加上*号(前缀)来获取指针所指向的内容,这里的*号是一个类型更改器。使用一个指针引用一个值被称为间接引用。
当一个指针被定义后没有分配到任何变量时,它的值为nil。一个指针变量通常缩写为ptr。
每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。Go语言中使用&作符放在变量前面对变量进行“取地址”操作。
格式如下:
ptr:=&v //v的类型为T
其中v代表被取地址的变量,被取地址的v使用ptr变量进行接收,ptr的类型就为*T,称做T的指针类型。*代表指针。
指针实际用法,通过下面的例子了解:
packagemainimport("fmt")funcmain(){varcatint=1varstrstring="banana"fmt.Printf("%p%p",&cat,&str)}
运行结果:
0xc0420520880xc0420461b0
代码说明如下:
第8行,声明整型cat变量。
第9行,声明字符串str变量。
第10行,使用fmt.Printf的动词%p输出cat和str变量取地址后的指针值,指针值带有0x的十六进制前缀。
输出值在每次运行是不同的,代表cat和str两个变量在运行时的地址。
在32位平台上,将是32位地址;64位平台上是64位地址。
提示:变量、指针和地址三者的关系是:每个变量都拥有地址,指针的值就是地址。
从指针获取指针指向的值
在对普通变量使用&操作符取地址获得这个变量的指针后,可以对指针使用*操作,也就是指针取值,代码如下。
packagemainimport("fmt")funcmain(){//准备一个字符串类型varhouse="MalibuPoint10880,90265"//对字符串取地址,ptr类型为*stringptr:=&house//打印ptr的类型fmt.Printf("ptrtype:%Tn",ptr)//打印ptr的指针地址fmt.Printf("address:%pn",ptr)//对指针进行取值操作value:=*ptr//取值后的类型fmt.Printf("valuetype:%Tn",value)//指针取值后就是指向变量的值fmt.Printf("value:%sn",value)}
运行结果:
ptrtype:*string
address:0xc0420401b0
valuetype:string
value:MalibuPoint10880,90265
代码说明如下:
第10行,准备一个字符串并赋值。
第13行,对字符串取地址,将指针保存到ptr中。
第16行,打印ptr变量的类型,类型为*string。
第19行,打印ptr的指针地址,每次运行都会发生变化。
第22行,对ptr指针变量进行取值操作,value变量类型为string。
第25行,打印取值后value的类型。
第28行,打印value的值。
取地址操作符&和取值操作符*是一对互补操作符,&取出地址,*根据地址取出地址指向的值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:
对变量进行取地址(&)操作,可以获得这个变量的指针变量。
指针变量的值是指针地址。
对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
使用指针修改值
通过指针不仅可以取值,也可以修改值。
前面已经使用多重赋值的方法进行数值交换,使用指针同样可以进行数值交换,代码如下:
packagemainimport"fmt"//交换函数funcswap(a,b*int){//取a指针的值,赋给临时变量tt:=*a//取b指针的值,赋给a指针指向的变量*a=*b//将a指针的值赋给b指针指向的变量*b=t}funcmain(){//准备两个变量,赋值1和2x,y:=1,2//交换变量值swap(&x,&y)//输出变量值fmt.Println(x,y)}
运行结果:
21
代码说明如下:
第6行,定义一个交换函数,参数为a、b,类型都为*int,都是指针类型。
第9行,将a指针取值,把值(int类型)赋给t变量,t此时也是int类型。
第12行,取b指针值,赋给a变量指向的变量。注意,此时*a的意思不是取a指针的值,而是“a指向的变量”。
第15行,将t的值赋给b指向的变量。
第21行,准备x、y两个变量,赋值1和2,类型为int。
第24行,取出x和y的地址作为参数传给swap()函数进行调用。
第27行,交换完毕时,输出x和y的值。
*操作符作为右值时,意义是取指针的值;作为左值时,也就是放在赋值操作符的左边时,表示a指向的变量。其实归纳起来,*操作符的根本意义就是操作指针指向的变量。当操作在右值时,就是取指向变量的值;当操作在左值时,就是将值设置给指向的变量。
如果在swap()函数中交换操作的是指针值,会发生什么情况?可以参考下面代码:
packagemainimport"fmt"funcswap(a,b*int){b,a=a,b}funcmain(){x,y:=1,2swap(&x,&y)fmt.Println(x,y)}
运行结果:
12
结果表明,交换是不成功的。上面代码中的swap()函数交换的是a和b的地址,在交换完毕后,a和b的变量值确实被交换。但和a、b关联的两个变量并没有实际关联。这就像写有两座房子的卡片放在桌上一字摊开,交换两座房子的卡片后并不会对两座房子有任何影响。
示例:使用指针变量获取命令行的输入信息
Go语言的flag包中,定义的指令以指针类型返回。通过学习flag包,可以深入了解指针变量在设计上的方便之处。
下面的代码通过提前定义一些命令行指令和对应变量,在运行时,输入对应参数的命令行参数后,经过flag包的解析后即可通过定义的变量获取命令行的数据。
获取命令行输入:
packagemain//导入系统包import("flag""fmt")//定义命令行参数varmode=flag.String("mode","","processmode")funcmain(){//解析命令行参数flag.Parse()//输出命令行参数fmt.Println(*mode)}
将这段代码命名为main.go,然后使用如下命令行运行:
$gorunflagparse.go–mode=fast
命令行输出结果如下:
fast
代码说明如下:
第10行,通过flag.String,定义一个mode变量,这个变量的类型是*string。后面3个参数分别如下:
参数名称:在给应用输入参数时,使用这个名称。
参数值的默认值:与flag所使用的函数创建变量类型对应,String对应字符串、Int对应整型、Bool对应布尔型等。
参数说明:使用-help时,会出现在说明中。
第15行,解析命令行参数,并将结果写入创建的指令变量中,这个例子中就是mode变量。
第18行,打印mode指针所指向的变量。
由于之前使用flag.String已经注册了一个mode的命令行参数,flag底层知道怎么解析命令行,并且将值赋给mode*string指针。在Parse调用完毕后,无须从flag获取值,而是通过自己注册的mode这个指针,获取到最终的值。代码运行流程如下图所示。
图:命令行参数与变量的关系
创建指针的另一种方法——new()函数
Go语言还提供了另外一种方法来创建指针变量,格式如下:
new(类型)
一般这样写:
str:=new(string)*str="ninja"fmt.Println(*str)
new()函数可以创建一个对应类型的指针,创建过程会分配内存。被创建的指针指向的值为默认值。