go 中的结构体嵌入和接口嵌入 | go 技术论坛-380玩彩网官网入口
之前刚学 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
都包含了一个指向 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 协议》,转载必须注明作者和本文链接