张子阳的博客

首页 读书 技术 店铺 关于
张子阳的博客 首页 读书 技术 店铺 关于

Go中的Receiver和Method Sets

2018-11-27 作者: 张子阳 分类: Go 语言

Go中没有class关键字,如果想将一组方法关联起来,除了将其放到同一个package下面以外,还有一种方式就是为方法指定receiver(receiver为某个类型),此时方法就与receiver类型关联起来了,有点类似于面向对象中类型方法的概念(但又很不同)。这篇文章介绍了指定receiver的两种方式以及相关的method sets概念。

两种类型的receiver

对于方法,可以指定两种类型的 receiver:T和*T。假设有一个struct类型User,则为方法SetName()指定Receiver时,可以使用User或者*User:

type User struct {
    Name string
}

func (x User) SetName(name string) {
    fmt.Printf("x1: %p\n", &x)
    x.Name = name
}

func (x *User) SetName2(name string) {
    fmt.Printf("x2: %p\n", x)
    x.Name = name
}

func (x User) GetName() string {
    return x.Name
}

一个实例胜过前言万语千言,运行一下就可以看出它们的差别:

func main() {
    test1()
}

func test1() {
    u1 := User{Name: "alex"}
    fmt.Printf("u1: %p\n", &u1)
    u1.SetName("jimmy")
    fmt.Println("u1.Name:", u1.Name) // alex

    u2 := &User{Name: "alex"} // u2为指针
    fmt.Printf("u2: %p\n", u2)
    u2.SetName2("jimmy")
    fmt.Println("u2.Name:", u2.Name) // jimmy
}

输出如下:

# go run main.go
u1: 0xc0000601c0
x1: 0xc0000601d0
u1.Name: alex
u2: 0xc0000601f0
x2: 0xc0000601f0
u2.Name: jimmy

当receiver为User时,传入SetName()的x是一个新的User结构;当reciver为*User时,传入SetName2()的仍是test1()中创建的u2。

看到这里,你可能觉得这是显而易见的,因为SetName2()接受和传入的都是指针类型。

但实际上,即使将u1转换为指针,而u2不为指针,结果也是一样的:

func test2() {
    u1 := &User{Name: "alex"}   // u1为指针
    u1.SetName("jimmy")
    fmt.Println("u1.Name:", u1.Name)    // alex

    u2 := User{Name: "alex"}
    u2.SetName2("jimmy")
    fmt.Println("u2.Name:", u2.Name)    // jimmy
}

可见,receiver参数和常规参数是有很大不同的,这里的关系取决于方法定义时参数是T还是*T,而不取决于传入的参数类型,编译器做了自动的转换。

除此以外,如果是常规方法,形参和实参的类型要完全一致,下面这个例子就能说明这个问题:

型参:方法定义时指定的参数;实参:调用方式时实际传入的参数。
func test3() {
    u1 := User{Name: "alex"}
    // cannot use u (type User) as type *User in argument to ShowName
    ShowName2(u1)

    //cannot use u2 (type *User) as type User in argument to ShowName
    u2 := &User{Name: "jimmy"}
    ShowName(u2)
}

func ShowName(u User) {
    fmt.Println("ShowName:", u.Name)
}

func ShowName2(u *User) {
    fmt.Println("ShowName:", u.Name)
}

上面的代码是无法编译的,因为参数类型不对。

method sets 和 interface

不同的receiver类型除了产生上面的影响以外,也影响了类型的method sets,进而影响了类型所实现的接口。关于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 type T consists of all methods declared with receiver type T. The method set of the corresponding pointer type *T is the set of all methods declared with receiver *T or T (that is, it also contains the method set of T).

对于类型 T 来说,它的method set就是所有receiver类型为T的方法。而对于它所对应的指针类型 *T,method set则为所有receiver类型为T和*T的方法。

对于我们的例子来说,User的method set为SetName()、GetName(),*User的method set为SetName()、SetName2()、GetName()。

这个method sets有什么用呢?它影响了类型实现的接口,在Go中不需要显示指定类型实现的接口,只要它的method sets包含了所有interface中定义的方法就可以了。

添加这样的一个interface和接受这个interface的方法:

type IUser interface {
    SetName(string)
    SetName2(string)
    GetName() string
}

func ShowUser(user IUser) {
    fmt.Println(user.GetName())
}

因为u1的method sets不包含SetName2(),也就没能实现IUser接口,而函数ShowUser()接受的是IUser接口,所以编译器就报错了(u2则没有这个问题):

func test4() {
    u1 := User{Name: "alex"}
    u1.SetName2("jimmy")
    ShowUser(u1)    // 编译器报错

    u2 := &User{Name: "alex"}
    u2.SetName2("jimmy")
    ShowUser(u2)
}

由此,应该已经明白了receiver、method sets的相关概念和区别。

感谢阅读,希望这篇文章能给你带来帮助!