Golang指针与nil浅析
Posted Golang语言社区
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang指针与nil浅析相关的知识,希望对你有一定的参考价值。
作者: 人世间
简书: http://www.jianshu.com/u/5qrPPM
共 9060 字,阅读需 23 分钟
曾经听说过一句话,编程的本质就是指针和递归。那会刚开始编码,只是这两个的概念有个感性粗浅的认识。最早接触指针,莫过于C语言了,能否理解用好指针也成为一个合格C语言的基本标志。
Golang也提供了指针,但是go不能进行指针运算,因此相对于C也少了很多复杂度。私以为,go之所以提供指针,并不是为了让你更多和内存打交道,而是提供操作数据的基本桥梁。因为go很多调用,往往复制一份对象,例如函数的参数,如果没有指针,有些情况不得不存在很多副本。
内存和变量
编程语言中一般都会有变量。变量存储一些值。通常我们会对变量声明,赋值,和销毁等操作。
通过编号找东西固然不错,可是有时候我们想直观的知道抽屉里放了什么内容,就给抽屉外面贴上(声明)一个标签(变量名),比如编号5的抽屉式水果,编号7的抽屉式书啦。下次要找书,就直接找到贴有书标签的抽屉即可。
指针
由此可见,&
和*
是是一对相爱相杀的兄弟,他们做着相反的事情。
零值与nil
talk is cheaper,下面来看看golang中的指针相关操作
package main
import "fmt"
func main() {
// 声明一个变量 aVar 类型为 string
var aVar string
fmt.Printf("aVar: %p %#v\n", &aVar, aVar) // 输出 aVar: 0xc42000e240 ""
}
fmt.Printf 函数可以通过格式化字串打印出变量,
p
表示可以打印指针,v
可以打印变量的值,#v
可以打印变量的结构。
上面的过程可以用下面的简图来表示:
下面再声明一个指针变量,使用*
符号声明一个指针变量。
// 声明一个指针变量 aPot 其类型也是 string
var aPot *string
fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)
这个过程可以用下面的图表示:
正常的变量初始化之后,可以使用=
赋值:
func main() {
// 声明一个变量 aVar 类型为 string
var aVar string
fmt.Printf("aVar: %p %#v\n", &aVar, aVar) // 输出 aVar: 0xc42000e240 ""
aVar = "This is a aVar"
fmt.Printf("aVar: %p %#v\n", &aVar, aVar) // 输出 aVar: 0xc42000e240 "This is a aVar"
}
普通变量赋值十分简单,无非就是抽屉换一个值啦。
可是如果一个值为nil的指针变量,直接赋值会出问题。
func main(){
// 声明一个指针变量 aPot 其类型也是 string
var aPot *string
fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)
*aPot = "This is a Pointer" // 报错: panic: runtime error: invalid memory address or nil pointer dereference
}
// 声明一个指针变量 aPot 其类型也是 string
var aPot *string
fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)
aPot = &aVar
*aPot = "This is a Pointer"
fmt.Printf("aVar: %p %#v \n", &aVar, aVar) // 输出 aVar: 0xc42000e240 "This is a Pointer"
fmt.Printf("aPot: %p %#v %#v \n", &aPot, aPot, *aPot) // 输出 aPot: 0xc42000c030 (*string)(0xc42000e240) "This is a Pointer"
new 关键字
var aNewPot *int
aNewPot = new(int)
*aNewPot = 217
fmt.Printf("aNewPot: %p %#v %#v \n", &aNewPot, aNewPot, *aNewPot) // 输出 aNewPot: 0xc42007a028 (*int)(0xc42006e1f0) 217
new 不仅可以为简单类型开辟内存,也可以为复合引用类型开辟,不过后者初始化的零值还是nil,如果需要赋值,还会有别的问题,下面我们再讨论。
复合类型与nil
int,string等是基础类型,array则是基于这些基础类型的复合类型。复合类型的指针初始化也需要注意:
var arr [5]int
fmt.Printf("arr: %p %#v \n", &arr, arr) // arr: 0xc420014180 [5]int{0, 0, 0, 0, 0}
arr[0], arr[1] = 1, 2
fmt.Printf("arr: %p %#v \n", &arr, arr) // arr: 0xc420014180 [5]int{1, 2, 0, 0, 0}
声明一个大小为5的数组,go会自动为数组的item初始化为零值,数组可以通过索引读取和赋值。
如果声明的是一个数组指针,即一个指针的类型是数组,这个指针如何初始化和赋值呢?
var arrPot *[5]int
fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 (*[5]int)(nil)
从输出可以看到,arrPot初始化的值是nil
。我们已经了解,nil的值是不能直接赋值的,因此(*arrPot)[0] = 11
直接赋值会抛错。
var arrPot *[5]int
fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 (*[5]int)(nil)
arrPot = new([5]int)
fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 &[5]int{0, 0, 0, 0, 0}
(*arrPot)[0] = 11
fmt.Printf("arrPot: %p %#v \n", &arrPot, arrPot) // arrPot: 0xc42000c040 &[5]int{11, 0, 0, 0, 0}
上面的内存图如下:
引用类型与nil
Go的array是虽然是复合类型,但不是引用类型。go中的引用类似是slice,map等。下面我们就看看map类型如何初始化已经对nil的处理。
var aMap map[string]string
fmt.Printf("aMap: %p %#v \n", &aMap, aMap) // aMap: 0xc42000c048 map[string]string(nil)
make 关键字
既然无法使用new,那么go提供了另外一个函数make。make不仅可以开辟一个内存,还能给这个内存的类型初始化其零值,同时返回这个内存实例。
aMap = make(map[string]string)
aMap["name"] = "Golang"
fmt.Printf("aMap: %p %#v \n", &aMap, aMap) // aMap: 0xc420078038 map[string]string{"name":"Golang"}
new 和 make
经过上面的case,相信再面对map类型的指针,也一样可以通过new和make配合完成初始化工作。
var mapPot *map[string]int
fmt.Printf("mapPot: %p %#v \n", &mapPot, mapPot) // mapPot: 0xc42000c050 (*map[string]int)(nil)
// 初始化map指针的地址
mapPot = new(map[string]int)
fmt.Printf("mapPot: %p %#v \n", &mapPot, mapPot) // mapPot: 0xc42000c050 &map[string]int(nil)
//(*mapPot)["age"] = 21 // 报错
// 初始化map指针指向的map
(*mapPot) = make(map[string]int)
(*mapPot)["age"] = 21
fmt.Printf("mapPot: %p %#v \n", &mapPot, mapPot) // mapPot: 0xc42000c050 &map[string]int{"age":21}
Make除了可以初始化map,还可以初始化slice和channel,以及基于这三种类型的自定义类型。
type User map[string]string
var user User
fmt.Printf("user: %p %#v \n", &user, user) // user: 0xc42000c060 main.User(nil)
user = make(User)
user["name"] = "Golang"
fmt.Printf("user: %p %#v \n", &user, user) // user: 0xc42007a050 main.User{"name":"Golang"}
var userPot *User
fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 (*main.User)(nil)
userPot = new(User)
fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 &main.User(nil)
(*userPot) = make(User)
fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 &main.User{}
(*userPot)["name"] = "Golang"
fmt.Printf("userPot: %p %#v \n", &userPot, userPot) // userPot: 0xc42000c068 &main.User{"name":"Golang"}
可见,再复杂的类型,只要弄清楚了指针与nil的关系,配合new和make就能轻松的给golang的数据类型进行初始化。
方法中的指针
Go可以让我自定义类型,而类型又可以创建方法。与OOP类似,方法接受一个类型的实例对象,称之为接受者,接受者既可以是类型的实例变量,也可以是类型的实例指针变量。
func main(){
person := Person{"vanyar", 21}
fmt.Printf("person<%s:%d>\n", person.name, person.age)
person.sayHi()
person.ModifyAge(210)
person.sayHi()
}
type Person struct {
name string
age int
}
func (p Person) sayHi() {
fmt.Printf("SayHi -- This is %s, my age is %d\n",p.name, p.age)
}
func (p Person) ModifyAge(age int) {
fmt.Printf("ModifyAge")
p.age = age
}
输出如下:
person<vanyar:21>
SayHi -- This is vanyar, my age is 21
ModifyAgeSayHi -- This is vanyar, my age is 21
尽管 ModifyAge 方法修改了其age字段,可是方法里的p是person变量的一个副本,修改的只是副本的值。下一次调用sayHi方法的时候,还是person的副本,因此修改方法并不会生效。
也许有人会想,方法会拷贝实例变量,如果实例变量是一个指针,不就轻而易举的修改了么?
personPot := &Person{"noldor", 27}
fmt.Printf("personPot<%s:%d>\n", personPot.name, personPot.age)
personPot.sayHi()
personPot.ModifyAge(270)
personPot.sayHi()
输出如下:
personPot<noldor:27>
SayHi -- This is noldor, my age is 27
ModifyAgeSayHi -- This is noldor, my age is 27
可见并没有效果,实际上,go的确实copy里personPot,只不过会根据接受者是值还是指针类型做一个自动转换,然后再拷贝转换后的对象。即personPot.ModifyAge(270)
实际上等同于(*personPot).ModifyAge(270)
,也就是拷贝的是(*personPot)。与personPot本身是值还是指针没有关系。
真正能修改对象的方式是设置指针类型的接受者。指针类型的接受者,如果实例对象是值,那么go会转换成指针,然后再拷贝,如果本身就是指针对象,那么就直接拷贝指针实例。因为指针都指向一处值,自然就能修改对象了。代码如下:
func (p *Person) ChangeAge(age int) {
fmt.Printf("ModifyAge")
p.age = age
}
Go会根据Person的示例类型,转换成指针类型再拷贝,即 person.ChangeAge会变成 (&person).ChangeAge。
总结
Golang是一门简洁的语言,提供了指针用于操作数据内存,并通过引用来修改变量。
只声明未赋值的变量,golang都会自动为其初始化为零值,基础数据类型的零值比较简单,引用类型和指针的零值都为nil,nil类型不能直接赋值,因此需要通过new开辟一个内存,或者通过make初始化数据类型,或者两者配合,然后才能赋值。
以上是关于Golang指针与nil浅析的主要内容,如果未能解决你的问题,请参考以下文章