nik*_*oss 29 struct interface go
在Golang中,我们使用带接收器方法的结构.一切都很完美到这里.
但是,我不确定接口是什么.我们在结构中定义方法,如果我们想在结构上实现一个方法,我们再次在另一个结构下编写它.
这意味着接口似乎只是方法定义,只占用了页面上额外不需要的空间.
有没有例子解释我为什么需要一个界面?
icz*_*cza 67
接口是一个太大的主题,无法在这里给出一个全面的答案,但有些事情可以让他们清楚地使用它们.
接口是一种工具.您是否使用它们取决于您,但它们可以使代码更清晰,并且它们可以在包或客户端(用户)和服务器(提供者)之间提供良好的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())
}
Run Code Online (Sandbox Code Playgroud)
我们已经可以在上面的代码中看到一些重复:同时做两件事Cat并Dog说些什么.我们可以像动物一样处理同一种实体吗?并不是的.当然我们可以处理两者interface{},但是如果我们这样做,我们就不能调用他们的Say()方法,因为类型的值interface{}没有定义任何方法.
两种类型都有一些相似之处:两者都有一个Say()具有相同签名的方法(参数和结果类型).我们可以通过界面捕获它:
type Sayer interface {
Say() string
}
Run Code Online (Sandbox Code Playgroud)
该接口仅包含方法的签名,但不包含其实现.
请注意,在Go中,如果其方法集是接口的超集,则类型会隐式实现接口.没有声明的声明.这是什么意思?我们以前Cat和Dog类型已经实现了这个Sayer接口,即使在我们前面写了他们这个接口定义根本不存在,而我们没有接触他们,以纪念他们什么.他们只是这样做.
接口指定行为.实现接口的类型意味着类型具有接口"规定"的所有方法.
由于两者都是实现的Sayer,我们可以将它们作为一个值来处理Sayer,它们有这个共同点.看看我们如何处理两者的统一:
animals := []Sayer{c, d}
for _, a := range animals {
fmt.Println(reflect.TypeOf(a).Name(), "says:", a.Say())
}
Run Code Online (Sandbox Code Playgroud)
(这反映了部分只是为了得到类型名称,现在不做很多.)
最重要的部分是,我们可以同时处理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())
}
Run Code Online (Sandbox Code Playgroud)
假设您要编写适用于这些类型的其他代码.辅助功能:
func MakeCatTalk(c Cat) {
fmt.Println("Cat says:", c.Say())
}
Run Code Online (Sandbox Code Playgroud)
是的,上面的功能可以使用Cat,没有别的.如果你想要类似的东西,你必须为每种类型写它.不用说这有多糟糕.
是的,您可以编写它来获取参数interface{},并使用类型断言或类型开关,这将减少辅助函数的数量,但仍然看起来非常难看.
解决方案?是的,接口.简单地声明函数取一个接口类型的值来定义你想用它做的行为,这就是全部:
func MakeTalk(s Sayer) {
fmt.Println(reflect.TypeOf(s).Name(), "says:", s.Say())
}
Run Code Online (Sandbox Code Playgroud)
你可以调用这个函数的值Cat,Dog,Horse或任何其他类型的不知道"到现在为止,有一个Say()方法.凉.
在Go Playground上试试这些例子.
小智 7
如前所述,接口是一种工具。并非所有的包都会从它们中受益,但对于某些编程任务,接口对于抽象和创建包 API 非常有用,特别是对于库代码或可能以多种方式实现的代码。
以负责将一些原始图形绘制到屏幕上的包为例。我们可以将屏幕的绝对基本本质要求视为能够绘制像素、清除屏幕、定期刷新屏幕内容,以及获取有关屏幕的一些基本几何信息,例如当前尺寸。因此,“屏幕”界面可能如下所示;
type Screen interface {
Dimensions() (w uint32, h uint32)
Origin() (x uint32, y uint32)
Clear()
Refresh()
Draw(color Color, point Point)
}
Run Code Online (Sandbox Code Playgroud)
现在我们的程序可能有几个不同的“图形驱动程序”,我们的图形包可以使用它们来满足屏幕的这个基本要求。您可能正在使用一些本机操作系统驱动程序,可能是 SDL2 包,也可能是其他东西。也许在您的程序中,您需要支持多种图形绘制选项,因为它取决于操作系统环境等。
因此,您可以定义三个结构体,每个结构体都包含操作系统/库等中底层屏幕绘制例程所需的资源;
type SDLDriver struct {
window *sdl.Window
renderer *sdl.Renderer
}
type NativeDriver struct {
someDataField *Whatever
}
type AnotherDriver struct {
someDataField *Whatever
}
Run Code Online (Sandbox Code Playgroud)
然后在代码中为所有这三个结构体实现方法接口,以便这三个结构体中的任何一个都可以满足 Screen 接口的要求
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
Run Code Online (Sandbox Code Playgroud)
现在,您的外部程序真的不应该关心您可能使用这些驱动程序中的哪一个,只要它可以通过标准界面清除、绘制和刷新屏幕即可。这是抽象。您在包级别提供程序其余部分工作所需的绝对最小值。只有图形内部的代码需要了解操作如何工作的所有“细节”。
因此,您可能知道需要为给定环境创建哪个屏幕驱动程序,这可能是在执行开始时根据检查用户系统上的可用内容来决定的。您决定 SDL2 是最佳选择,并创建一个新的 SDLGraphics 实例;
sdlGraphics, err := graphics.CreateSDLGraphics(0, 0, 800, 600)
Run Code Online (Sandbox Code Playgroud)
但是您现在可以从中创建 Screen 类型变量;
var screen graphics.Screen = sdlGraphics
Run Code Online (Sandbox Code Playgroud)
现在您有一个称为“屏幕”的通用“屏幕”类型,它实现(假设您对它们进行了编程)Clear()、Draw()、Refresh()、Origin() 和 Dimensions() 方法。从现在开始,您可以在代码中完全自信地发出诸如
screen.Clear()
screen.Refresh()
Run Code Online (Sandbox Code Playgroud)
等等......这样做的美妙之处在于您有一个称为“屏幕”的标准类型,您的程序的其余部分实际上并不关心图形库的内部工作,可以使用而无需考虑它。您可以将“屏幕”传递给任何函数等,确信它会正常工作。
接口非常有用,它们确实可以帮助您思考代码的功能而不是结构中的数据。而且小接口更好!
例如,不要在 Screen 界面中进行一大堆渲染操作,也许你会像这样设计第二个界面;
type Renderer interface {
Fill(rect Rect, color Color)
DrawLine(x float64, y float64, color Color)
... and so on
}
Run Code Online (Sandbox Code Playgroud)
这肯定需要一些时间来适应,这取决于您的编程经验和您以前使用过的语言。如果到目前为止您一直是严格的 Python 程序员,您会发现 Go 完全不同,但如果您一直在使用 Java/C++,那么您会很快了解 Go。接口为您提供面向对象的特性,而没有其他语言(例如 Java)中存在的烦恼。
接口提供了一些泛型。想想鸭子打字。
type Reader interface{
Read()
}
func callRead(r Reader){
r.Read()
}
type A struct{
}
func(_ A)Read(){
}
type B struct{
}
func(_ B)Read(){
}
Run Code Online (Sandbox Code Playgroud)
可以将struct A和传递B给callRead,因为两者都实现了Reader接口。但是如果没有接口,我们应该为A和编写两个函数B。
func callRead(a A){
a.Read()
}
func callRead2(b B){
b.Read()
}
Run Code Online (Sandbox Code Playgroud)
小智 6
我认为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)
}
Run Code Online (Sandbox Code Playgroud)
然后o可以访问所有字段struct。你可能不想要那样。在这种情况下,你可以使用这样的东西:
type Country interface {
UK(string) string
US(string) string
}
func NewHalloween() Country {
o := Halloween { Month: "October", Day: "31" }
return Country(o)
}
Run Code Online (Sandbox Code Playgroud)
我们所做的唯一更改是添加interface,然后返回struct
包装在interface. 在这种情况下,只有方法才能访问这些struct字段。