问题提要
之前写代码的时候遇到了一个问题:自己编写了一个接口,然后又写了一个结构体实现这个接口,在通过函数调用接口方法时出现了问题。
代码如下:
type Validator interface {
Valid() bool
}
type LoginInput struct {
Username string
Password string
}
func (input *LoginInput) Valid() bool {
// 一些检验逻辑
// 返回校验结果
}
func Handle(v Validator) {
res := v.Valid()
// 根据校验结果做一些逻辑处理
}
func main() {
// 对具体过程做了提炼,最终逻辑一致
input := LoginInput{Username: "XXX", Password: "YYY"}
Handle(input)
}
在main
中调用Handle()
时传参失败,Goland
提示消息如下:Cannot use 'input' (type LoginInput) as type ValidatorType does not implement 'Validator' as 'Valid' method has a pointer receiver
解决方法其实很简单,就是调用Handle()
时不要传值,要传指针。把调用改成这样就行:Handle(&input)
但这是为什么呢?回去翻了翻书,发现是因为方法集
。
什么是方法集
我们先来看看Golang官方对它的描述:
https://golang.google.cn/ref/spec#Method_sets
A type may have a method set associated with it. The method set of an interface type is its interface. The method set of any other typeT
consists of all methods declared with receiver typeT
. The method set of the corresponding pointer type*T
is the set of all methods declared with receiver*T
orT
(that is, it also contains the method set ofT
). Further rules apply to structs containing embedded fields, as described in the section on struct types. Any other type has an empty method set. In a method set, each method must have a unique non-blank method name.
The method set of a type determines the interfaces that the type implements and the methods that can be called using a receiver of that type.
一个类型会有一个与它关联的方法集。interface类型的方法集就是接口本身。其他任意类型T
的方法集由接收者为T
类型的全部方法构成。对应的指针类型*T
的方法集是由接收者为T
或*T
的全部方法构成的(也就是说,它也包含了T的方法集)。更多的规则应用在包含嵌入字段的结构体上,就像struct types章节中描述的一样。任何其他类型都有一个空的方法集。在方法集中,每个方法必须具有唯一的非空方法名。
类型的方法集确定类型实现的接口以及可以使用该类型的接收器调用的方法。
总结一下官方文档表述的意思,我们得到如下一张表:
变量类型 | 方法接收器类型 |
---|---|
T | (t T) |
*T | (t T) + (t *T) |
对于T
类型,它的方法集只包含接收者类型是T
的方法;而对于*T
类型,它的方法集则包含接收者为T
和*T
类型的方法,也就是全部方法。
只有一个类型的方法集完全涵盖了接口的方法集后,这个类型才会被认为是接口的实现类型。
从这里可以看出来,我们最开始的代码就是因为LoginInput
类型的方法集中没有notify
方法,所以函数调用失败了。
结构体的方法调用与方法集之间的关系
其实到这里就会有个疑问:平时调用方法时,无论变量类型是值类型还是指针类型都能调用成功,也没出过问题啊。
这里其实是Golang的一个语法糖:在使用选择器(Selectors)调用方法时,编译器会帮你做好取址或取值的操作的。
下面通过代码说明一下这个关系:
type StructA struct {}
func (s StructA) ValueReceiver () {}
func (s *StructA) PointReceiver () {}
func main() {
value := StructA{}
point := &value
// 编译器不做处理,就是value.ValueReceiver()
value.ValueReceiver()
// 其实是(&value).ValueReceiver()的简便写法
value.PointReceiver()
// 其实是(*point).ValueReceiver()的简便写法
point.ValueReceiver()
// 编译器不做处理,就是point.ValueReceiver()
point.PointReceiver()
}