结构体声明
为了说明函数以及方法调用的过程,这里先定义一个struct,在下面的描述中会使用到它。
type Person struct {
Name string
Age uint16
}
普通函数中的参数传递
在Golang中,普通函数的行参如果是值类型,那么调用的实参也必须是实参,反之,如果函数行参是指针类型,那么传入的实参必须是地址。我们来看几个例子。
行参要求值传递
// 定义一个普通函数,要求值传递
func tell(person Person) {
fmt.Printf("Hi, my name's %v, I'm %v years old", person.Name, person.Age)
}
实参按照值的方式传递
// 调用函数,提供一个struct实例
person := Person{"Tom", 28}
tell(person) // Hi, my name's Tom, I'm 28 years old
我们可以看到,这种调用肯定是没有问题的,程序能够正常运行,当然这也是理所当然的
实参按照指针的方式传递
// 调用函数,提供一个struct实例的地址
person := Person{"Tom", 28}
tell(&person) // 编译错误:Cannot use '&person' (type *Person) as type Person
这会直接报编译错误,因为函数要求传入的是一个值,而函数调用给的却是一个地址,是不符合编译器要求的。
行参要求指针传递
这种情况,对于普通函数来讲,与上面的情形类似,即实参必须是地址,而不能是值。这里就不举例了。
方法中的参数传递
由上面我们可以看出,其实对于普通函数而言,参数的传递方式非常直接,而且容易理解。即,函数要求什么,调用的时候就给它什么。
但是对于方法调用来说,就会稍微复杂一些了(其实这种复杂性是为了给程序员提供方便,是不是感觉有点矛盾?哈哈)
对象是值的情况
首先,让我们把上面的函数改成一个方法,并且要求使用值对象的方式传递对象
func (person Person) tell() {
fmt.Printf("Hi, my name's %v, I'm %v years old", person.Name, person.Age)
}
按照我们对于普通函数的理解,调用该方法时,我们必须提供一个值对象,即
person := Person{"Tom", 28}
person.tell() // Hi, my name's Tom, I'm 28 years old
这种方式肯定工作,而且与普通函数的行为一致。但是,接下来,我要给该函数提供一个指针,我们来看
// 得到一个结构体指针
var person *Person = &Person{"Tom", 28}
person.tell() // Hi, my name's Tom, I'm 28 years old
从上面的运行结果,我们看到,这种调用也是被允许的,并没有报错。但是为什么?
这其实由于Go在编译的时候,会帮我们做一些事情,把代码转换成类似下面这个样子
(*person).tell()
即,会把指针类型先进行取值运算,然后再将其传递给方法。有了背后这一个转换,程序员不用在调用的时候先进行取值运算,再调用方法。从这个角度来讲,这种操作是不是给程序员提供了方便?
对象是指针的情况
类似的,如果方法要求传入的对象是指针类型,Go也允许我们传入值对象,Go在背后也是做了一个转换,即把传入的值,先进行地址运算,然后再将其传入方法中。下面详细描述一下。
假设我们的方法改成这样
// 该方法接收一个指针对象
func (person *Person) tell() {
fmt.Printf("Hi, my name's %v, I'm %v years old", person.Name, person.Age)
}
调用的时候,我们可以传入指针,也可以传入值。下面是传入值的情况
person := Person{"Tom", 28}
person.tell() // Hi, my name's Tom, I'm 28 years old
之所以这种调用没有报错,也是因为Go其实在编译的时候,进行了转换
(&person).tell()
在方法内修改对象的属性值,是否会影响原来的对象?
让我们来考虑一种情况,如果方法要求的对象是值对象,即
func (person Person) rename(newName string) {
person.Name = newName
}
而在调用这个方法的时候,调用的对象是个指针,那么下面的程序输出什么?
var person *Person = &Person{"Tom", 28}
person.rename("Jimmy")
person.tell()
答案是Hi, my name's Tom, I'm 28 years old
。名字并没有改变,即,虽然我们传递了一个指针给rename
方法,但是在rename
方法里面对于person的修改,并没有反映到外部的person对象。也就是说,方法内的对象与方法外的对象之间是值拷贝。
接下来,我们再看另外一种情况,即,方法要求传入的对象是指针,但是调用的时候是在值对象上进行访问
func (person *Person) rename(newName string) {
person.Name = newName
}
person := Person{"Tom", 28}
person.rename("Jimmy")
person.tell()
输出的结果是Hi, my name's Jimmy, I'm 28 years old
,也就是说名字改变了,即使我们在调用的时候传入的是值对象。
总结
- 如果方法要求的是值对象,那么无论调用的时候传递的是值还是指针,都是值拷贝
- 如果方法要求的是指针,那么无论调用的时候传递的是值还是指针,都是地址拷贝
- 背后的原理:go在编译的时候会根据方法定义的要求对传入的对象进行取值或取地址运算