更新
在看了另一本书后发现这个问题也能从另一方向理解,即报错本身是因为类型没有实现对应的方法,而Go中对于接口方法的实现满足这样一个规律:
对于非接口类型的自定义类型T,其方法集合由所有receiver为T类型的方法组成;而类型*T的方法集合则包含所有receiver为T和*T类型的方法。
因此用结构体实现的接口可以被指针类型使用,但指针实现的接口则不能被结构体类型使用。以下则是为什么不能这样用的原因。
在使用 kratos 框架的时候,发现其 data 层中实现一个 biz 层定义的接口类型,而返回的是指针:
刚使用 Golang 对这里比较好奇,定义接口,实现之后返回的类型必须使用指针吗?而删掉 & 符号,直接返回 struct 则会报错。
// biz 层定义 repo 接口
type GreeterRepo interface {
CreateGreeter(context.Context, *Greeter) error
UpdateGreeter(context.Context, *Greeter) error
}
// data中实现
func (r *greeterRepo) CreateGreeter(ctx context.Context, g *biz.Greeter) error {
return nil
}
func (r *greeterRepo) UpdateGreeter(ctx context.Context, g *biz.Greeter) error {
return nil
}
// NewGreeterRepo .
func NewGreeterRepo(data *Data, logger log.Logger) biz.GreeterRepo {
// 返回指针
return &greeterRepo{
data: data,
log: log.NewHelper(logger),
}
}
这个问题的根本原因在于:Go 语言中的参数传递都是传值。
Go 中实现接口都是隐式的实现,实现了所有方法则算实现了该接口。而实现的方法上的接收者,可以为结构体或指针:
//结构体 greeterRepo
func (r greeterRepo) CreateGreeter(ctx context.Context, g *biz.Greeter) error {
return nil
}
// 指针 *greeterRepo
func (r *greeterRepo) CreateGreeter(ctx context.Context, g *biz.Greeter) error {
return nil
}
接收者本质上也相当于参数传递进了方法里,因此它也要使用值传递,被复制一份。
因此,无论使用 greeterRepo{} 还是 &greeterRepo{} 来调用方法 CreateGreeter 时,都会复制一份自己传入方法中。
但在使用指针实现接口的时候,如果使用 greeterRepo{} 结构体来调用,复制的则是结构体,而方法要求的是指针,系统不能凭空产生一个指针出来。如果获取复制的结构体的指针,那这个指针指向的是副本而不是原本的结构体,这样可能会导致一些不可预期的结果。因此不能允许这样的调用。
但另一种方式则可以,使用结构体作为接收者,实现接口,则可以同时使用指针和结构体作为类型:
// 结构体中实现
func (r greeterRepo) CreateGreeter(ctx context.Context, g *biz.Greeter) error {
return nil
}
func (r greeterRepo) UpdateGreeter(ctx context.Context, g *biz.Greeter) error {
return nil
}
// 可以同时使用两种方式
var repo biz.GreeterRepo = greeterRepo{}
var repo biz.GreeterRepo = &greeterRepo{}
按照上面的说法,使用结构体实现,定义结构体来使用,这是一致的没有问题。使用指针来使用,方法内部同样可以通过指针获取到对应的结构体,因此也没有问题。