为啥 Golang 需要接口?
Posted
技术标签:
【中文标题】为啥 Golang 需要接口?【英文标题】:Why are interfaces needed in Golang?为什么 Golang 需要接口? 【发布时间】:2016-12-29 19:05:25 【问题描述】:在 Golang 中,我们使用带有接收器方法的结构。到这里为止一切都很完美。 但是,我不确定接口是什么。我们在结构中定义方法,如果我们想在一个结构上实现一个方法,我们无论如何都要在另一个结构下再次编写它。 这意味着接口似乎只是方法定义,只是在我们的页面上占用了额外的不需要的空间。
有没有例子解释我为什么需要一个接口?
【问题讨论】:
你将如何解组未知结构的 JSON?或者如果 fmt.Printf 不存在,它将如何工作? 好吧,如果它不在那里,它应该工作,我猜你是什么意思它会如何工作?它是从 fmt 导出的 Go: What's the meaning of interface?的可能重复 @molivier 这不是关于结构是什么,而是关于它们的用途,它们似乎完全没用 为什么需要接口?解耦代码。见***.com/a/62297796/12817546。 “动态”调用方法。见***.com/a/62336440/12817546。访问 Go 包。见***.com/a/62278078/12817546。将任何值分配给变量。见***.com/a/62337836/12817546。 【参考方案1】:接口是一个太大的话题,无法在这里给出全面的答案,但有些事情要让它们的使用变得清晰。
接口是一个工具。是否使用它们取决于您,但它们可以使代码更清晰、更短、更具可读性,并且它们可以在包或客户端(用户)和服务器(提供者)之间提供一个很好的 API。
是的,您可以创建自己的struct
类型,并且可以将方法“附加”到它,例如:
type Cat struct
func (c Cat) Say() string return "meow"
type Dog struct
func (d Dog) Say() string return "woof"
func main()
c := Cat
fmt.Println("Cat says:", c.Say())
d := Dog
fmt.Println("Dog says:", d.Say())
我们已经可以在上面的代码中看到一些重复:当 Cat
和 Dog
都说些什么时。我们可以将两者都当作同一种实体来处理,比如动物吗?并不真地。当然我们可以将两者都处理为interface
,但是如果我们这样做,我们就不能调用他们的Say()
方法,因为interface
类型的值没有定义任何方法。
上述两种类型都有一些相似之处:两者都有一个方法Say()
,具有相同的签名(参数和结果类型)。我们可以捕捉这个界面:
type Sayer interface
Say() string
接口只包含方法的签名,而不包含它们的实现。
请注意,在 Go 中,如果其方法集是接口的超集,则类型隐式实现接口。没有意图的声明。这是什么意思?我们之前的Cat
和Dog
类型已经实现了这个Sayer
接口,尽管这个接口定义在我们之前编写它们时甚至不存在,而且我们没有触摸它们来标记它们或其他什么。他们就是这么做的。
接口指定行为。实现接口的类型意味着该类型具有接口“规定”的所有方法。
由于两者都实现了Sayer
,我们可以将两者都作为Sayer
的值来处理,它们有这个共同点。看看我们如何统一处理这两者:
animals := []Sayerc, d
for _, a := range animals
fmt.Println(reflect.TypeOf(a).Name(), "says:", a.Say())
(反射部分只是为了获取类型名称,暂时不要太在意。)
重要的部分是我们可以将Cat
和Dog
作为同一种类型(接口类型)处理,并使用它们/使用它们。如果您很快开始使用 Say()
方法创建其他类型,它们可以在 Cat
和 Dog
旁边排列:
type Horse struct
func (h Horse) Say() string return "neigh"
animals = append(animals, Horse)
for _, a := range animals
fmt.Println(reflect.TypeOf(a).Name(), "says:", a.Say())
假设您想编写适用于这些类型的其他代码。辅助函数:
func MakeCatTalk(c Cat)
fmt.Println("Cat says:", c.Say())
是的,上述功能适用于Cat
,没有其他功能。如果您想要类似的东西,则必须为每种类型编写它。不用说这有多糟糕。
是的,您可以编写它来接受interface
的参数,并使用type assertion 或type switches,这将减少辅助函数的数量,但仍然看起来很丑。
解决方案?是的,接口。只需声明该函数以获取一个接口类型的值,该接口类型定义了您想要对其执行的行为,仅此而已:
func MakeTalk(s Sayer)
fmt.Println(reflect.TypeOf(s).Name(), "says:", s.Say())
您可以使用Cat
、Dog
、Horse
或任何其他类型的值调用此函数,该类型直到现在还没有Say()
方法。很酷。
在Go Playground 上尝试这些示例。
【讨论】:
我仍然不明白为什么不把动物嵌入马或猫或狗身上仍然没有意义 @nikoss 不确定我理解你的意思,请说明如何将它们添加到切片中并迭代并在每个切片上调用Say()
。
啊哈! Golang 非常微妙,你的回答为我清除了这个话题。
那里完全一样。有点了解接口是什么,但同时不了解实际示例范围内的用法。猫说话和狗叫的例子没有意义。我试图在一些现实世界中反映这一点 - 客户、销售订单、提货订单、其他订单上下文,但不知道我需要在那里使用接口。
@Dzintars 我的建议是,如果您没有看到在代码中使用接口的好处,那么就不要创建/使用接口。当你开始重复代码来处理多个具体类型时,你会看到使用接口的好处/收获,你也可以稍后添加它们。【参考方案2】:
接口提供了一些泛型。想想鸭子打字。
type Reader interface
Read()
func callRead(r Reader)
r.Read()
type A struct
func(_ A)Read()
type B struct
func(_ B)Read()
将struct A
和B
传递给callRead
是可以的,因为它们都实现了Reader 接口。
但是如果没有接口,我们应该为A
和B
编写两个函数。
func callRead(a A)
a.Read()
func callRead2(b B)
b.Read()
【讨论】:
很好的例子。此外——这与 C++/Java 不同——使用 Go 接口,您也不需要创建func callRead(r Reader)
。这是相当强大的!【参考方案3】:
如前所述,接口是一种工具。并不是所有的包都会从中受益,但对于某些编程任务,接口对于抽象和创建包 API 非常有用,特别是对于库代码或可能以多种方式实现的代码。
以负责将一些原始图形绘制到屏幕上的包为例。我们可以认为屏幕的绝对基本基本要求是能够绘制像素、清除屏幕、定期刷新屏幕内容,以及获取有关屏幕的一些基本几何信息,例如其当前尺寸。因此,“屏幕”界面可能如下所示;
type Screen interface
Dimensions() (w uint32, h uint32)
Origin() (x uint32, y uint32)
Clear()
Refresh()
Draw(color Color, point Point)
现在我们的程序可能有几个不同的“图形驱动程序”,我们的图形包可以使用它们来满足屏幕的这个基本要求。您可能正在使用一些本机操作系统驱动程序,可能是 SDL2 包,也可能是其他东西。也许在您的程序中,您需要支持多种绘制图形的选项,因为它依赖于操作系统环境等等。
因此,您可以定义三个结构,每个结构都包含操作系统/库等中底层屏幕绘制例程所需的资源;
type SDLDriver struct
window *sdl.Window
renderer *sdl.Renderer
type NativeDriver struct
someDataField *Whatever
type AnotherDriver struct
someDataField *Whatever
然后您在代码中实现所有这三个结构的方法接口,以便这三个结构中的任何一个都可以满足屏幕接口的要求
func (s SDLDriver) Dimensions() (w uint32, h uint32)
// implement Dimensions()
func (s SDLDriver) Origin() (x uint32, y uint32)
// implement Origin()
func (s SDLDriver) Clear()
// implement Clear()
func (s SDLDriver) Refresh()
// implement Refresh()
func (s SDLDriver) Draw(color Color, point Point)
// implement Draw()
...
func (s NativeDriver) Dimensions() (w uint32, h uint32)
// implement Dimensions()
func (s NativeDriver) Origin() (x uint32, y uint32)
// implement Origin()
func (s NativeDriver) Clear()
// implement Clear()
func (s NativeDriver) Refresh()
// implement Refresh()
func (s NativeDriver) Draw(color Color, point Point)
// implement Draw()
... and so on
现在,您的外部程序真的不应该关心您可能正在使用哪些驱动程序,只要它可以通过标准界面清除、绘制和刷新屏幕即可。这是抽象。您在包级别提供程序其余部分运行所需的绝对最小值。只有图形内的代码需要了解操作如何工作的所有“细节”。
因此您可能知道需要为给定环境创建哪个屏幕驱动程序,这可能是在执行开始时根据检查用户系统上可用的内容来决定的。您决定 SDL2 是最佳选择并创建一个新的 SDLGraphics 实例;
sdlGraphics, err := graphics.CreateSDLGraphics(0, 0, 800, 600)
但是你现在可以从这里创建一个 Screen 的类型变量;
var screen graphics.Screen = sdlGraphics
现在您有了一个名为“screen”的通用“Screen”类型,它实现(假设您对它们进行了编程)Clear()、Draw()、Refresh()、Origin() 和 Dimensions() 方法。从此时起,在您的代码中,您可以完全自信地发出诸如
之类的语句screen.Clear()
screen.Refresh()
等等......这样做的好处是你有一个标准类型,称为“屏幕”,你的程序的其余部分,它真的不关心图形库的内部工作,可以在没有的情况下使用它考虑一下。您可以将“屏幕”传递给任何功能等,确信它会正常工作。
接口非常有用,它们确实可以帮助您考虑代码的功能,而不是结构中的数据。而且小接口更好!
例如,与其在 Screen 界面中进行一大堆渲染操作,不如设计第二个这样的界面;
type Renderer interface
Fill(rect Rect, color Color)
DrawLine(x float64, y float64, color Color)
... and so on
这肯定需要一些时间来适应,具体取决于您的编程经验和您以前使用过的语言。如果你之前一直是一个严格的 Python 程序员,你会发现 Go 完全不同,但如果你一直在使用 Java/C++,那么你会很快理解 Go。接口为您提供面向对象的特性,而没有其他语言(例如 Java)中存在的烦恼。
【讨论】:
【参考方案4】:我可以看到 interface
有用的地方是实现私有 struct
字段。例如,如果您有以下代码:
package main
type Halloween struct
Day, Month string
func NewHalloween() Halloween
return Halloween Month: "October", Day: "31"
func (o Halloween) UK(Year string) string
return o.Day + " " + o.Month + " " + Year
func (o Halloween) US(Year string) string
return o.Month + " " + o.Day + " " + Year
func main()
o := NewHalloween()
s_uk := o.UK("2020")
s_us := o.US("2020")
println(s_uk, s_us)
然后o
可以访问所有struct
字段。你可能不想要那个。在那里面
如果你可以使用这样的东西:
type Country interface
UK(string) string
US(string) string
func NewHalloween() Country
o := Halloween Month: "October", Day: "31"
return Country(o)
我们所做的唯一更改是添加interface
,然后返回struct
包裹在interface
中。在这种情况下,只有方法可以访问
struct
字段。
【讨论】:
【参考方案5】:我将在这里展示两个有趣的 Go 接口用例:
1- 看看这两个简单的界面:
type Reader interface
Read(p []byte) (n int, err error)
type Writer interface
Write(p []byte) (n int, err error)
使用这两个简单的界面,你可以做这个有趣的魔法:
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"strings"
)
func main()
file, err := os.Create("log.txt")
if err != nil
panic(err)
defer file.Close()
w := io.MultiWriter(file, os.Stdout)
r := strings.NewReader("You'll see this string twice!!\n")
io.Copy(w, r)
slice := []byte33, 34, 35, 36, 37, 38, 39, 10, 13
io.Copy(w, bytes.NewReader(slice)) // !"#$%&'
buf := &bytes.Buffer
io.Copy(buf, bytes.NewReader(slice))
fmt.Println(buf.Bytes()) // [33 34 35 36 37 38 39 10 13]
_, err = file.Seek(0, 0)
if err != nil
panic(err)
r = strings.NewReader("Hello\nWorld\nThis\nis\nVery\nnice\nInterfacing.\n")
rdr := io.MultiReader(r, file)
scanner := bufio.NewScanner(rdr)
for scanner.Scan()
fmt.Println(scanner.Text())
输出:
You'll see this string twice!!
!"#$%&'
[33 34 35 36 37 38 39 10 13]
Hello
World
This
is
Very
nice
Interfacing.
You'll see this string twice!!
!"#$%&'
我希望这段代码足够清晰:
使用strings.NewReader
从字符串读取,并同时使用io.MultiWriter
和io.Copy(w, r)
写入file
和os.Stdout
。然后使用bytes.NewReader(slice)
从切片中读取并同时写入file
和os.Stdout
。然后将切片复制到缓冲区io.Copy(buf, bytes.NewReader(slice))
,然后使用file.Seek(0, 0)
转到文件源,然后首先使用strings.NewReader
从字符串中读取,然后使用io.MultiReader(r, file)
和bufio.NewScanner
继续读取file
,然后使用@987654340 打印所有内容@。
2- 这是接口的另一个有趣用途:
package main
import "fmt"
func main()
i := show()
fmt.Println(i) // 0
i = show(1, 2, "AB", 'c', 'd', []int1, 2, 3, [...]int1, 2)
fmt.Println(i) // 7
func show(a ...interface) (count int)
for _, b := range a
if v, ok := b.(int); ok
fmt.Println("int: ", v)
return len(a)
输出:
0
int: 1
int: 2
7
还有很好的例子:Explain Type Assertions in Go
另见:Go: What's the meaning of interface?
【讨论】:
【参考方案6】:如果你需要一个方法来实现而不考虑结构。
您可能有一个处理程序方法来访问您的本地结构并在知道该结构之前使用该处理程序。
如果您需要其他或当前结构独有的行为。
您可能希望使用少数方法查看您的界面,因为用户可能永远不会使用它们。 您可能希望您的结构按其用例划分。
如果你需要一个实现任何东西的类型。
你可能知道或不知道类型,但至少你有价值。
【讨论】:
以上是关于为啥 Golang 需要接口?的主要内容,如果未能解决你的问题,请参考以下文章