我很熟悉这样一个事实,在 Go 中,接口(interface)定义的是功能,而不是数据。您将一组方法放入接口(interface)中,但您无法指定实现该接口(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
}
现在我们可以使用接口(interface)及其实现了:
// 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)
}
然而,在玩弄了接口(interface)和嵌入式结构之后,我发现了一种方法可以做到这一点,而且很流行:
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 接口(interface),因此我们可以将 Bob 传递给旨在使用该接口(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演示了上面的代码。
使用这种方法,我可以创建一个定义数据而不是行为的接口(interface),并且可以通过嵌入该数据由任何结构实现。您可以定义与该嵌入数据显式交互并且不知道外部结构的性质的函数。并且在编译时检查所有内容! (我可以看到,唯一可能搞砸的方法是将接口(interface) PersonProvider
嵌入到 Bob
中,而不是具体的 Person
。它会在运行时编译并失败。)
现在,我的问题是:这是一个巧妙的技巧,还是我应该换一种方式?
最佳答案
这绝对是一个巧妙的技巧。但是,公开指针仍然可以直接访问可用的数据,因此它只会为您购买有限的额外灵 active 以应对 future 的变化。此外,Go 约定并不要求您始终将抽象放在数据属性前面。
把这些东西放在一起,对于给定的用例,我会倾向于一个极端或另一个极端:要么a)只创建一个公共(public)属性(如果适用,使用嵌入)并传递具体类型,要么b)如果暴露数据似乎为了使您认为可能的一些实现更改复杂化,请通过方法将其公开。您将在每个属性的基础上权衡这一点。
如果您犹豫不决,并且该接口(interface)仅在您的项目中使用,则可能倾向于公开一个裸属性:如果以后给您带来麻烦,refactoring tools可以帮助您找到对它的所有引用以更改为 getter/setter。
将属性隐藏在 getter 和 setter 后面为您提供了一些额外的灵 active ,以便以后进行向后兼容的更改。假设您有一天想要更改 Person
以不仅存储单个“名称”字段,而且存储 first/middle/last/prefix;如果你有方法 Name() string
和 SetName(string)
,你可以让 Person
接口(interface)的现有用户满意,同时添加新的更精细的 -粒度方法。或者,您可能希望能够在数据库支持的对象有未保存的更改时将其标记为“脏”;当数据更新都通过 SetFoo()
方法时,您可以这样做。 (您也可以通过其他方式进行操作,例如将原始数据存储在某处并在调用 Save()
方法时进行比较。)
所以:使用 getter/setter,您可以在保持兼容 API 的同时更改结构字段,并围绕属性 get/sets 添加逻辑,因为没有人可以只做 p.Name = "bob"
浏览您的代码。
当类型复杂(并且代码库很大)时,这种灵 active 更为重要。如果您有 PersonCollection
,它可能在内部由 sql.Rows
、[]*Person
、[] 支持uint
的数据库 ID 或其他。使用正确的接口(interface),您可以避免调用者关心它,io.Reader
使网络连接和文件看起来相似的方式。
一个具体的事情:Go 中的 interface
具有一个特殊的属性,您可以在不导入定义它的包的情况下实现它;可以帮助你avoid cyclic imports .如果你的界面返回一个*Person
,而不仅仅是字符串或其他什么,所有PersonProviders
都必须导入定义了Person
的包。这可能很好,甚至是不可避免的;这只是一个需要知道的结果。
但是,Go 社区并没有严格的约定,反对在您的类型的公共(public) API 中公开数据成员。在给定的情况下,将属性的公共(public)访问用作 API 的一部分是否合理,而不是阻止任何暴露,因为它可能会使以后的实现更改复杂化或阻止实现更改,这由您自行判断。
因此,例如,stdlib 会做一些事情,比如让你用你的配置初始化一个 http.Server
并 promise 零 bytes.Buffer
是可用的。自己做这样的事情很好,而且,事实上,如果更具体的、数据暴露的版本似乎可行,我认为你不应该先发制人地把事情抽象出来。这只是关于权衡取舍。
关于struct - Go 接口(interface)字段,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26027350/