满足接口的Go语言结构方法的类型

4

给出以下 Go 代码示例:

package main

import "fmt"

type greeter interface {
    hello()
    goodbye()
}

type tourGuide struct {
    name string
}

func (t tourGuide) hello() {
    fmt.Println("Hello", t.name)
}

func (t *tourGuide) goodbye() {
    fmt.Println("Goodbye", t.name)
}

func main() {
    var t1 tourGuide = tourGuide{"James"}
    t1.hello()   // Hello James
    t1.goodbye() // Goodbye James (same as (&t1).goodbye())

    var t2 *tourGuide = &tourGuide{"Smith"}
    t2.hello()   // Hello Smith
    t2.goodbye() // Goodbye Smith (same as (*t2).hello())

    // illegal: t1 is not assignable to g1 (why?)
    // var g1 greeter = t1

    var g2 greeter = t2
    g2.hello()   // Hello Smith
    g2.goodbye() // Goodbye Smith
}

我能够使用类型为tourGuide的变量t1或指向tourGuide的指针t2调用结构体tourGuide的两种方法。换句话说,我可以使用类型T或*T的变量调用具有T接收器的方法。类似地,如果T是可寻址的,则我可以使用类型T的变量(而不是*T)调用具有*T接收器的方法。我理解编译器在这里处理差异(请参见我的代码注释)。
然而,在实现接口时情况会发生改变。在上面的代码中,greeter接口的变量可以从tourGuide的指针分配,但不能从tourGuide分配。
有人能告诉我为什么会这样吗?为什么我能调用t1.hello()和t1.goodbye(),但是t1对于接口greeter来说不够?

谢谢。但是我的问题是,为什么编译器允许使用值类型和指针来调用具有值接收器和指针接收器的结构体方法,但不提供类似的支持给接口?也许这更像是一个设计问题而不是语言如何工作的问题? - danze
如果这是一个设计问题,那么它不适合在SO上讨论。 - Volker
@Volker 我的问题源于我没有完全理解Go接口,而被接受的答案为我澄清了这一点。这不是一个设计问题。 - danze
3个回答

8
如果一个方法有指针接收器,那么只能使用指针值作为接收器值。因此,要在某个值上调用此方法,该值本身必须是一个指针,或者必须可以获得其地址(用作接收器)。
例如,如果您有一个变量,则它是可寻址的,因此可以获取其地址并将其用作接收器。规范允许您这样做,这会自动发生。
包装在接口中的值是不可寻址的。当创建接口值时,封装在接口中的值会被复制。因此,无法获取其地址。从理论上讲,您可以允许获取副本的地址,但这将是比提供什么更多混淆的源头,因为地址将指向副本,并且具有指针接收器的方法只能修改副本而不是原始对象。
请参见此答案,其中详细说明/证明了在创建接口值时复制值的过程:如何使一个切片包含它自己?

这就是我想要的。现在我明白编译器为什么会这样行事了。谢谢! - danze

3
如果您有一个指向结构体的指针,则Go将允许您访问该结构体及其函数的属性,这些函数具有值类型接收器(而不是指针接收器),而无需对指针进行取消引用操作。但是,这仅适用于一级指针。请看下面的代码,我将t2转换为指向tourguide的指针的指针,此时我需要显式取消引用它,以使其恢复为指向tourguide的指针。将指向结构体的第一级指针视为Go允许您使用语法糖来访问值类型属性和函数的特殊情况,以节省手动取消引用变量的时间。
package main

import "fmt"

type greeter interface {
    hello()
    goodbye()
}

type tourGuide struct {
    name string
}

func (t tourGuide) hello() {
    fmt.Println("Hello", t.name)
}

func (t *tourGuide) goodbye() {
    fmt.Println("Goodbye", t.name)
}

func main() {
    var t1 tourGuide = tourGuide{"James"}
    t1.hello()   // Hello James
    t1.goodbye() // Goodbye James (same as (&t1).goodbye())

    var tmpT2 *tourGuide = &tourGuide{"Smith"}
    var t2 **tourGuide = &tmpT2
    (*t2).hello()   // Hello Smith
    (*t2).goodbye() // Goodbye Smith (same as (*t2).hello())

    //illegal: t1 is not assignable to g1 (why?)
    //var g1 greeter = t1

    //now this is illegal too
    //var g2 greeter = t2

    var g3 greeter = (*t2)
    g3.hello()   // Hello Smith
    g3.goodbye() // Goodbye Smith
}

1

我的回答解释了为什么Go会阻止你获取存储在接口中的值的地址。

简而言之,这是因为指向接口中类型为A的值的指针将在随后存储不同类型B的值时失效。


1
因为你链接的答案(也是你写的)非常好,所以我给它点了赞。不过,我认为应该重新措辞为“我的回答在这里”,以明确其他答案也是你自己写的。 - kapad

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接