go 中的结构体嵌入和接口嵌入 | go 技术论坛-金年会app官方网

之前刚学 go 的时候,对于结构体嵌入和接口嵌入这块一直感觉不太懂,最近重新看完托尼白老师的 go 语言第一课才算是弄懂了。记录一下。

先来看一个问题:下面代码中的 s1和 s2 等价么?如果不等价,区别在哪里?

type t struct{
    n int
}
func (t t) getn() int {
    return t.n
}
type i interface {
    m1()
}
type s1 struct {
    *t
    i
    s string
}
type s2 struct { 
    t *t
    i  i
    s string
}    

答案当然是不等价。虽然 s1s2 都包含了一个指向 t 类型的指针、一个 i 接口类型以及一个名为 s 的字符串字段,但它们在使用方式和行为上有着显著的区别。

s1属于匿名嵌入,而s2属于自定义字段。这里面的区别我们分成两部分来讨论:

  • 结构体类型嵌入。

  • 接口类型嵌入。

  • s1 因为嵌入了 *t,所以它继承了 t 的所有方法。这意味着如果 t 实现了某些接口,那么 s1 的实例也可以直接被视为实现了这些接口,只要这些接口的方法是定义在 *t 上的。(重要)
  • s2 没有嵌入 t,而是将 t 作为普通字段包含。因此,s2 并没有自动获得 t 的方法。因此,即使 t 实现了一些接口,s2 本身也不会自动被视为实现了这些接口。为了使 s2 实现某个接口,你需要为 s2 定义相应的方法。

下面详细解释一下这段话。

s1 嵌入了 *t 而“继承”了 t 的所有方法

当一个结构体(如 s1)嵌入了另一个类型(如 *t),这意味着 s1 将“继承”该类型的字段和方法。具体来说:

  1. 如果 *t 实现了某些接口的方法,那么这些方法会被视为 s1 的一部分。因此,s1 的实例可以被视为实现了那些接口。

  2. 只要 *t 的方法集中包含了某个接口的所有方法,并且这些方法的签名匹配接口的要求,s1 的实例就可以直接被赋值给该接口类型的变量,而无需额外定义方法。

示例代码

package main
import "fmt"
type i2 interface {
    m2()
}
type t struct {
    n int
}
// *t 实现了接口 i2
func (t *t) m2() {
    fmt.println("m2 from *t")
}
type s1 struct {
    *t // 嵌入 *t
    s  string
}
func main() {
    // 创建 s1 实例并赋值给 i2 类型变量
    var i i2 = &s1{t: new(t), s: "hello"} // 合法,因为 *t 实现了 i2
    i.m2()                                // 输出: m2 from *t
}

在这个例子中,s1 因为嵌入了 *t,所以它继承了 *tm2 方法。因此,s1 的指针实例可以直接被赋值给 i2 类型的变量,并调用 m2 方法。

s2 没有嵌入t,而是将t作为普通字段包含

首先,对于s2这种结构体的定义不是嵌入 t,而是显式地定义了一个名为 t 的字段,其类型是 *t。这意味着:

  1. s2 不会自动获得 t 的任何方法。即使 *t 实现了某些接口,s2 也不会自动实现这些接口。

  2. 为了访问 t 的方法,必须通过 s2.t 字段来显式调用它们。

  3. 为了让 s2 实现某个接口,需要为 s2 显式定义相应的方法,或者使用组合模式委托给 t 的方法。

示例代码

package main
import "fmt"
type i2 interface {
    m12()
}
type t struct {
    n int
}
// *t 实现了接口 i2
func (t *t) m2() {
    fmt.println("m2 from *t")
}
type s2 struct {
    t *t // 普通字段
    s   string
}
func main() {
    // 创建 s2 实例
    s2 := s2{t: new(t), s: "world"}
    // s2 的实例不能直接赋值给 i2 类型变量,因为 s2 没有实现 i2
    // 下面这行会报错
    // var i i2 = s2 // 编译错误
    // 正确的做法是通过 s2.t 来调用 m2 方法
    s2.t.m2() // 输出: m2 from *t
    // 如果想让 s2 实现 i2 接口,需要为 s2 定义 m2 方法
    // 或者使用组合模式委托给 t 的 m2 方法
}

