Go接口字段

170

我知道在Go语言中,接口定义的是功能而非数据。你可以将一组方法放入接口中,但是无法指定任何实现该接口的对象必须包含哪些字段。

例如:

// Interface
type Giver interface {
    Give() int64
}

// One implementation
type FiveGiver struct {}

func (fg *FiveGiver) Give() int64 {
    return 5
}

// Another implementation
type VarGiver struct {
    number int64
}

func (vg *VarGiver) Give() int64 {
    return vg.number
}

现在我们可以使用接口及其实现:

// A function that uses the interface
func GetSomething(aGiver Giver) {
    fmt.Println("The Giver gives: ", aGiver.Give())
}

// Bring it all together
func main() {
    fg := &FiveGiver{}
    vg := &VarGiver{3}
    GetSomething(fg)
    GetSomething(vg)
}

/*
Resulting output:
5
3
*/

现在,您不能做的是像这样:

type Person interface {
    Name string
    Age int64
}

type Bob struct implements Person { // Not Go syntax!
    ...
}

func PrintName(aPerson Person) {
    fmt.Println("Person's name is: ", aPerson.Name)
}

func main() {
    b := &Bob{"Bob", 23}
    PrintName(b)
}

然而,在尝试使用接口和嵌入结构体后,我发现了一种方法来做到这一点:

type PersonProvider interface {
    GetPerson() *Person
}

type Person struct {
    Name string
    Age  int64
}

func (p *Person) GetPerson() *Person {
    return p
}

type Bob struct {
    FavoriteNumber int64
    Person
}

由于嵌入结构体,Bob拥有了Person的所有内容。同时,它也实现了PersonProvider接口,因此我们可以将Bob传递给那些设计为使用该接口的函数。

func DoBirthday(pp PersonProvider) {
    pers := pp.GetPerson()
    pers.Age += 1
}

func SayHi(pp PersonProvider) {
    fmt.Printf("Hello, %v!\r", pp.GetPerson().Name)
}

func main() {
    b := &Bob{
        5,
        Person{"Bob", 23},
    }
    DoBirthday(b)
    SayHi(b)
    fmt.Printf("You're %v years old now!", b.Age)
}

这里有一个Go Playground,可以演示上面的代码。

使用这种方法,我可以创建一个定义数据而非行为的接口,并且任何构造体只要嵌入该数据就能够实现该接口。你可以定义与这个嵌入式数据进行显式交互的函数,这些函数不知道外部结构体的性质。并且一切都在编译时进行检查!(我唯一能想到的搞砸的方法是将接口PersonProvider嵌入到Bob中,而不是具体的Person。它会编译但在运行时失败。)

现在,我的问题是:这是一个好方法,还是我应该采用其他方式?


8
我可以制作一个接口来定义数据,而不是行为。我认为你有一个返回数据的行为。 - jmaloney
我将写一个答案;如果你需要并知道后果,我认为这是可以的,但是有后果,我不会一直这样做。 - twotwotwo
1
这不是“答案”材料。我通过谷歌搜索“interface as struct property golang”找到了你的问题。我发现一种类似的方法,即将实现接口的结构设置为另一个结构的属性。这是游乐场链接,https://play.golang.org/p/KLzREXk9xo。感谢你给了我一些想法。 - Dale
嵌入式结构体不就是组合吗? - Alexander Mills
7
回顾过去,使用 Go 语言 5 年后,我认为上述代码不符合 Go 的惯用写法。这是一种力图实现泛型的尝试。如果你也有类似的想法,请重新思考你系统的架构。接受接口,返回结构体,通过通信共享,享受编程吧。 - Matt Mc
显示剩余4条评论
2个回答

66

这绝对是个巧妙的技巧。然而,暴露指针仍然使数据直接可访问,所以它只为未来的更改提供了有限的灵活性。此外,Go 的惯例并不要求您总是将一个抽象放在数据属性的前面

综合考虑这些因素,我倾向于根据特定用例选择以下极端之一:a) 只需创建一个公共属性(如果适用,可以使用嵌入)并传递具体类型,或者 b) 如果暴露数据似乎会复杂化您认为可能出现的某些实现更改,请通过方法公开它。您将根据每个属性进行权衡。

如果您犹豫不决,并且该接口仅在项目内部使用,则可以倾向于暴露裸属性:如果它以后给您带来麻烦,重构工具 可以帮助您找到所有对它的引用以更改为 getter/setter。


