【Go参数传递:值类型、引用类型和指针类型】

修改参数

func main() {
    p := person{name : "zhangsan", age : 18}
    modifyPerson(p)
    
    fmt.Println("name:", p.name, "age:", p.age)
}

func modifyPerson(p person) {
    p.name = "lisi"
    p.age = 19
}

type person struct {
    name string
    age int 
}

// 输出结果
name:zhangsan, age:18    // 发现结果没有被改变,我们改成指针类型试试呢?


modifyPerson(&p)
func modifyPerson(p *person){
	p.name = "lisi"
	p.age = 19
}

// 输出结果
name: lisi, age: 19   // 接收参数修改为指针参数,就可以满足需求了。

值类型

​ 上述示例中,定义的普通变量p是person类型的。在Go语言中,person是一个值类型,而&p获取的指针是*person类型的,即指针类型。那么为什么值类型在参数传递中无法修改呢? 要从内存讲起。

​ 变量的值是存储在内存中的,而内存都有一个编号,称为内存地址。所以要想修改内存中的数据,就要找到这个内存地址。我们来对比值类型变量在函数内外的内存地址,如下

func main() {
    p := person{name : "zhangsan", age : 18}
    fmt.Println("main函数,p的内存地址:", &p)
    modifyPerson(p)
    
    fmt.Println("name:", p.name, "age:", p.age)
}

func modifyPerson(p person) {
    fmt.Println("modifyPerson 函数p的内存地址:", &p)
    p.name = "lisi"
    p.age = 19
}

// 输出结果
main函数,p的内存地址: 0x0000a3020
modifyPerson 函数p的内存地址:0x0000a3040
name:zhangsan, age:18

// 我们发现内存地址不一样,意味着,在modifyPerson函数中修改的参数p和main函数中的变量p不是同一个,这就是为什么我们在modifyPerson函数中修改了参数p,但是在main函数中打印结果中没有修改的原因

​ 导致这种结果的原因是Go语言中函数传参都是值传递。值传递值得是传递原来数据的一份拷贝,而不是原来的数据本身。

image-20211217101053089

​ 调用modifyPerson函数传递变量p的时候,Go语言会拷贝一个p放在新的内存中,这样新的p的内存地址就和原来的不一样了,但是里面的值是一样的。即副本的意思,变量中的数据一样,但是内存地址不一样。

除了struct外,**浮点型、整型、字符串、布尔、数组,这些都是值类型**。

指针类型

​ 指针类型的变量保存的值就是数据对应的内存地址,所以在函数参数传递是传值的原则下,拷贝的值也是内存地址。

func main() {
    p := person{name : "zhangsan", age : 18}
    fmt.Println("main函数,p的内存地址:", &p)
    modifyPerson(&p)
    
    fmt.Println("name:", p.name, "age:", p.age)
}

func modifyPerson(p *person) {
    fmt.Println("modifyPerson 函数p的内存地址:", &p)
    p.name = "lisi"
    p.age = 19
}

// 输出结果
main函数,p的内存地址: 0x0000a3020
modifyPerson 函数p的内存地址:0x0000a3020
name:lisi, age:19

指针类型的参数是永远可以修改原数据的,因为在参数传递时,传递的是内存地址。

提示: 值传递的是指针,即内存地址。通过内存地址可以找到元数据的那块内存,所以修改它也就等于修改了原数据。

引用类型

​ 引用类型,包括map、slice和chan。

map

func main() {
    m := make(map[string]int)
    m["奔跑的蜗牛"] = 18
    fmt.Println("age:" : m["奔跑的蜗牛"])
    modifyMap(m)
    fmt.Println("age:" : m["奔跑的蜗牛"])
}

func modifyMap(m map[string]int) {
    p["奔跑的蜗牛"] = 19
}

// 输出结果
age:18
age:19

// 我们发现修改成功了。为什么没有使用指针,只是使用了map类型的参数,按照Go语言值传递原则,modifyMap函数中map是一个副本,为什么修改成功了呢?

​ 一切原因要从 make 这个Go语言內建的函数说起。在Go语言中,任何创建map的代码(不管是字面量还是make函数)最终调用的都是 runtime.makemap函数

提示: 用字面量或者make函数的方式创建map,并转换成 makemap 函数的调用,这个转换是Go语言编译器自动帮我们做的。

func makmap(t *maptype, hint int, h*hmap) *hamp{
    
}

​ 从源码可以看出,Go语言的map类型本质上就是 *hmap,所以根据替换原则,modifyMap(a map) 函数其实就是modifyMap(a *hmap)。 这就和上面的指针类型的参数调用一样了,也就是通过map类型的参数可以修改原始数据的原因,因为本质上就是指针。

​ 为了验证创建的map是一个指针,修改上述示例,

func main() {
    m := make(map[string]int)
    m["奔跑的蜗牛"] = 18
    fmt.Println("age:" : m["奔跑的蜗牛"])
    fmt.Println("main函数内存地址:", m)
    modifyMap(m)
    fmt.Println("age:" : m["奔跑的蜗牛"])
}

func modifyMap(m map[string]int) {
    p["奔跑的蜗牛"] = 19
    fmt.Println("modifyMap函数内存地址:", m)
}

// 输出结果
age:18
main函数内存地址:0x000060170
age:19
modifyMap函数内存地址:0x000060170

// 从输出结果看,内存地址一模一样,所以可以修改原始数据。而且在打印指针的时候,直接使用的是变量m和p,并没有用取地址符&,因为他们本省就是指针,所以没必要在使用&取地址了

​ Go语言通过make函数或字面量的包装为我们省去了指针的操作,让我们可以更容易的使用map。其实就是语法糖,这是编程界的老传统了。

注意: 这里的map可以理解为引用类型, 但是它本质是指针,只是可以叫做引用类型而已。在参数传递时,它还是值传递,并不是其他编程语言中所谓的引用传递。

chan

​ channel也可以理解为引用类型, 而它本质也是一个指针。

func makechan(t *chantype, size int64) *hchan{}

// 从源码可以看到,所创建的chan其实是个*hchan,所以它在参数传递中也和map一样

严格的说,Go语言没有引用类型,但是我们可以把map、chan称为引用类型,这样便于理解。除了map、chan外,Go语言中的函数、接口、slice切片都可以称为引用类型。

类型零值

​ 在Go语言中,定义变量要么通过声明、要么通过make和new函数,不一样的是make和new函数属于显示声明并初始化。如果我们声明的变量没有显示声明初始化,那么该变量的默认值就是对应类型的零值。

类型零值
数值类型(int、float)0
boolfalse
string“”(空字符串)
struct内部字段零值
slicenil
mapnil
指针nil
函数nil
channil
interfacenil

总结:在Go语言中,函数的参数传递只有值传递,而且传递的实参都是原始数据的一份拷贝。如果拷贝的内容是值类型的,那么在函数中无法修改原始数据,如果拷贝的内容是指针(或者可以理解为引用类型),那么可以在函数中修改原始数据。