Go 接口字段 [英] Go Interface Fields

查看:20
本文介绍了Go 接口字段的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我熟悉这样一个事实,即在 Go 中,接口定义功能,而不是数据.您将一组方法放入一个接口中,但您无法指定实现该接口的任何内容所需的任何字段.

I'm familiar with the fact that, in Go, interfaces define functionality, rather than data. You put a set of methods into an interface, but you are unable to specify any fields that would be required on anything that implements that interface.

例如:

// 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
}

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

Now we can use the interface and its implementations:

// 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
*/

现在,你不能做的事情是这样的:

Now, what you can't do is something like this:

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)
}

然而,在玩弄接口和嵌入结构之后,我发现了一种方法来做到这一点,以一种时尚的方式:

However, after playing around with interfaces and embedded structs, I've discovered a way to do this, after a fashion:

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 传递给旨在使用该接口的函数.

Because of the embedded struct, Bob has everything Person has. It also implements the PersonProvider interface, so we can pass Bob into functions that are designed to use that interface.

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

func SayHi(pp PersonProvider) {
    fmt.Printf("Hello, %v!
", 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.它会在运行时编译并失败.)

Using this method, I can make an interface that defines data rather than behavior, and which can be implemented by any struct just by embedding that data. You can define functions that explicitly interact with that embedded data and are unaware of the nature of the outer struct. And everything is checked at compile time! (The only way you could mess up, that I can see, would be embedding the interface PersonProvider in Bob, rather than a concrete Person. It would compile and fail at runtime.)

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

Now, here's my question: is this a neat trick, or should I be doing it differently?

推荐答案

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

It is definitely a neat trick. However, exposing pointers still makes direct access to data available, so it only buys you limited additional flexibility for future changes. Also, Go conventions do not require you to always put an abstraction in front of your data attributes.

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

Taking those things together, I would tend towards one extreme or the other for a given use case: either a) just make a public attribute (using embedding if applicable) and pass concrete types around or b) if exposing the data seems to complicate some implementation change you think is likely, expose it through methods. You're going to be weighing this on a per-attribute basis.

如果你在篱笆上,并且接口只在你的项目中使用,那么可能倾向于暴露一个裸属性:如果它以后给你带来麻烦,重构工具 可以帮助你找到所有对它的引用以更改为 getter/setter.

If you're on the fence, and the interface is only used within your project, maybe lean towards exposing a bare attribute: if it causes you trouble later, refactoring tools can help you find all the references to it to change to a getter/setter.

在 getter 和 setter 后面隐藏属性为您提供了一些额外的灵活性,以便以后进行向后兼容的更改.假设有一天你想改变 Person 来存储不只是一个姓名"字段但第一个/中间/最后/前缀;如果你有方法 Name() stringSetName(string),你可以让 Person 界面的现有用户开心,同时添加新的更好的 -粒状方法.或者您可能希望能够将数据库支持的对象标记为脏".当它有未保存的更改时;当数据更新全部通过 SetFoo() 方法时,您可以这样做.(您也可以通过其他方式进行操作,例如将原始数据存储在某处并在调用 Save() 方法时进行比较.)

Hiding properties behind getters and setters gives you some extra flexibility to make backwards-compatible changes later. Say you someday want to change Person to store not just a single "name" field but first/middle/last/prefix; if you have methods Name() string and SetName(string), you can keep existing users of the Person interface happy while adding new finer-grained methods. Or you might want to be able to mark a database-backed object as "dirty" when it has unsaved changes; you can do that when data updates all go through SetFoo() methods. (You could do it other ways, too, like stashing the original data somewhere and comparing when a Save() method is called.)

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

So: with getters/setters, you can change struct fields while maintaining a compatible API, and add logic around property get/sets since no one can just do p.Name = "bob" without going through your code.

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

That flexibility is more relevant when the type is complicated (and the codebase is big). If you have a PersonCollection, it might be internally backed by an sql.Rows, a []*Person, a []uint of database IDs, or whatever. Using the right interface, you can save callers from caring which it is, the way io.Reader makes network connections and files look alike.

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

One specific thing: interfaces in Go have the peculiar property that you can implement one without importing the package that defines it; that can help you avoid cyclic imports. If your interface returns a *Person, instead of just strings or whatever, all PersonProviders have to import the package where Person is defined. That may be fine or even inevitable; it's just a consequence to know about.

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

But again, the Go community does not have a strong convention against exposing data members in your type's public API. It's left to your judgment whether it's reasonable to use public access to an attribute as part of your API in a given case, rather than discouraging any exposure because it could possibly complicate or prevent an implementation change later.

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

So, for example, the stdlib does things like let you initialize an http.Server with your config and promises that a zero bytes.Buffer is usable. It's fine to do your own stuff like that, and, indeed, I don't think you should abstract things away preemptively if the more concrete, data-exposing version seems likely to work. It's just about being aware of the tradeoffs.

这篇关于Go 接口字段的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