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 协议》,转载必须注明作者和本文链接
推荐文章: