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 是否等价?
答案当然是不等价。虽然 S1 和 S2 都包含了一个指向 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 将“继承”该类型的字段和方法。具体来说:
- 如果 - *T实现了某些接口的方法,那么这些方法会被视为- S1的一部分。因此,- S1的实例可以被视为实现了那些接口。
- 只要 - *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,所以它继承了 *T 的 M2 方法。因此,S1 的指针实例可以直接被赋值给 I2 类型的变量,并调用 M2 方法。
S2 没有嵌入T,而是将T作为普通字段包含
首先,对于S2这种结构体的定义不是嵌入 T,而是显式地定义了一个名为 T 的字段,其类型是 *T。这意味着:
- S2不会自动获得- T的任何方法。即使- *T实现了某些接口,- S2也不会自动实现这些接口。
- 为了访问 - T的方法,必须通过- S2.T字段来显式调用它们。
- 为了让 - 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 并没有自动获得 T 的 M2 方法,因此它不能直接被赋值给 I2 类型的变量。如果想让 S2 实现 I2 接口,需要为 S2 定义 M1 方法:
func (s2 S2) M2() {
    if s2.T != nil {
        s2.T.M2()
    }
}这样做之后,S2 的实例就可以被赋值给 I2 类型的变量,并且可以通过 S2 的 M2 方法间接调用 T 的 M1 方法。
总结
- S1 通过嵌入 *T继承了T的所有方法,因此它可以自动实现由*T方法集所涵盖的接口。
- S2 只是包含了一个指向 T的指针作为普通字段,因此它不会自动获得T的方法或接口实现。为了让S2实现某个接口,必须为S2定义相应的接口方法。
接口嵌入
关于接口 I 的实现:
- 在 S1中,因为I是一个匿名字段,如果S1的实例实现了I接口所要求的方法,那么该实例可以直接被赋值给I类型的变量。
- 对于 S2,由于I是一个命名字段,S2的实例不会自动被视为实现了I接口,除非它本身确实实现了I接口的方法。不过,S2.I字段可以持有任何实现了I接口的对象。
下面通过具体的代码示例来澄清 S1 和 S2 在实现接口 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 接口的对象 C,S2 的实例也不能直接被赋值给 I 类型的变量。这是因为 S2 中的 I 是一个命名字段,而不是匿名字段。因此,S2 不会自动获得 I 接口的方法集合,也不会被视为实现了 I 接口。然而,可以通过访问 S2.I 字段来获取实现了 I 接口的对象,并将其赋值给 I 类型的变量。
总结
- S1可以直接被视为实现了- I接口,因为它嵌入了实现了- I接口的对象。这种情况下,- S1的实例可以直接被赋值给- I类型的变量。
- S2则不能自动被视为实现了- I接口,因为它只是包含了一个实现了- I接口的对象作为命名字段。需要显式地通过- S2.I来访问实现了- I接口的对象。
本作品采用《CC 协议》,转载必须注明作者和本文链接
 
           wuvikr 的个人博客
 wuvikr 的个人博客
         
             
           
           关于 LearnKu
                关于 LearnKu
               
                     
                     
                     粤公网安备 44030502004330号
 粤公网安备 44030502004330号 
 
推荐文章: