我最近喜欢Go编程语言,到目前为止我发现它很精彩,但我真的很难理解界面。我已经阅读了很多关于它们的内容,但它们对我来说似乎仍然非常抽象。
我写了一些使用以下界面的代码:
package main
import (
"fmt"
"math"
)
type Circer interface {
Circ() float64
}
type Square struct {
side float64
}
type Circle struct {
diam, rad float64
}
func (s *Square) Circ() float64 {
return s.side * 4
}
func (c *Circle) Circ() float64 {
return c.diam * math.Pi
}
func (c *Circle) Area() float64 {
if c.rad == 0 {
var rad = c.diam / 2
return (rad*rad) * math.Pi
} else {
return (c.rad*c.rad) * math.Pi
}
}
func main() {
var s = new(Square)
var c = new(Circle)
s.side = 2
c.diam = 10
var i Circer = s
fmt.Println("Square Circ: ", i.Circ())
i = c
fmt.Println("Circle Circ: ", i.Circ())
}
我无法真正看到Circer界面的目的。这些方法已经编写好了,我可以直接在结构上调用它们来保存两行代码,而不是使用Circer作为包装器。
有什么我想念的吗?我是否错误地使用了界面?任何帮助或示例表示赞赏。
答案 0 :(得分:8)
接口的要点是你可以使用下面的ShowMeTheCircumference
这样的通用函数。
package main
import (
"fmt"
"math"
)
type Circer interface {
Circ() float64
}
type Square struct {
side float64
}
type Circle struct {
diam, rad float64
}
func (s *Square) Circ() float64 {
return s.side * 4
}
func (c *Circle) Circ() float64 {
return c.diam * math.Pi
}
func ShowMeTheCircumference(name string, shape Circer) {
fmt.Printf("Circumference of %s is %f\n", name, shape.Circ())
}
func main() {
square := &Square{side: 2}
circle := &Circle{diam: 10}
ShowMeTheCircumference("square", square)
ShowMeTheCircumference("circle", circle)
}
答案 1 :(得分:4)
您缺少的是无法静态知道您手头有什么样的事情。让我们具体化。
例如,可以考虑io.Reader
。有很多东西可以实现接口的read
方法。假设你编写了一个使用io.Reader
的程序。例如,程序可能会在io.Reader
中打印内容的MD5总和。
package mypackage
import (
"fmt"
"crypto/md5"
"io"
"strings"
)
func PrintHashsum(thing io.Reader) {
hash := md5.New()
io.Copy(hash, thing)
fmt.Println("The hash sum is:", hash.Sum(nil))
}
并说你在其他地方使用此mypackage
:
func main() {
mypackage.PrintHashsum(strings.NewReader("Hello world"))
}
现在假设您使用io.Reader
的实现来解压缩zip文件,例如archive/zip
包中的文件。
import "archive/zip"
// ...
func main() {
// ...
anotherReader = zip.NewReader(...)
// ...
}
由于界面如何工作,您可以将这样的源代码阅读器提供给MD5-sum计算mypackage.PrintHashsum
函数,而无需对其现有代码执行任何其他操作或重新编译mypackage
!
func main() {
// ...
anotherReader = zip.NewReader(...)
mypackage.PrintHashsum(anotherReader)
}
接口与让程序对动态扩展开放有关。在您的示例中,您可能会争辩说编译器应该确切地知道应该调用哪种方法。但是在编译器支持单独编译(如Go)以获得速度的情况下,编译器可能无法知道:在编译mypackage
时,编译器无法看到{{1}的所有可能实现。 }:它不是心灵读者或时间旅行者!
答案 2 :(得分:2)
“我们要求严格定义的疑点和不确定性区域!” - 道格拉斯亚当斯,银河系漫游指南
为了理解Go中的接口,我们必须首先理解为什么我们一般编程接口。
我们使用接口来隐藏抽象背后的实现细节。我们喜欢隐藏这些细节,因为细节(即如何)比抽象更容易改变,因为它允许我们扩展和更改我们的应用程序而不会在整个程序中产生变化。当消费者依赖于接口而不是具体类型时,他们将他们的程序与接口背后的实现细节脱钩,使消费者免受变化,并使其更容易测试,扩展,并维护他们的申请。
Go有一个非常强大的接口实现。与大多数语言一样,它提供了一种通过抽象指定对象行为的方法,以便使用抽象的任何位置可以使用该抽象的任何实现,但是在Go中没有必要显式地声明你的具体实现给定的接口,因为Go自动处理这个。
删除显式声明要求会产生一些有趣的后果,例如:您可以让程序显示接口,以帮助您识别相应的抽象,而无需在发现它们时对所有实现进行注释。这也意味着为测试创建的接口不需要污染您的实现代码。此外,接口和实现者之间没有明确的关系,因此实现者在该方向上没有依赖/耦合。
在您提供的示例中,它确实更简单,更容易避免使用Interface而不是绑定到实现(concretion)的复杂性和“认知负载”。在大多数琐碎的例子中,使用界面看起来像是教条而不是工程。
接口是我们分离应用程序以使其更容易随时间增长的强大方式。如果您预期更改/变化(并且需要保护您的应用程序免受该更改/变更),那么创建并依赖于接口是朝着正确方向迈出的一步。
更多信息......
请参阅此“好”Go Object Oriented Design post。
并查看SOLID design principles,因为在考虑抽象和管理依赖关系以及变更的影响时,它是一个很好的起点。