Golang嵌入式接口在父结构体上

8
我有一个程序正在尝试在“子类”上实现功能,其中父类可以检查接口是否已实现。从角度上看,它真正处理基于方法存在与否的REST URL生成。
我遇到的问题是,根据以下模式,在TestController对象上找到了IList和IGet界面,而只有1个被实现。当调用IGet接口时,我会遇到恐慌。
我宁愿不对基础结构中的Get/List进行具体定义,然后再覆盖它们,而是更喜欢测试其是否存在,然后再执行操作。
这里还有一个go playground链接:https://play.golang.org/p/5j58fejeJ3
package main

import "fmt"

type IGet interface {
    Get(int)
}

type IList interface {
    List(int)
}

type Application struct {
    name    string
}

type BaseAppController struct {
    *Application

    IGet
    IList
}

type TestController struct {
    *BaseAppController
}

func (ctrl *BaseAppController) Init() {
    fmt.Println("In Init")

    if f, ok := interface{}(ctrl).(IGet); ok {
        fmt.Println("Controller Found GET", f)
    } else {
        fmt.Println("Controller NOT Found GET", f)
    }

    if f, ok := interface{}(ctrl).(IList); ok {
        fmt.Println("Controller Found LIST", f)
    } else {
        fmt.Println("Controller NOT Found LIST", f)
    }
}

func (ctrl *BaseAppController) Call() {
    fmt.Println("In Call")

    if f, ok := interface{}(ctrl).(IGet); ok {
        fmt.Println("Controller Found GET - going to call", f)

        f.Get(7)
    } else {
        fmt.Println("Controller NOT Found GET - can't call", f)
    }
}

// Test controller implements the Get Method
func (ctrl *TestController) Get(v int) {
    fmt.Printf("Hi name=%s v=%d\n", ctrl.name, v)
}

func main() {
    app := Application{"hithere"}
    ctrl := TestController{&BaseAppController{Application: &app}}

    ctrl.Init()

    ctrl.Call()
}
4个回答

23
你似乎忽略了嵌入接口如何影响Go中的结构。嵌入将嵌入类型(struct或interface无所谓)的所有方法提升为父类型的方法,但使用嵌入对象作为接收器调用。
这样做的实际副作用是,将接口嵌入到结构中可确保该结构履行其嵌入的接口,因为它定义了该接口的所有方法。 但如果没有定义填充结构中接口字段的内容,则尝试调用其中任何一个方法都会导致恐慌,因为默认情况下该接口字段为nil。
结果,您的类型断言将始终为true。BaseAppController嵌入了IGet和IList接口,因此始终履行两个接口。
如果您想要鸭子类型(duck-typing),即基于类型上的方法的存在或缺失选择性地启用行为,您需要使用类似于标准库 io.WriterTo 接口 的东西。该接口和 io.ReaderFrom 是可选接口,io.Writerio.Reader 对象可以实现它们以直接从另一个源写入或读取,而不是 io 包需要自己缓冲读取数据或要写入的数据。
基本要点是您定义了一个具有所需方法的底层接口,并将其传递。然后,您有一个或多个可选接口,您可以检查传递的类型是否满足这些接口,并在满足时使用该可选接口的方法(如果不满足,则恢复默认行为)。在这种情况下,不需要嵌入。

接口嵌入,不仅是为了鸭子类型,更多的是关于多态性。例如,如果您想要访问SQL数据库,但希望能够处理标准数据库调用和事务内调用,您可以创建一个结构来保存这两种类型(sql.DBsql.Tx)的共同方法:

type dber interface {
    Query(query string, args ...interface{}) (*sql.Rows, error)
    QueryRow(query string, args ...interface{}) *sql.Row
    Exec(query string, args ...interface{}) (sql.Result, error)
}

然后您可以制作如下结构:
type DBHandle struct {
    dber
}

