综述

在 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
}

测试表明

  1. 如果结构体本身并未有和接口冲突的名字,则等价于自己也拥有了接口的实现,两种调用方法效果一致
  2. 否则两种方法调用各自的实现函数

但此时我们仍未知道这三个对象调用的函数之间是否具有关联,还是各自拥有了函数的副本。换句话说,变量名的冲突是如何解决的。

按照 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()。那我们会好奇:

  1. 如果 Inner 或者叫 O1.Interface 中没有Value这个变量,那么O1.Print()会如何呢?这种情况其实是不可能的,因为 Inner 为了实现Interface 这个接口,必然要用到Value。
  2. 如果O2中没有声明Value这个变量呢?O2自己的Print函数会调用O2.Interface的Value吗?答案是无法通过编译,原理其实和上面是一样的,编译的时候会变量生存期只局限于接收器,不会跨越结构体层次寻找。

总结

这样,我们就讲清了结构体中嵌入接口会遇到的各种情况。在 golang 的Context 包里,正是使用了这样的特性,才实现了 Context.Value 的逐级向上查找。