我们在前面多次提到过指针及指针类型。例如,*Person
是Person
的指针类型。又例如,表达式&p
的求值结果是p
的指针。方法的接收者类型的不同会给方法的功能带来什么影响?该方法所属的类型又会因此发生哪些潜移默化的改变?现在,我们就来解答第一个问题。至于第二个问题,我会在下一小节予以解答。
指针操作涉及到两个操作符——&
和*
。这两个操作符均有多个用途。但是当它们作为地址操作符出现时,前者的作用是取址,而后者的作用是取值。更通俗地讲,当地址操作符&
被应用到一个值上时会取出指向该值的指针值,而当地址操作符*
被应用到一个指针值上时会取出该指针指向的那个值。它们可以被视为相反的操作。
除此之外,当*
出现在一个类型之前(如*Person
和*[3]string
)时就不能被看做是操作符了,而应该被视为一个符号。如此组合而成的标识符所表达的含义是作为第二部分的那个类型的指针类型。我们也可以把其中的第二部分所代表的类型称为基底类型。例如,*[3]string
是数组类型[3]string
的指针类型,而[3]string
是*[3]string
的基底类型。
好了,我们现在回过头去再看结构体类型Person
。它及其两个方法的完整声明如下:
type Person struct { Name string Gender string Age uint8 Address string } func (person *Person) Grow() { person.Age++ } func (person *Person) Move(newAddress string) string { old := person.Address person.Address = newAddress return old }
注意,Person
的两个方法Grow
和Move
的接收者类型都是*Person
,而不是Person
。只要一个方法的接收者类型是其所属类型的指针类型而不是该类型本身,那么我就可以称该方法为一个指针方法。上面的Grow
方法和Move
方法都是Person
类型的指针方法。
相对的,如果一个方法的接收者类型就是其所属的类型本身,那么我们就可以把它叫做值方法。我们只要微调一下Grow
方法的接收者类型就可以把它从指针方法变为值方法:
func (person Person) Grow() { person.Age++ }
那指针方法和值方法到底有什么区别呢?我们在保留上述修改的前提下编写如下代码:
p := Person{"Robert", "Male", 33, "Beijing"} p.Grow() fmt.Printf("%v\n", p)
这段代码被执行后,标准输出会打印出什么内容呢?直觉上,34
会被打印出来,但是被打印出来的却是33
。这是怎么回事呢?Grow
方法的功能失效了?!
解答这个问题需要引出一条定论:方法的接收者标识符所代表的是该方法当前所属的那个值的一个副本,而不是该值本身。例如,在上述代码中,Person
类型的Grow
方法的接收者标识符person
代表的是p
的值的一个拷贝,而不是p
的值。我们在调用Grow
方法的时候,Go语言会将p
的值复制一份并将其作为此次调用的当前值。正因为如此,Grow
方法中的person.Age++
语句的执行会使这个副本的Age
字段的值变为34
,而p
的Age
字段的值却依然是33
。这就是问题所在。
只要我们把Grow
变回指针方法就可以解决这个问题。原因是,这时的person
代表的是p
的值的指针的副本。指针的副本仍会指向p
的值。另外,之所以选择表达式person.Age
成立,是因为如果Go语言发现person
是指针并且指向的那个值有Age
字段,那么就会把该表达式视为(*person).Age
。其实,这时的person.Age
正是(*person).Age
的速记法。
我们在讲接口的时候说过,如果一个数据类型所拥有的方法集合中包含了某一个接口类型中的所有方法声明的实现,那么就可以说这个数据类型实现了那个接口类型。要获知一个数据类型都包含哪些方法并不难。但是要注意指针方法与值方法的区别。
拥有指针方法Grow
和Move
的指针类型*Person
是接口类型Animal
的实现类型,但是它的基底类型Person
却不是。这样的表象隐藏着另一条规则:一个指针类型拥有以它以及以它的基底类型为接收者类型的所有方法,而它的基底类型却只拥有以它本身为接收者类型的方法。
以上一小节练习题中的类型MyInt
为例,如果Increase
方法是它的指针方法且Decrease
方法是它的值方法,那么*MyInt
类型会拥有这两个方法,而MyInt
类型仅拥有Decrease
方法。再以Person
类型为例。即使我们把Grow
和Move
都改为值方法,*Person
类型也仍会是Animal
接口的实现类型。另一方面,Grow
和Move
中只要有一个是指针方法,Person
类型就不可能是Animal
接口的实现类型。
另外,还有一点需要大家注意,我们在基底类型的值上仍然可以调用它的指针方法。例如,若我们有一个Person
类型的变量bp
,则调用表达式bp.Grow()
是合法的。这是因为,如果Go语言发现我们调用的Grow
方法是bp
的指针方法,那么它会把该调用表达式视为(&bp).Grow()
。实际上,这时的bp.Grow()
是(&bp).Grow()
的速记法。
在Go语言中,与指针有关的操作实际上还有更多。我们也可以依据这些操作玩儿出很多花样。不过就一般的Go语言编程而言,目前讲述的这些知识已经足够了。如果大家想深入下去,可以参看Go语言官方文档和《Go并发编程实战》中的相关章节。