现在您可以将sql.DBsql.Tx存储在结构体的dber插槽中,使用DBHandle(以及DBHandle本身的所有方法)的任何内容都可以在DBHandle上调用Query()QueryRow()Exec(),而不需要知道它们是否在事务范围内被调用(请记住,接口字段必须首先初始化!)。
这种功能是嵌入式真正开始发挥作用的地方,因为它允许实现类似于完全多态继承系统的功能和灵活性,而无需显式的“implements”或“extends”语句。但对于您正在寻找的动态鸭子类型行为,它并不是真正有用的。

5
请不要将接口嵌入结构体嵌入混淆。
如果在结构体上嵌入接口,实际上是向结构体添加了新的字段,名称为接口名称,因此如果您不初始化它们,将会出现恐慌,因为它们是nil。
您的BaseAppController希望有人填写IGetIList字段,这些字段需要满足IGetIList接口。
这就是您的BaseAppController结构体的真实样子:
type BaseAppController struct {
    Application *Application
    IGet        IGet
    IList       IList
}

看起来你正在尝试使用Go语言进行Java式编程,但这样做通常会带来不好的结果。


1
这是更多的Python风格代码(鸭子类型),如果你是“X”并实现了“Y”,那么添加以下行为。尽管我意识到这实际上是不可能的。 - koblas
1
无论是结构体还是接口都没关系。在这两种情况下,您都需要向结构体添加另一个字段,该字段将与嵌入类型具有相同的名称。它会出现恐慌,因为在Go中,接口内部只是两个指针-指向类型信息和指向数据的指针。如果接口类型的字段为nil,则两个指针都为nil,当尝试对其进行操作时,您将会得到恐慌。如果您嵌入了一个指向结构体的指针,则支持字段将默认为nil,并且在尝试访问它时会出现恐慌。 - creker

1

如果你想继承任何基础结构的结构体,你可以编写这个(丑陋的)代码,它像反射包一样工作:

package main

import (
"fmt"
"unsafe"
)

type i interface{
  i() interface{}
  ct(i)
}

type t1 struct{
  rptr unsafe.Pointer
}

func(x *t1) i() interface{} {
  // parent struct can view child changed value, stored in rptr, as original value type with changes after store, instead of i interface
  rv:= *(*i)(x.rptr)
  fmt.Printf("%#v %d\n", rv, rv.(*t2).a)
  return rv
}

func(x *t1) ct(vv i){
  // store pointer to child value of i interface type
  // we can store any of types, i is for the sample
  x.rptr = unsafe.Pointer(&vv)
}

type t2 struct{
  t1
  a int
}


func main() {
  t:=&t2{}
  t.ct(t) // store original
  t.a = 123 // change original
  ti:=(t.i()).(*t2) // t.i() is a method of parent (embedded) struct, that return stored value as original with changes in interface{}
  fmt.Printf("%#v %d\n",ti, ti.a)
}

这个样例不太好。 最佳样例可以包含接口字段:

type t1 struct{
  original i
}

1

将其命名为er:Getter而不是IGet
您无需在结构中嵌入接口方法,只需确保您的结构具有Get接收器方法即可。

调用空接口值的任何方法都会导致恐慌:
您定义了ctrl.IGet但未初始化它,
在Init()的第一个if语句内添加此行:

fmt.Printf("f=%T IGet:T=%T V=%[2]v\n", f, ctrl.IGet)

输出结果为:

f=*main.BaseAppController IGet:T=<nil> V=<nil>

变量总是被初始化为一个明确定义的值,接口也不例外。接口的零值将其类型和值组件都设置为nil。
您的BaseAppController缺少Get接口方法。
并将示例代码中的最后一个方法编辑为以下内容:
func (ctrl *BaseAppController) Get(v int) {
    fmt.Printf("Hi name=%s v=%d\n", ctrl.name, v)
}

现在你的代码可以运行。

如果你想以最小的更改修复当前的代码,只需用以下代码替换你的主函数(请注意,这里的ctrl是指针):

