综述
在 go 语言的教程中,结构体嵌入结构体或者接口嵌入接口的说明是比较详细的,但对在结构体中嵌入接口的说明则比较少见。本文将对其本质进行探讨。
在结构体声明中嵌入接口之后,再对这一接口进行赋值,即可使用这一特性。如果未赋值,则这一接口(相当于一变量)为 nil。对其调用则有两种方法,Struct.Interface.Method 或 Struct.Method, 但其实这两种方法在某些情况下是有区别的。
构造测试用例
package main
import "fmt"
// 定义接口
type Interface interface {
Print()
Inc()
}
// Inner 结构体直接实现了这一接口
type Inner struct {
Value int
}
func(s * Inner) Print() {
fmt.Println("Inner, Value = ", s.Value)
}
func(s * Inner) Inc() {
fmt.Println("Inner Value Inc")
s.Value++
}
// Inner 结构体将直接嵌入 Outer1 中,而 Outer1 并未实现 Interface 接口
type Outer1 struct {
Interface
Value int
}
// Inner 结构体将直接嵌入 Outer2 中,但 Outer2 自己也实现了 Interface 接口,造成了函数名和变量名的冲突。
type Outer2 struct {
Interface
Value int
}
func(s * Outer2) Print() {
fmt.Println("Outer2, Value = ", s.Value)
}
func(s * Outer2) Inc() {
fmt.Println("Outer2 Value Inc")
s.Value++
}
测试1
func main() {
I: = & Inner {
Value: 0
}
O1: = & Outer1 {
Interface: I,
Value: 1
}
O2: = & Outer2 {
Interface: I,
Value: 2
}
I.Print() //Inner, Value = 0
O1.Print() //Inner, Value = 0
O1.Interface.Print() //Inner, Value = 0
O2.Print() //Outer2, Value = 2
O2.Interface.Print() //Inner, Value = 0
}
测试表明
- 如果结构体本身并未有和接口冲突的名字,则等价于自己也拥有了接口的实现,两种调用方法效果一致
- 否则两种方法调用各自的实现函数
但此时我们仍未知道这三个对象调用的函数之间是否具有关联,还是各自拥有了函数的副本。换句话说,变量名的冲突是如何解决的。
按照 go 的原理,我们可以大胆推测,对接口名的赋值过程,其实是让接口名(指针)指向了某个对象,这个过程中并没有发生拷贝。
测试2
func main() {
I: = & Inner {
Value: 0
}
O1: = & Outer1 {
Interface: I,
Value: 1
}
O2: = & Outer2 {
Interface: I,
Value: 2
}
// ----------------------
I.Inc() //Inner Value Inc
I.Print() //Inner, Value = 1
O1.Print() //Inner, Value = 1
O1.Interface.Print() //Inner, Value = 1
O2.Print() //Outer2, Value = 2
O2.Interface.Print() //Inner, Value = 1
}
确实如我们所料,除了 O2.Print() 使用了 O2 自己的 Value 之外,其他均使用了同一个 I 的 Value。这说明,结构体里的接口名确实是一个指针。
测试3
从测试1中我们可以看到,O1.Print()并没有使用O1.Value,这其实是很自然的,因为它调用的函数毕竟其实是O1.Interface.Print()。那我们会好奇:
- 如果 Inner 或者叫 O1.Interface 中没有Value这个变量,那么O1.Print()会如何呢?这种情况其实是不可能的,因为 Inner 为了实现Interface 这个接口,必然要用到Value。
- 如果O2中没有声明Value这个变量呢?O2自己的Print函数会调用O2.Interface的Value吗?答案是无法通过编译,原理其实和上面是一样的,编译的时候会变量生存期只局限于接收器,不会跨越结构体层次寻找。
总结
这样,我们就讲清了结构体中嵌入接口会遇到的各种情况。在 golang 的Context 包里,正是使用了这样的特性,才实现了 Context.Value 的逐级向上查找。