英文:
Struggling to See the Purpose of an Interface Type
问题
我最近对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作为一个包装器,这样可以节省两行代码。
我是否理解有误?我是否错误地使用了接口?任何帮助或示例都将不胜感激。
英文:
I've recently taken a liking to the Go programming language, I've found it wonderful so far but am really struggling to understand interfaces. I've read about quite a bit about them, but they still seem very abstract to me.
I've wrote a quick bit of code that uses an interface below:
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())
}
I can't really see the purpose of the Circer interface. The methods are already written and I could save two lines of code by simply calling them directly on the structs, rather than using Circer as a wrapper.
Is there something I'm missing? Am I using the interface incorrectly? Any help or examples are appreciated.
答案1
得分: 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("周长:%s 是 %f\n", name, shape.Circ())
}
func main() {
square := &Square{side: 2}
circle := &Circle{diam: 10}
ShowMeTheCircumference("正方形", square)
ShowMeTheCircumference("圆形", circle)
}
英文:
The point of interfaces is that you can make general purpose functions like ShowMeTheCircumference
below.
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)
}
答案2
得分: 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"))
}
现在假设你使用了一个能够实时解压缩zip文件的io.Reader
实现,比如archive/zip
包中的实现。
import "archive/zip"
// ...
func main() {
// ...
anotherReader = zip.NewReader(...)
// ...
}
由于接口的工作原理,你可以将这样一个从zip文件中读取的Reader
传递给计算MD5哈希值的mypackage.PrintHashsum
函数,而无需对现有代码进行任何其他更改或重新编译mypackage
!
func main() {
// ...
anotherReader = zip.NewReader(...)
mypackage.PrintHashsum(anotherReader)
}
接口与让程序具备动态扩展性有着密切关系。在你的例子中,你可能会认为编译器应该准确地知道应该调用哪个方法。但是在编译器支持分离编译(如Go语言)以提高速度的情况下,编译器不可能知道所有可能的io.Reader
实现:在编译mypackage
时,编译器无法预知所有可能的io.Reader
实现,因为它既不是心灵读者也不是时光旅行者!
英文:
What you're missing are scenarios where you can't know statically what kind of thing you have on hand. Let's get concrete.
Think of io.Reader
, for example. There are lots of things that implement the read
method of the interface. Say that you write a program that uses io.Reader
. For example, a program might print an MD5-sum of the content in a io.Reader
.
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))
}
and say that you use this mypackage
in another file elsewhere:
func main() {
mypackage.PrintHashsum(strings.NewReader("Hello world"))
}
Now say that you use an implementation of an io.Reader
that decompresses zip files on the fly, such as the one in the archive/zip
package.
import "archive/zip"
// ...
func main() {
// ...
anotherReader = zip.NewReader(...)
// ...
}
Due to how interfaces work, you can feed such a zip-sourced reader into the MD5-sum computing mypackage.PrintHashsum
function without doing anything else to its existing code or recompiling mypackage
!
func main() {
// ...
anotherReader = zip.NewReader(...)
mypackage.PrintHashsum(anotherReader)
}
Interfaces have everything to do with letting programs be open to dynamic extension. In your example, you might argue that the compiler should just know exactly what method should be called. But in cases where your compiler supports separate compilation (like Go) for speed, the compiler can't possibly know: at the point of compiling mypackage
, the compiler can't see all the possible implementations of io.Reader
: it's not a mind reader or time traveler!
答案3
得分: 2
"我们要求明确定义的怀疑和不确定的领域!"
- 道格拉斯·亚当斯,《银河系漫游指南》
为了理解Go中的接口,我们首先必须了解为什么我们通常会编写面向接口的程序。
将"是什么"与"如何"分离
我们使用接口来隐藏抽象后面的实现细节。我们喜欢隐藏这些细节,因为细节(即"如何")比抽象更容易发生变化,并且这样可以在不影响整个程序的情况下扩展和修改应用程序。当消费者依赖于接口而不是具体类型时,他们将其程序与接口背后的实现细节解耦,这样可以使其应用程序更容易进行测试、扩展和维护。
Go语言的接口
Go语言具有非常强大的接口实现。与大多数语言一样,它提供了一种通过抽象来指定对象行为的方式,以便在使用抽象的任何地方都可以使用该抽象的任何实现。但在Go语言中,无需显式声明你的具体类型实现了给定的接口,因为Go语言会自动处理这个问题。
去除了显式声明的要求会产生有趣的影响,例如:你可以让你的程序在进行开发时逐渐暴露接口,以帮助你识别适当的抽象,而无需在发现它们时对所有实现进行注释。这也意味着为测试而创建的接口不需要污染你的实现代码。此外,接口与实现者之间没有明确的关系,因此实现者在这方面没有依赖/耦合。
Circer接口示例
在你提供的示例中,避免使用接口而直接绑定到实现(具体类型)确实更简单、更容易。在大多数简单的示例中,使用接口可能会显得过于教条主义和过度工程化。
总结
接口是我们将应用程序解耦的一种强大方式,使其更容易随着时间的推移而发展。如果你预见到变化/变异(并且需要保护你的应用程序免受变化/变异的影响),那么创建并依赖于接口是朝着正确方向迈出的一步。
了解更多...
查看这篇"好的" Go面向对象设计文章。
并且看一下SOLID 设计原则,因为它是考虑抽象和管理依赖关系和变化影响的绝佳起点。
英文:
"We demand rigidly defined areas of doubt and uncertainty!"
- Douglas Adams, The Hitchhiker's Guide to the Galaxy
In order to understand Interfaces in Go, we must first understand why we program to Interfaces in general.
Separating the what from the how
We use interfaces to hide implementation details behind an abstraction. We like to hide these details because the details (i.e. the how) are more likely to change than the abstraction and because it allows us to extend and change our applications without having the change ripple throughout our programs. When a consumer depends on an Interface instead of a concrete type, they are decoupling their program from the implementation details behind the Interface which shelters the consumer from change and makes it easier to test, extend, and maintain their application.
Golang Interfaces
Go has a very powerful Interface implementation. As in most languages, it provides a way to specify the behavior of an object through an abstraction so that any place the abstraction is used any implementation of that abstraction can be used, but in Go there is no need to explicitly declare that your concretion implements a given Interface as Go handles this automatically.
Removing the explicit declaration requirement has interesting ramifications, such as: you can let your program surface the Interfaces as you go to help you identify the appropriate abstractions, without needing to annotate all of the implementations as you discover them. This also means Interfaces that are created for Testing does not need to pollute your implementation code. Additionally, there isn't an explicit relationship between the Interface and the implementer, so the implementer has no dependency/coupling in that direction.
The Circer Interface example
In the example you've provided it is certainly simpler and easier to avoid the complexity and "cognitive load" of using an Interface instead of binding to the implementation (concretion). In most trivial examples it can seem like dogmatic over engineering to use an interface.
Summary
Interfaces are a powerful way for us to decouple our applications to make it easier for them to grow over time. If you anticipate change/variation (and need to protect your application from that change/variation) then creating and depending on an Interface is a step in the right direction.
For more...
See this "good" Go Object Oriented Design post.
And take a look at the SOLID design principles, as it is an excellent place to start when considering the implications of abstraction and managing dependencies and change.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论