2014-09-24 27 views
39

我熟悉的一个事实是,在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 
} 

由于嵌入式结构,鲍勃拥有一切人有。它还实现了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) 
} 

Here is a Go Playground说明上面的代码。

使用这种方法,我可以创建一个定义数据而不是行为的接口,并且可以通过嵌入数据来实现任何结构。您可以定义明确与该嵌入数据交互的函数,但不知道外部结构的性质。并且在编译时检查所有内容! (只有这样,你可以搞砸了,我所看到的,将是嵌入接口PersonProviderBob,而不是具体Person这将编译并在运行时失败。)

现在,这里是我的问题:是这样的一个整洁的技巧,还是我应该以不同的方式做?

+1

“我可以制作一个定义数据而不是行为的界面”。我认为你有一个返回数据的行为。 – jmaloney 2014-09-24 22:22:08

+0

我会写一个答案;我认为如果你需要它并且知道后果就没有问题,但是会有后果,我不会一直这么做。 – twotwotwo 2014-09-24 22:22:58

+0

@jmaloney我认为你是对的,如果你想清楚地看看它。但总的来说,在我展示的不同部分中,语义变成“这个函数接受任何结构中有___的结构”。至少,这是我的意图。 – 2014-09-25 00:11:39

回答

21

这绝对是一个巧妙的技巧,只要你很酷就可以访问这些字段作为你的API的一部分。我会考虑的另一种选择是保持嵌入式结构/ interface的设置,但是根据getter和setter来定义接口。

隐藏getter和setter背后的属性为您提供一些额外的灵活性,以便稍后进行向后兼容的更改。假设你有一天想改变Person来存储不仅仅一个“姓名”字段,而是第一个/中间/最后一个/前缀;如果您有方法Name() stringSetName(string),则可以在添加新的更细粒度方法的同时保持Person接口的现有用户的满意度。或者您可能希望能够在未保存的更改时将数据库支持的对象标记为“脏”;您可以在数据更新全部通过SetFoo()方法时执行此操作。所以:使用getters/setters,你可以在维护一个兼容的API的时候改变struct字段,并且在属性get/sets周围添加逻辑,因为没有人可以不经过你的代码就做p.Name = "bob"

当你的类型做更复杂的事情时,这种灵活性更相关。如果您有PersonCollection,它可能由内部支持sql.Rows[]*Person,数据库ID的[]uint或任何其他内容。使用正确的界面,您可以保存来电者的身份,io.Reader使网络连接和文件看起来相似。

一个特定的事情:interface在Go中有特殊的属性,你可以在不导入定义它的包的情况下实现它;那可以帮助你avoid cyclic imports。如果你的接口返回一个*Person,而不是只是字符串或其他,所有PersonProviders必须导入定义了Person的包。这可能很好,甚至是不可避免的;这只是了解的一个结果。

所有这一切说,没有围棋的惯例,你必须隐藏所有的数据。 (与C++相比,这是一个值得欢迎的区别)。stdlib可以让你用你的配置初始化一个http.Server,并承诺零可用bytes.Buffer。这样做你自己的东西很好,而且事实上,如果更具体的数据暴露版本起作用,我认为你不必过早抽象。这只是关于意识到权衡。

+0

说得好,谢谢。 – 2014-09-24 23:46:51

+0

阅读完后,我的回答听起来很具说明性,因为我几乎只是给出了缺点。与C++不同的是,如果Go对你不起作用,那么它就不存在暴露数据的教条;试图编辑以强调更多。 – twotwotwo 2014-09-25 00:47:47

+0

另外一件事:嵌入方法有点像继承,对吧?您可以获得嵌入式结构所具有的任何字段和方法,并且您可以使用其接口,以便任何超结构都可以符合条件,而无需重新实现接口集。 – 2014-09-26 02:22:05

相关问题