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