更新

在看了另一本书后发现这个问题也能从另一方向理解,即报错本身是因为类型没有实现对应的方法,而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{}

按照上面的说法,使用结构体实现,定义结构体来使用,这是一致的没有问题。使用指针来使用,方法内部同样可以通过指针获取到对应的结构体,因此也没有问题。

Tagged in: