Golang指针与nil浅析

Posted Golang语言社区

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang指针与nil浅析相关的知识,希望对你有一定的参考价值。

作者: 人世间

简书: http://www.jianshu.com/u/5qrPPM

共 9060 字,阅读需 23 分钟

Golang指针与nil浅析

曾经听说过一句话,编程的本质就是指针和递归。那会刚开始编码,只是这两个的概念有个感性粗浅的认识。最早接触指针,莫过于C语言了,能否理解用好指针也成为一个合格C语言的基本标志。

Golang也提供了指针,但是go不能进行指针运算,因此相对于C也少了很多复杂度。私以为,go之所以提供指针,并不是为了让你更多和内存打交道,而是提供操作数据的基本桥梁。因为go很多调用,往往复制一份对象,例如函数的参数,如果没有指针,有些情况不得不存在很多副本。

内存和变量

编程语言中一般都会有变量。变量存储一些值。通常我们会对变量声明,赋值,和销毁等操作。

Golang指针与nil浅析

通过编号找东西固然不错,可是有时候我们想直观的知道抽屉里放了什么内容,就给抽屉外面贴上(声明)一个标签(变量名),比如编号5的抽屉式水果,编号7的抽屉式书啦。下次要找书,就直接找到贴有书标签的抽屉即可。

Golang指针与nil浅析

指针

Golang指针与nil浅析

由此可见,&*是是一对相爱相杀的兄弟,他们做着相反的事情。

零值与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可以打印变量的结构。

上面的过程可以用下面的简图来表示:

Golang指针与nil浅析

下面再声明一个指针变量,使用*符号声明一个指针变量。

    // 声明一个指针变量 aPot 其类型也是 string
    var aPot *string
    fmt.Printf("aPot: %p %#v\n", &aPot, aPot) // 输出 aPot: 0xc42000c030 (*string)(nil)

这个过程可以用下面的图表示:

Golang指针与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"
}

普通变量赋值十分简单,无非就是抽屉换一个值啦。

Golang指针与nil浅析

可是如果一个值为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"

Golang指针与nil浅析

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}

上面的内存图如下:

Golang指针与nil浅析

引用类型与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浅析的主要内容,如果未能解决你的问题,请参考以下文章

Golang 空指针nil的方法和数据成员

有没有办法与golang中的任何指针对象进行比较?

golang - mysql 恐慌:运行时错误:无效的内存地址或 nil 指针取消引用

Golang入门到项目实战 | golang指向数组的指针

Go“一个包含nil指针的接口不是nil接口”踩坑

sql golang查询时,无效的内存地址或nil指针取消引用。