使用getter和setter隐藏属性可以为您提供额外的灵活性,以便稍后进行向后兼容的更改。比如说,如果您有一天想要将Person存储的不仅仅是单个“name”字段,而是first/middle/last/prefix,如果您有方法Name() stringSetName(string),则可以在添加新的细粒度方法的同时保持现有Person接口的用户满意。或者您可能希望能够在具有未保存更改的情况下将基于数据库的对象标记为“脏”;当数据更新都通过SetFoo()方法进行时,您可以这样做。(您也可以用其他方式来实现,例如将原始数据存储在某个地方并在调用Save()方法时进行比较。)
因此,使用getter/setter,您可以在保持兼容API的同时更改结构字段,并在属性获取/设置周围添加逻辑,因为没有人可以只是通过您的代码进行p.Name = "bob"操作。

当类型很复杂(且代码库庞大)时,灵活性更加重要。如果你有一个PersonCollection,它可能内部由sql.Rows[]*Person[]uint数据库ID等支持。使用正确的接口,您可以使调用者不必关心它是什么,就像io.Reader让网络连接和文件看起来一样。

其中一个特定的事情:Go中的interface具有奇特的属性,即您可以实现一个而不导入定义它的包;这可以帮助您避免循环导入。如果您的接口返回一个*Person,而不仅仅是字符串或其他内容,所有的PersonProviders都必须导入定义Person的包。这可能是可以接受的,甚至是不可避免的;这只是需要知道的一个结果。


但是,Go社区并没有强制要求在类型的公共API中不暴露数据成员。在某些情况下,使用公共属性作为API的一部分是否合理取决于你的判断,而不是避免任何暴露,因为它可能会使实现更改变得复杂或无法实现。
例如,stdlib 允许你使用自己的配置初始化 http.Server,并承诺一个零 bytes.Buffer 是可用的。如果更具体、暴露数据的版本似乎可行,那么自己的东西做起来也可以,我认为并不应该预先抽象化它们,只需要意识到权衡的问题即可。

还有一件事:嵌入式方法有点像继承,对吧?你可以获得嵌入结构体的任何字段和方法,并且你可以使用它的接口,以便任何超级结构都能够符合条件,而无需重新实现接口集。 - Matt Mc
是的 - 在其他语言中很像虚拟继承。您可以使用嵌入来实现接口,无论它是根据getter和setter还是数据指针定义的(或者对于只读访问小结构的第三个选项,是结构的副本)。 - twotwotwo
我必须说,这让我回想起1999年学习在Java中编写大量样板getter和setter的情景。 - Tom
很遗憾,Go自己的标准库并不总是这样做。我正在尝试模拟一些针对单元测试的os.Process调用。我不能只是在接口中包装进程对象,因为Pid成员变量是直接访问的,而Go接口不支持成员变量。 - Alex Jansen
1
@Tom 那是真的。我确实认为getter/setter比暴露指针更灵活,但我也不认为每个人都应该将所有东西都getter/setter化(或者这符合典型的Go风格)。我之前有几个词语暗示了这一点,但现在已经修改了开头和结尾,更加强调了这一点。 - twotwotwo
在Go语言中,Getter和Setter不是受欢迎的模式。 - Igor A. Melekhine

10
如果我理解正确,您希望将一个结构体的字段填充到另一个结构体中。我的建议是不要使用接口进行扩展。您可以通过以下方法轻松完成这个操作。
package main

import (
    "fmt"
)

type Person struct {
    Name        string
    Age         int
    Citizenship string
}

type Bob struct {
    SSN string
    Person
}

func main() {
    bob := &Bob{}

    bob.Name = "Bob"
    bob.Age = 15
    bob.Citizenship = "US"

    bob.SSN = "BobSecret"

    fmt.Printf("%+v", bob)
}
<注意>在的声明中出现了<人物>。这将使得包含结构字段可以直接在结构中使用,并且具有一些语法糖。

https://play.golang.org/p/aBJ5fq3uXtt


1
这样做会失败: bob:=&Bob {Name:“Bob”} - Bijan
Bijan是正确的,为什么只能使用点符号将分配项分配给上面的内容? - NomNomCameron
6
这是“结构体嵌入”机制的语法糖,如果你想在分配时进行赋值,你需要使用更明确的声明: bob :=&Bob {Person:Person {Name:“Bob”}} - Igor A. Melekhine

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