go 中的结构体嵌入和接口嵌入

之前刚学 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
}    

S1 和 S2 是否等价?

答案当然是不等价。虽然 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
(= ̄ω ̄=)··· 暂无内容!

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