func main() {
    app := Application{"hithere"}
    ctrl := &TestController{&BaseAppController{Application: &app}}
    ctrl.IGet = interface{}(ctrl).(IGet)
    ctrl.Init()
    ctrl.Call()
}

工作示例代码(最小更改版):

package main

import "fmt"

type IGet interface {
    Get(int)
}

type IList interface {
    List(int)
}

type Application struct {
    name string
}

type BaseAppController struct {
    *Application

    IGet
    IList
}

type TestController struct {
    *BaseAppController
}

func (ctrl *BaseAppController) Init() {
    fmt.Println("In Init")

    if f, ok := interface{}(ctrl).(IGet); ok {
        fmt.Println("Controller Found GET", f)
    } else {
        fmt.Println("Controller NOT Found GET", f)
    }

    if f, ok := interface{}(ctrl).(IList); ok {
        fmt.Println("Controller Found LIST", f)
    } else {
        fmt.Println("Controller NOT Found LIST", f)
    }
}

func (ctrl *BaseAppController) Call() {
    fmt.Println("In Call")

    if f, ok := interface{}(ctrl).(IGet); ok {
        fmt.Println("Controller Found GET - going to call", f)

        f.Get(7)
    } else {
        fmt.Println("Controller NOT Found GET - can't call", f)
    }
}

// Test controller implements the Get Method
func (ctrl *TestController) Get(v int) {
    fmt.Printf("Hi name=%s v=%d\n", ctrl.name, v)
}

func main() {
    app := Application{"hithere"}
    ctrl := &TestController{&BaseAppController{Application: &app}}
    ctrl.IGet = interface{}(ctrl).(IGet)
    ctrl.Init()
    ctrl.Call()
}

也可以这样做(嵌入接口已移除):
package main

import "fmt"

type IGet interface {
    Get(int)
}

type IList interface {
    List(int)
}

type Application struct {
    name string
}

type BaseAppController struct {
    *Application
}

type TestController struct {
    *BaseAppController
}

func (ctrl *TestController) Init() {
    fmt.Println("In Init")

    if f, ok := interface{}(ctrl).(IGet); ok {
        fmt.Println("Controller Found GET", f)
    } else {
        fmt.Println("Controller NOT Found GET", f)
    }

    if f, ok := interface{}(ctrl).(IList); ok {
        fmt.Println("Controller Found LIST", f)
    } else {
        fmt.Println("Controller NOT Found LIST", f)
    }
}

func (ctrl *TestController) Call() {
    fmt.Println("In Call")

    if f, ok := interface{}(ctrl).(IGet); ok {
        fmt.Println("Controller Found GET - going to call", f)

        f.Get(7)
    } else {
        fmt.Println("Controller NOT Found GET - can't call", f)
    }
}

// Test controller implements the Get Method
func (ctrl *TestController) Get(v int) {
    fmt.Printf("Hi name=%s v=%d\n", ctrl.name, v)
}

func main() {
    app := Application{"hithere"}
    ctrl := TestController{&BaseAppController{Application: &app}}

    ctrl.Init()

    ctrl.Call()
}

输出:

In Init
Controller Found GET &{0xc082026028}
Controller NOT Found LIST <nil>
In Call
Controller Found GET - going to call &{0xc082026028}
Hi name=hithere v=7

实际代码从未有过IGet(只是为了举例懒惰)。您发布的第二个版本将Init放在最终结构上,这当然使其可以访问接口。如果我的目标是拥有有效和抽象的基类,在每个控制器上不必拥有具体实现,那么这不可能吗?它可能不是go,但它会非常有用。FYI-其中很多都是我试图理解接口可以做什么的可能性。 - koblas
@koblas 完全有可能,你只是在反向思考。不要嵌入基类“可以但不必须”实现的接口(因为按定义嵌入会使其实现它们),而是将基类嵌入还实现了 IGetIList 的附加结构中,并拥有一个包含所需方法以传递的接口。示例:https://play.golang.org/p/SXuGc8fAMY - Kaedys

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接