繁体   English   中英

Go 接口字段

[英]Go Interface Fields

我熟悉这样一个事实,即在 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 。它会在运行时编译并失败。)

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

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

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

如果您在围栏上,并且该接口仅在您的项目中使用,则可能倾向于暴露一个裸属性:如果以后给您带来麻烦,重构工具可以帮助您找到对其的所有引用以更改为 getter/二传手。


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

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

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

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


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

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

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

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结构中可用一些语法糖。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM