转到接口域

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

现在,我的问题是: 这是一个巧妙的把戏,还是我应该采取不同的做法?

92268 次浏览

这绝对是一个巧妙的把戏。但是,公开指针仍然可以直接访问数据,因此它只能为将来的更改提供有限的额外灵活性。还有 Go 约定并不要求您总是在数据属性前面放置一个抽象

把这些东西放在一起,对于一个给定的用例,我倾向于一个极端或另一个极端: 要么 a)只是创建一个公共属性(如果适用的话,使用嵌入)并传递具体的类型; 要么 b)如果暴露数据似乎使你认为可能发生的一些实现更改复杂化,通过方法暴露它。你要根据每个属性来权衡这个问题。

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


将属性隐藏在 getter 和 setter 之后可以提供一些额外的灵活性,以便稍后进行向后兼容的更改。假设有一天您想要更改 Person,使其不仅存储单个“ name”字段,而且存储 first/mid/last/prefix; 如果您有方法 Name() stringSetName(string),那么您可以让 Person接口的现有用户满意,同时添加新的更细粒度的方法。或者,当数据库支持的对象有未保存的更改时,您可能希望能够将其标记为“脏”; 当数据更新全部通过 SetFoo()方法进行时,您可以这样做。(您也可以采用其他方法,比如将原始数据存放在某个地方,并在调用 Save()方法时进行比较。)

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

当类型很复杂(而且代码库很大)时,这种灵活性更相关。如果您有一个 PersonCollection,它可能在内部由一个 sql.Rows、一个 []*Person、一个数据库 ID 的 []uint支持,或者其他任何支持。使用正确的界面,您可以保存呼叫者从关心它是,方式 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结构中使用一些句法糖。