我熟悉 Go 中,界面定义功能,而不是数据。您将一组方法放入界面中,但您无法指定界面所需的任何字段。 - f, p' A$ w3 s/ @& {例如: + o9 N& S8 L. q/ L
// Interfacetype Giver interface Give() int64}// One implementationtype FiveGiver struct {}func (fg *FiveGiver) Give() int64 return 5}// Another implementationtype VarGiver struct number int64}func (vg *VarGiver) Give() int64 return vg.number} + Y6 c) ?. i( u$ y+ I
现在我们可以使用接口及其实现:0 l' j+ B9 c. t
// A function that uses the interfacefunc GetSomething(aGiver Giver) fmt.Println("The Giver gives: ",aGiver.Give()}// Bring it all togetherfunc main() fg := &FiveGiver{} vg := &VarGiver{3} GetSomething(fg) GetSomething(vg)}/*Resulting output:53*/ : t+ _) ?6 R- f: J: k3 s
现在,你不能做这样的事: * b- N6 Y0 [0 l# }6 g5 M/ y
type Person interface Name string Age int64}type Bob struct implements Person { // Not Go syntax! ...}func PrintName(aPerson Person) fmt.Println("erson's name is: ",aPerson.Name)}func main() b := &Bob{"Bob",23} PrintName(b)}7 e. `% J1 T7 L% S2 J1 p$ x. j
然而,在玩了界面和嵌入式结构后,我找到了一种时尚的方法来做到这一点:6 Y h: ^$ ^% Y
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}6 Y' z5 u+ E8 U$ G4 \
结构体嵌入,Bob 拥有 Person 的一切。它还实现了 PersonProvider 接口,所以我们可以Bob 传递给使用该接口的函数。 0 J/ ~7 {$ X" c$ a6 Z! J9 S
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{ 5Person{"Bob", DoBirthday(b) SayHi(b) fmt.Printf("You're %v years old now!",b.Age)}5 E1 O. O' Q0 q r' e F
使用这种方法,我可以创建一个定义数据而不是行为的接口,任何结构都可以通过嵌入数据来实现。您可以定义与嵌入数据显式交互的函数,并且不知道外部结构的性质。并在编译过程中检查一切!(我可以看到,唯一可能搞砸的方法是嵌入接口PersonProvider中Bob,而不是具体的 中Person。操作时会编译失败。 ! b% [5 a* u: l% V1 i1 i1 `现在,这是我的问题:这是一项巧妙的技能,还是我应该以不同的方式去做?% o. c3 G# m1 i4 @9 t2 d `% E" L! e
& s( b$ X3 W0 a2 E解决方案: " |3 V0 W7 ^ o# e, q2 b 这绝对是一项聪明的技能。然而,公共指针仍然可以直接访问数据,因此它只会给您带来有限的额外灵活性来应对未来的变化。Go 协议不要求你总是把抽象放在数据属性面前。0 ~, E. J. d+ S$ Y7 I
把这些东西放在一起,对于给定的用例,我倾向于一个极端或另一个极端:a) 只创建公共属性(如果适用,使用嵌入)并传递具体类型或 b) 如果公开数据似乎改变了你认为可能的一些复杂性,则通过方法公开它。您将在每个属性的基础上权衡它。 + S: a9 j. I3 U- u9 W如果你在围栏上,接口只是使用您的项目,它可能倾向于暴露裸体属性:如果将来给你带来麻烦,重建工具可以帮助你找到所有的参考,并将其更改为 getter/二传手。/ k7 l0 b/ a' S( R
属性隐藏在 getter 和 setter 之后,它为您提供了一些额外的灵活性,以便以后进行兼容化。假设有一天你想改变Person存储的不仅仅是名称字段first/middle/last/prefix;如果您有方法Name() string和SetName(string),则可以Person添加新的细粒度方法来满足界面的现有用户。或者,您可能希望在未保存和更改的情况下将数据库支持的对象标记为脏;当数据更新通过时SetFoo()你可以这样做。(也可以通过其他方式操作,比如将原始数据存储在某个地方并存储在某个地方。Save()调用方法时进行比较。) 2 O7 X, j" l" r所以:使用 getter/setter,维护时可以兼容 API 同时改变结构字段,围绕属性 get/set 添加逻辑,因为没有人能添加逻辑p.Name = "bob"不用你的代码就能做到。 5 P2 V; v! E) e! d/ @; B/ m当类型复杂(代码库大)时,这种灵活性更为重要。如果你有PersonCollection,它可能由sql.Rows、[]*Person、[]uint数据库 ID 或内部支持任何其他内容。使用正确的界面可以避免呼叫者关心它是什么,这样io.Reader使网络连接看起来与文件相似。4 x/ a- ?. E& M) r
一件特别的事:interfaceGo 中的 s 有一个特殊的属性,可以在不导入定义其包的情况下实现;这可以帮助你避免循环导入。如果您的接口返回 a *Person,而不仅仅是字符串或任何其他东西PersonProviders必须在Person定义位置导入包。这可能是好的,甚至是不可避免的;这只是结果。 ' S f; S# U' H( k9 [# M但同样,Go 社区没有强烈的协议反对你类型的公共 API 公开数据成员。在给定的情况下,将属性的公共访问用作 API 的一部分是否合理取决于你,而不是阻止它任何公开,因为它可能会使未来的变化复杂化或防止变化。 3 V) j% i+ _# R9 c例如,stdlib 执行就像让你http.Server使用配置初始化 an并承诺使用零和其他东西bytes.Buffer。这样做你自己的事情很好,事实上,如果更具体的数据披露版本似乎是可行的,我认为你不应该先抽象。这只是理解权衡。