Go语言接口字段

huangapple go评论71阅读模式
英文:

Go Interface Fields

问题

我熟悉在Go语言中,接口(interface)定义的是功能,而不是数据。你可以将一组方法放入接口中,但无法指定任何实现该接口的对象所需的字段。

例如:

// 接口
type Giver interface {
    Give() int64
}

// 一个实现
type FiveGiver struct {}

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

// 另一个实现
type VarGiver struct {
    number int64
}

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

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

// 使用接口的函数
func GetSomething(aGiver Giver) {
    fmt.Println("The Giver gives:", aGiver.Give())
}

// 将它们组合起来
func main() {
    fg := &FiveGiver{}
    vg := &VarGiver{3}
    GetSomething(fg)
    GetSomething(vg)
}

/*
输出结果:
5
3
*/

现在,你不能像这样做:

type Person interface {
    Name string
    Age int64
}

type Bob struct implements Person { // 这不是Go语法!
    ...
}

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,演示了上述代码。

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

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

英文:

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.

For example:

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

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!\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 that demonstrates the above code.

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?

答案1

得分: 66

这绝对是一个巧妙的技巧。然而,暴露指针仍然使得直接访问数据成为可能,因此它只为未来的变化提供了有限的灵活性。此外,Go 的约定并不要求你总是在数据属性前面放置一个抽象层。

综合考虑这些因素,对于特定的用例,我倾向于以下两个极端之一:a) 只需创建一个公共属性(如果适用,使用嵌入),并在各个具体类型之间传递;b) 如果暴露数据似乎会使某些可能的实现更改变得复杂,那么通过方法来暴露它。你需要根据每个属性来权衡这个选择。

如果你还没有决定,而且接口只在你的项目内部使用,也许倾向于暴露一个裸属性:如果以后它给你带来麻烦,重构工具可以帮助你找到所有对它的引用并将其更改为 getter/setter。


将属性隐藏在 getter 和 setter 后面可以为以后进行向后兼容的更改提供一些额外的灵活性。比如说,假设你将来想要将 Person 存储的不仅仅是一个名字字段,而是姓、名、中间名、前缀等;如果你有 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 的一部分,而不是因为它可能会使实现的更改变得复杂或不可能,这取决于你的判断。

例如,标准库允许你使用配置初始化 http.Server,并承诺零值的 bytes.Buffer 是可用的。你可以自己做类似的事情,事实上,我认为如果更具体、暴露数据的版本似乎可行,你不应该过早地进行抽象。这只是关于了解权衡的问题。

英文:

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.

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.

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.


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

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.

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.

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.


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.

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.

答案2

得分: 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)
}

注意Bob结构体声明中的Person。这将使得包含的结构体字段在Bob结构体中直接可用,并带有一些语法糖。

你可以在这里查看代码示例:https://play.golang.org/p/aBJ5fq3uXtt

英文:

If I correctly understand you want to populate one struct fields into another one. My opinion not to use interfaces to extend. You can easily do it by the next approach.

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

Note Person in Bob declaration. This will be made the included struct field be available in Bob structure directly with some syntactic sugar.

huangapple
  • 本文由 发表于 2014年9月25日 06:07:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/26027350.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定