在这个例子中,s2 并没有自动获得 tm2 方法,因此它不能直接被赋值给 i2 类型的变量。如果想让 s2 实现 i2 接口,需要为 s2 定义 m1 方法:

func (s2 s2) m2() {
    if s2.t != nil {
        s2.t.m2()
    }
}

这样做之后,s2 的实例就可以被赋值给 i2 类型的变量,并且可以通过 s2m2 方法间接调用 tm1 方法。

总结

  • s1 通过嵌入 *t 继承了 t 的所有方法,因此它可以自动实现由 *t 方法集所涵盖的接口。
  • s2 只是包含了一个指向 t 的指针作为普通字段,因此它不会自动获得 t 的方法或接口实现。为了让 s2 实现某个接口,必须为 s2 定义相应的接口方法。

关于接口 i 的实现:

  • s1 中,因为 i 是一个匿名字段,如果 s1 的实例实现了 i 接口所要求的方法,那么该实例可以直接被赋值给 i 类型的变量。
  • 对于 s2,由于 i 是一个命名字段,s2 的实例不会自动被视为实现了 i 接口,除非它本身确实实现了 i 接口的方法。不过,s2.i 字段可以持有任何实现了 i 接口的对象。

下面通过具体的代码示例来澄清 s1s2 在实现接口 i 方面的区别。

s1 中的匿名字段 i

type i interface {
    m1()
}
type c struct{}
func (c c) m1() {
    fmt.println("c.m1 called")
}
type s1 struct {
    *t // 嵌入了 *t
    i  // 嵌入了接口 i
    s  string
}
// 创建 s1 实例并赋值给 i 类型变量
func main() {
    var i i = &s1{t: new(t), i: c{}, s: "hello"} // 合法,因为 s1 包含了实现了 i 的 c
    i.m1()                                       // 输出: c.m1 called
}

在这个例子中,s1 包含了一个匿名字段 i,这意味着如果 s1 包含了一个实现了 i 接口的对象(例如 c),那么 s1 的实例可以直接被赋值给 i 类型的变量。这正是因为 s1 继承了 i 接口的方法集合。

s2 中的命名字段 i

type s2 struct {
    t *t // 显式定义了 *t 字段
    i  i // 显式定义了 i 字段
    s  string
}
// 创建 s2 实例并尝试赋值给 i 类型变量
func main() {
    var i i
    s2 := s2{t: new(t), i: c{}, s: "world"}
    // 下面这一行会报错,因为 s2 没有实现 i 接口
    // var i i = s2 // 编译错误
    // 正确的做法是使用 s2.i 字段
    i = s2.i // 合法,因为 s2.i 实现了 i 接口
    i.m1()   // 输出: c.m1 called
}

s2 的例子中,即使 s2 包含了一个实现了 i 接口的对象 cs2 的实例也不能直接被赋值给 i 类型的变量。这是因为 s2 中的 i 是一个命名字段,而不是匿名字段。因此,s2 不会自动获得 i 接口的方法集合,也不会被视为实现了 i 接口。然而,可以通过访问 s2.i 字段来获取实现了 i 接口的对象,并将其赋值给 i 类型的变量。

总结

  • s1 可以直接被视为实现了 i 接口,因为它嵌入了实现了 i 接口的对象。这种情况下,s1 的实例可以直接被赋值给 i 类型的变量。
  • s2 则不能自动被视为实现了 i 接口,因为它只是包含了一个实现了 i 接口的对象作为命名字段。需要显式地通过 s2.i 来访问实现了 i 接口的对象。
本作品采用《cc 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
网站地图