回答

收藏

Go 接口字段

技术问答 技术问答 306 人阅读 | 0 人回复 | 2023-09-12

我熟悉 Go 中,界面定义功能,而不是数据。您将一组方法放入界面中,但您无法指定界面所需的任何字段。
! @$ b# q/ f' I. C3 z9 l例如:
/ ^) k3 v4 q" e1 a% w4 y( t
    // 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}
    , u# R0 S, L% a9 X
现在我们可以使用接口及其实现:
( R9 j, P/ X$ P
    // 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*/
    # `! r: [3 |1 a3 M. d3 E9 v
现在,你不能做这样的事:
0 K9 j' q9 W9 e/ f; }2 [
    type Person interface    Name string    Age int64}type Bob struct implements Person { // Not Go syntax!    ...}func PrintName(aPerson Person)    fmt.Println(&quoterson's name is: ",aPerson.Name)}func main()    b := &Bob{"Bob",23}    PrintName(b)}, v. f- k7 Z: v; e$ X# w
然而,在玩了界面和嵌入式结构后,我找到了一种时尚的方法来做到这一点:1 E2 a& v8 k9 s  Y. ^/ n8 M. r. k
    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}
    9 c! Y. `8 H  p/ _1 H  j0 x  Y0 p
结构体嵌入,Bob 拥有 Person 的一切。它还实现了 PersonProvider 接口,所以我们可以Bob 传递给使用该接口的函数。
) A, [0 ?" @$ e
    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)}
    8 n! p3 R% Y) v9 Z/ c4 ^
使用这种方法,我可以创建一个定义数据而不是行为的接口,任何结构都可以通过嵌入数据来实现。您可以定义与嵌入数据显式交互的函数,并且不知道外部结构的性质。并在编译过程中检查一切!(我可以看到,唯一可能搞砸的方法是嵌入接口PersonProvider中Bob,而不是具体的 中Person。操作时会编译失败。( m' Y8 {3 I) M: s) U! ^  M
现在,这是我的问题:这是一项巧妙的技能,还是我应该以不同的方式去做?% ]; o* n. }  J7 v$ Z2 {. X9 V
                                                                - E. [" E8 y+ N! V( ~
    解决方案:                                                               
- x/ C6 v: U( u0 L+ H                                                                这绝对是一项聪明的技能。然而,公共指针仍然可以直接访问数据,因此它只会给您带来有限的额外灵活性来应对未来的变化。Go 协议不要求你总是把抽象放在数据属性面前, o) z) K1 Q5 I# m( U/ k  d4 m, X
把这些东西放在一起,对于给定的用例,我倾向于一个极端或另一个极端:a) 只创建公共属性(如果适用,使用嵌入)并传递具体类型或 b) 如果公开数据似乎改变了你认为可能的一些复杂性,则通过方法公开它。您将在每个属性的基础上权衡它。/ K& Y0 i3 a: s
如果你在围栏上,接口只是使用您的项目,它可能倾向于暴露裸体属性:如果将来给你带来麻烦,重建工具可以帮助你找到所有的参考,并将其更改为 getter/二传手。2 T% z- B$ o% h3 @. u
属性隐藏在 getter 和 setter 之后,它为您提供了一些额外的灵活性,以便以后进行兼容化。假设有一天你想改变Person存储的不仅仅是名称字段first/middle/last/prefix;如果您有方法Name() string和SetName(string),则可以Person添加新的细粒度方法来满足界面的现有用户。或者,您可能希望在未保存和更改的情况下将数据库支持的对象标记为脏;当数据更新通过时SetFoo()你可以这样做。(也可以通过其他方式操作,比如将原始数据存储在某个地方并存储在某个地方。Save()调用方法时进行比较。)
) S) Q! k6 p* ~8 O7 H所以:使用 getter/setter,维护时可以兼容 API 同时改变结构字段,围绕属性 get/set 添加逻辑,因为没有人能添加逻辑p.Name = "bob"不用你的代码就能做到。+ H( S/ n+ a9 O, T
当类型复杂(代码库大)时,这种灵活性更为重要。如果你有PersonCollection,它可能由sql.Rows、[]*Person、[]uint数据库 ID 或内部支持任何其他内容。使用正确的界面可以避免呼叫者关心它是什么,这样io.Reader使网络连接看起来与文件相似。
  v% Z( b/ i2 `' g一件特别的事:interfaceGo 中的 s 有一个特殊的属性,可以在不导入定义其包的情况下实现;这可以帮助你避免循环导入。如果您的接口返回 a *Person,而不仅仅是字符串或任何其他东西PersonProviders必须在Person定义位置导入包。这可能是好的,甚至是不可避免的;这只是结果。0 }" O0 l; @6 t* b
但同样,Go 社区没有强烈的协议反对你类型的公共 API 公开数据成员。在给定的情况下,将属性的公共访问用作 API 的一部分是否合理取决于你,而不是阻止它任何公开,因为它可能会使未来的变化复杂化或防止变化。
& f! l5 x) b# P2 g例如,stdlib 执行就像让你http.Server使用配置初始化 an并承诺使用零和其他东西bytes.Buffer。这样做你自己的事情很好,事实上,如果更具体的数据披露版本似乎是可行的,我认为你不应该先抽象。这只是理解权衡。
分享到:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则