【问题标题】:Go Interface FieldsGo 接口字段
【发布时间】:2014-11-19 13:31:33
【问题描述】:

我很熟悉这样一个事实,在 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)
}

Here is a Go Playground 演示了上面的代码。

使用这种方法,我可以创建一个定义数据而不是行为的接口,并且可以通过嵌入该数据由任何结构实现。您可以定义与该嵌入数据显式交互并且不知道外部结构的性质的函数。并且在编译时检查所有内容! (我可以看到,唯一可能搞砸的方法是将接口 PersonProvider 嵌入到 Bob 中,而不是具体的 Person。它会在运行时编译并失败。)

现在,我的问题是:这是一个巧妙的技巧,还是我应该换一种方式?

【问题讨论】:

  • “我可以创建一个定义数据而不是行为的接口”。我认为你有一种返回数据的行为。
  • 我要写一个答案;我认为如果你需要它并且知道后果就可以了,但是有后果我不会一直这样做。
  • @jmaloney 我认为你是对的,如果你想清楚地看待它。但总的来说,随着我展示的不同部分,语义变成“这个函数接受任何在其组成中具有 ___ 的结构”。至少,这是我的本意。
  • 这不是“答案”材料。我通过谷歌搜索“interface as struct property golang”来回答你的问题。我通过将实现接口的结构设置为另一个结构的属性找到了类似的方法。这里是操场,play.golang.org/p/KLzREXk9xo 谢谢你给我一些想法。
  • 回想起来,在使用 Go 5 年后,我很清楚以上不是惯用的 Go。这是对仿制药的一种压力。如果你想做这种事情,我建议你重新考虑你的系统架构。接受接口并返回结构,通过交流来分享,并为之欢欣鼓舞。

标签: struct interface go


【解决方案1】:

这绝对是一个巧妙的技巧。但是,公开指针仍然可以直接访问可用的数据,因此它只会为您购买有限的额外灵活性以应对未来的变化。此外,Go 约定并不要求您始终将抽象放在数据属性前面

把这些东西放在一起,对于给定的用例,我会倾向于一个极端或另一个极端:要么a)只创建一个公共属性(如果适用,使用嵌入)并传递具体类型,要么b)如果暴露数据似乎为了使您认为可能的一些实现更改复杂化,请通过方法将其公开。您将在每个属性的基础上权衡这一点。

如果您犹豫不决,并且该接口仅在您的项目中使用,可能倾向于暴露一个裸属性:如果以后给您带来麻烦,refactoring tools 可以帮助您找到所有对它的引用都更改为 getter/setter。


将属性隐藏在 getter 和 setter 后面为您提供了一些额外的灵活性,以便以后进行向后兼容的更改。假设您有一天想要更改 Person 以不仅存储单个“名称”字段,还存储第一个/中间/最后一个/前缀;如果你有方法Name() stringSetName(string),你可以让Person 接口的现有用户满意,同时添加新的更细粒度的方法。或者,您可能希望能够在数据库支持的对象有未保存的更改时将其标记为“脏”;当数据更新全部通过SetFoo() 方法时,您可以这样做。 (您也可以通过其他方式进行操作,例如将原始数据存储在某处并比较何时调用 Save() 方法。)

因此:使用 getter/setter,您可以在保持兼容 API 的同时更改结构字段,并围绕属性 get/sets 添加逻辑,因为没有人可以在不通过您的代码的情况下执行p.Name = "bob"

当类型复杂(并且代码库很大)时,这种灵活性更为重要。如果您有PersonCollection,它可能在内部由sql.Rows[]*Person[]uint 数据库ID 或其他支持。使用正确的界面,您可以避免调用者关心它,就像io.Reader 使网络连接和文件看起来一样。

一个具体的事情:Go 中的interfaces 有一个特殊的属性,您可以在不导入定义它的包的情况下实现它;这可以帮助你avoid cyclic imports。如果你的接口返回一个*Person,而不仅仅是字符串或其他任何东西,所有PersonProviders都必须导入定义Person的包。这可能很好,甚至是不可避免的;这只是一个需要知道的结果。


但是,Go 社区并没有严格的约定,反对在您的类型的公共 API 中公开数据成员。在给定的情况下,将属性的公共访问用作 API 的一部分是否合理,而不是阻止任何暴露,因为这可能会使以后的实现更改复杂化或阻止实现更改,这由您自行判断。

因此,例如,stdlib 会做一些事情,比如让您使用您的配置初始化 http.Server,并承诺零 bytes.Buffer 是可用的。自己做这样的事情很好,而且,事实上,如果更具体的数据暴露版本似乎可行,我认为你不应该先发制人地把事情抽象出来。这只是关于权衡取舍。

【讨论】:

  • 还有一点:嵌入方法有点像继承,对吧?您可以获得嵌入结构具有的任何字段和方法,并且可以使用它的接口,以便任何超结构都符合条件,而无需重新实现接口集。
  • 是的——很像其他语言中的虚拟继承。您可以使用嵌入来实现接口,无论它是根据 getter 和 setter 定义还是指向数据的指针(或者,只读访问微型结构的第三个选项,结构的副本)。
  • 我不得不说,这让我回想起 1999 年,并学习用 Java 编写大量样板的 getter 和 setter。
  • 太糟糕了 Go 自己的标准库并不总是这样做。我正在尝试模拟一些对 os.Process 的调用以进行单元测试。我不能只将进程对象包装在一个接口中,因为 Pid 成员变量是直接访问的,而 Go 接口不支持成员变量。
  • @Tom 是的。我确实认为 getter/setter 比暴露指针增加了更多的灵活性,但我不认为每个人都应该 getter/setter-ify 一切(或者这将符合典型的 Go 风格)。我之前有几个字在做手势,但修改了开头和结尾以更加强调它。
【解决方案2】:

如果我正确理解您想将一个结构字段填充到另一个结构字段中。我的意见是不要使用接口来扩展。您可以通过下一种方法轻松做到这一点。

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

Bob 声明中注明Person。这将使包含的结构字段在 Bob 结构中直接可用一些语法糖。

【讨论】:

  • 这样做会失败:bob := &Bob{Name: "Bob"}
  • Bijan 是对的,为什么只能使用点符号对上面的进行赋值?
  • 这是“结构嵌入”机制的语法糖,如果你想在分配点上进行赋值,你需要使用更明确的声明:bob := &Bob{Person: Person{Name: "Bob"}}
猜你喜欢
  • 1970-01-01
  • 2021-09-08
  • 1970-01-01
  • 2015-01-30
  • 2019-01-31
  • 2010-12-18
  • 2013-01-04
  • 2013-02-21
  • 2012-10-17
相关资源
最近更新 更多