转到界面字段


105

我熟悉一个事实,在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

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

现在,这是我的问题:这是一个巧妙的窍门,还是我应该以不同的方式来做?


3
“我可以创建一个定义数据而不是行为的接口”。我认为您的行为会返回数据。
jmaloney 2014年

我要写一个答案;我认为如果您需要它并知道后果,那很好,但是有后果,我不会一直这样做。
twotwotwo 2014年

@jmaloney我想你是对的,如果你想简单地看的话。但是总的来说,随着我展示的不同部分的出现,语义变成了“此函数接受组成为___的任何结构”。至少,这就是我的意图。
Matt Mc

1
这不是“答案”材料。我通过搜索“接口作为结构属性golang”来回答您的问题。通过设置将接口实现为另一个结构的属性的结构,我发现了一种类似的方法。这是操场,play.golang.org / p / KLzREXk9xo感谢您给我一些想法。
戴尔

1
回想起来,在使用Go的5年之后,我很清楚上述内容并不是惯用的Go。这对仿制药是一种压力。如果您想做这样的事情,建议您重新考虑系统的体系结构。接受接口并返回结构,通过交流共享并欢欣鼓舞。
马特·麦克

Answers:


55

这绝对是一个巧妙的把戏。但是,公开指针仍然可以直接访问可用数据,因此仅购买有限的额外灵活性即可用于将来的更改。此外,Go约定不要求您始终将抽象放在数据属性前面

综合考虑这些因素,对于给定的用例,我倾向于一个极端或另一个极端:要么a)仅仅设置一个公共属性(在适用时使用嵌入),然后传递具体类型,要么b)如果看起来像暴露数据那​​样稍后造成麻烦,请公开获取器/设置器以获得更可靠的抽象。

您将在每个属性的基础上进行权衡。例如,如果某些数据是特定于实现的,或者由于其他原因您希望更改表示形式,则您可能不希望直接公开该属性,而其他数据属性可能足够稳定以至于可以公开。


将属性隐藏在getter和setter的后面,可以为以后提供向后兼容的更改提供更多的灵活性。假设您某天想更改Person为不仅存储单个“名称”字段,还存储第一/中间/最后/前缀;如果您有方法Name() stringSetName(string),则可以Person在添加新的更细粒度的方法的同时让界面的现有用户满意。或者,您可能希望在未保存更改的情况下将数据库支持的对象标记为“脏”。您可以在数据更新全部通过SetFoo()方法时执行此操作。

因此:使用getters / setter方法,您可以在维护兼容API的同时更改struct字段,并在属性get / sets周围添加逻辑,因为没有人p.Name = "bob"无需浏览代码就可以这样做。

当类型复杂(并且代码库很大)时,这种灵活性更为重要。如果您有一个PersonCollection,则它可能在内部由数据库ID,sql.Rowsa []*Person,a []uint或任何其他内容支持。使用正确的界面,您可以节省呼叫者的烦恼,io.Reader使网络连接和文件看起来相似。

一件事:interfaceGo中的s具有独特的属性,您无需导入定义它的包就可以实现它。这可以帮助您避免周期性进口。如果您的介面传回*Person,而不仅仅是字串或其他字词,那么所有人PersonProviders都必须将套件汇入Person定义的位置。这可能是好的,甚至是不可避免的;这只是要知道的结果。


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

因此,例如,stdlib会执行一些操作,例如让您http.Server使用配置初始化a 并承诺可以使用零bytes.Buffer。这样做自己的事很好,而且,实际上,如果更具体的,数据公开的版本似乎可行,我不建议您先将东西抽象掉。这只是要注意权衡。


另一件事:嵌入方法有点像继承,对吗?您可以获得嵌入式结构所具有的任何字段和方法,并且可以使用其接口,以便任何上级结构都符合条件,而无需重新实现接口集。
Matt Mc

是的-很像其他语言中的虚拟继承。您可以使用嵌入来实现接口,无论该接口是根据getter和setter定义的还是指向数据的指针(或用于只读访问微小结构的第三个选项,即结构的副本)。
twotwotwo 2014年

我不得不说,这让我回想起了1999年,并学会了用Java编写大量样例获取器和设置器。
汤姆

太糟糕了,Go自己的标准库并不总是这样做。我正在尝试模拟对os.Process的一些调用以进行单元测试。我不能只将过程对象包装在接口中,因为直接访问Pid成员变量,而Go接口不支持成员变量。
Alex Jansen

1
@Tom是的。我认为getter / setter方法比露出一个指针添加更多的灵活性,但我并不认为每个人都应该的getter / setter方法的IFY一切(或将匹配典型的围棋风格)。之前,我曾打过几个手势,但后来修改了开头和结尾以多加强调。
2twotwo

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

注意PersonBob声明。这将使包含的struct字段Bob与某些语法糖直接在结构上可用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.