理解 Go interface 的 5 个关键点

1、interface 是一种类型

 

type I interface {
    Get() int
}

首先 interface 是一种类型,从它的定义可以看出来用了 type 关键字,更准确的说 interface 是一种具有一组方法的类型,这些方法定义了 interface 的行为。

go 允许不带任何方法的 interface ,这种类型的 interface 叫 empty interface

如果一个类型实现了一个 interface 中所有方法,我们说类型实现了该 interface,所以所有类型都实现了 empty interface,因为任何一种类型至少实现了 0 个方法。go 没有显式的关键字用来实现 interface,只需要实现 interface 包含的方法即可。

2、interface 变量存储的是实现者的值

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s *S) Set(age int) {
    s.Age = age
}

//3
func f(i I){
    i.Set(10)
    fmt.Println(i.Get())
}

func main() {
    s := S{} 
    f(&s)  //4
}

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
root@ubuntu:~/go_learn/example.com/hello# ./hello 
10

 

这段代码在 #1 定义了 interface I,在 #2 用 struct S 实现了 I 定义的两个方法,接着在 #3 定义了一个函数 f 参数类型是 I,S 实现了 I 的两个方法就说 S 是 I 的实现者,执行 f(&s) 就完了一次 interface 类型的使用。

interface 的重要用途就体现在函数 f 的参数中,如果有多种类型实现了某个 interface,这些类型的值都可以直接使用 interface 的变量存储

 

s := S{}
var i I //声明 i 
i = &s //赋值 s 到 i
fmt.Println(i.Get())

 


不难看出 interface 的变量中存储的是实现了 interface 的类型的对象值,这种能力是 duck typing。在使用 interface 时不需要显式在 struct 上声明要实现哪个 interface ,只需要实现对应 interface 中的方法即可,go 会自动进行 interface 的检查,并在运行时执行从其他类型到 interface 的自动转换,即使实现了多个 interface,go 也会在使用对应 interface 时实现自动转换,这就是 interface 的魔力所在。 

定义interface中不存在的方法

 

hello.go

 1 package main
 2 import "fmt"
 3 //1
 4 type I interface {    
 5     Get() int
 6     Set(int)
 7 }
 8 
 9 //2
10 type S struct {
11     Age int
12 }
13 
14 func(s S) Get()int {
15     return s.Age
16 }
17 
18 func(s *S) Set(age int) {
19     s.Age = age
20 }
21 
22 func(s *S) show() {
23     fmt.Println(s.Age)
24 }
25 
26 //3
27 func f(i I){
28      i.show()
29 }
30 
31 func main() {
32     s := S{} 
33     f(&s)  //

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
# hello
./hello.go:28:7: i.show undefined (type I has no field or method show)

3、如何判断 interface 变量存储的是哪种类型

一个 interface 被多种类型实现时,有时候我们需要区分 interface 的变量究竟存储哪种类型的值,go 可以使用 comma, ok 的形式做区分 value, ok := em.(T)em 是 interface 类型的变量,T代表要断言的类型,value 是 interface 变量存储的值,ok 是 bool 类型表示是否为该断言的类型 T

 

if t, ok := i.(*S); ok {
    fmt.Println("s implements I", t)
}

ok 是 true 表明 i 存储的是 *S 类型的值,false 则不是,这种区分能力叫 Type assertions (类型断言)。

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s *S) Set(age int) {
    s.Age = age
}

func(s *S) show() {
    fmt.Println(s.Age)
}

func judge(i I){
   if t, ok := i.(*S); ok {
           fmt.Println("s implements I", t)
   }
}

func main() {
    s := S{} 
    judge(&s)  //4
}

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
root@ubuntu:~/go_learn/example.com/hello# ./hello 
s implements I &{0}

 

 

如果需要区分多种类型,可以使用 switch 断言,更简单直接,这种断言方式只能在 switch 语句中使用。

switch t := i.(type) {
case *S:
    fmt.Println("i store *S", t)
case *R:
    fmt.Println("i store *R", t)
}

 

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s *S) Set(age int) {
    s.Age = age
}

func(s *S) show() {
    fmt.Println(s.Age)
}

func judge(i I){
     switch t := i.(type) {
     case *S:
            fmt.Println("i store *S", t)
     case *int:
            fmt.Println("i store *int", t)
    }
}


func main() {
    s := S{} 
    judge(&s)  //4
}

编译报错

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
# hello
./hello.go:30:6: impossible type switch case: i (type I) cannot have dynamic type *int (missing Get method)

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s *S) Set(age int) {
    s.Age = age
}

func(s *S) show() {
    fmt.Println(s.Age)
}

func judge(i I){
     switch t := i.(type) {
     case *S:
            fmt.Println("i store *S", t)
    }
}


func main() {
    s := S{} 
    judge(&s)  //4
}

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
root@ubuntu:~/go_learn/example.com/hello# 

4、空的 interface

interface{} 是一个空的 interface 类型,根据前文的定义:一个类型如果实现了一个 interface 的所有方法就说该类型实现了这个 interface,空的 interface 没有方法,所以可以认为所有的类型都实现了 interface{}。如果定义一个函数参数是 interface{} 类型,这个函数应该可以接受任何类型作为它的参数。

 

func doSomething(v interface{}){    
}

如果函数的参数 v 可以接受任何类型,那么函数被调用时在函数内部 v 是不是表示的是任何类型?并不是,虽然函数的参数可以接受任何类型,并不表示 v 就是任何类型,在函数 doSomething 内部 v 仅仅是一个 interface 类型,之所以函数可以接受任何类型是在 go 执行时传递到函数的任何类型都被自动转换成 interface{}。go 是如何进行转换的,以及 v 存储的值究竟是怎么做到可以接受任何类型的,感兴趣的可以看看 Russ Cox 关于 interface 的实现 。

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
root@ubuntu:~/go_learn/example.com/hello# ./hello 
i store *S &{0}

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s *S) Set(age int) {
    s.Age = age
}

func(s *S) show() {
    fmt.Println(s.Age)
}

func judge(i interface{}){
     switch t := i.(type) {
     case *S:
            fmt.Println("i store *S", t)
     case *int:
            fmt.Println("i store *int", t)
    }
}


func main() {
    s := S{} 
    judge(&s)  //4
}

 

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
root@ubuntu:~/go_learn/example.com/hello# ./hello 
i store *S &{0}

底层结构

空接口的底层结构记录在反射包中 reflect/value.go

type emptyInterface struct {
   typ  *rtype            // 类型描述
   word unsafe.Pointer    // 值
}

正如之前解释的那样,我们可以清楚的看到空结构体有一个类型描述字段和一个包含着值的字段。

rtype 结构体包含了类型的基本描述信息:

type rtype struct {
   size       uintptr
   ptrdata    uintptr
   hash       uint32
   tflag      tflag
   align      uint8
   fieldAlign uint8
   kind       uint8
   alg        *typeAlg
   gcdata     *byte
   str        nameOff
   ptrToThis  typeOff
}

在这些字段中,有些非常简单,且广为人知:

  • size 是以字节为单位的大小
  • kind 包含类型有:int8,int16,bool 等。
  • align 是变量与此类型的对齐方式

根据空接口嵌入的类型,我们可以映射导出字段或列出方法: | 译者注:方法在结构体最下面,这篇文章中是看不到的;需要先将这个 rtye 映射成 结构体才能看到,映射是基于 tflag 做的

type structType struct {
   rtype
   pkgPath name
   fields  []structField
}

这个结构还有两个映射,包含字段列表。它清楚地表明,将内建类型转换为空接口将导致扁平转换(译者注:不需要做其他额外的处理),其中字段的描述及值将存储在内存中。

下边是我们看到的空结构体的表示:

结构体由两个指针构成

现在让我们看看空接口实际上可以实现哪种转换。

 

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
root@ubuntu:~/go_learn/example.com/hello# cat hello.go
package main
func main() {
    var i int8 = 1
    read(i)
}

//go:noinline
func read(i interface{}) {
    n := i.(int16)
    println(n)
}
root@ubuntu:~/go_learn/example.com/hello# ./hello 
panic: interface conversion: interface {} is int8, not int16

goroutine 1 [running]:
main.read(0x79180, 0xf0681)
        /root/go_learn/example.com/hello/hello.go:9 +0x78
main.main()
        /root/go_learn/example.com/hello/hello.go:4 +0x38
root@ubuntu:~/go_learn/example.com/hello# 

虽然转换 int8 为 int16 是有效的,但程序还是会 panic 异常:

 

让我们生成 asm 代码,以便查看 Go 执行的检查:

生成汇编码过程中检查空接口类型

有以下几个步骤:

  • 步骤 1:比较 int16 类型与空接口的内部类型:比较(指令CMPQint16类型(加载有效地址LEAQ(Load Effective Address)到空接口的内部类型(从空接口MOVQ的内存段读取 48 字节偏移量的内存的指令)

  • step 2:JNE 指令,即不相等则跳转指令(Jump if Not Equal),会跳转到已生成的处理错误的指令,这些指令将在步骤中处理错误 3

  • 步骤 3:代码将 panic 并生成我们上面看到的错误信息

  • 步骤 4:这是错误指令的结束。此特定指令由显示指令的错误消息引用:main.go:10 +0x7d

任何从空接口内部类型的转换,都应该在原始类型转换完成后进行。这种转换为空接口,然后转换回原始类型会导致程序损耗。让我们运行一些基准测试来简单了解一下。 | 译者注:这句话是说,比如 interface{} 存了一个 int16; 需要转换为 int32 时,不能直接 interface{}-> int32;应该是 interface{}->int16->int32,这也是上面的例子 panic 的原因

 

 

既然空的 interface 可以接受任何类型的参数,那么一个 interface{}类型的 slice 是不是就可以接受任何类型的 slice ?

 

 

 

package main
import "fmt"
func printAll(vals []interface{}) { //1
for _, val := range vals {
fmt.Println(val)
}
}

func main(){
names := []string{"stanley", "david", "oscar"}
printAll(names)
}

 

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
# hello
./hello.go:11:9: cannot use names (type []string) as type []interface {} in argument to printAll

上面的代码是按照我们的假设修改的,执行之后竟然会报 cannot use names (type []string) as type []interface {} in argument to printAll 错误,why?

这个错误说明 go 没有帮助我们自动把 slice 转换成 interface{} 类型的 slice,所以出错了。go 不会对 类型是interface{} 的 slice 进行转换 。为什么 go 不帮我们自动转换,一开始我也很好奇,最后终于在 go 的 wiki 中找到了答案 https://github.com/golang/go/wiki/InterfaceSlice 大意是 interface{} 会占用两个字长的存储空间,一个是自身的 methods 数据,一个是指向其存储值的指针,也就是 interface 变量存储的值,因而 slice []interface{} 其长度是固定的N*2,但是 []T 的长度是N*sizeof(T),两种 slice 实际存储值的大小是有区别的(文中只介绍两种 slice 的不同,至于为什么不能转换猜测可能是 runtime 转换代价比较大)。

但是我们可以手动进行转换来达到我们的目的。

 

问题的原因

首先需要明白,[]interface{} 不是接口,二是一个切片,其元素类型为 interface{},即该切片中的元素实际可为任意类型。

其次,[]MyType 切片与 []interface{} 切片的内存布局是完全不同的:

  • 每个 interface{} 接口占用两个字(一个字表示所包含内容的类型,另一个字表示所包含的数据或指向数据的指针),于是长度为 N 的 []interface{} 切片,由 N * 2 个字长的数据块支持;
  • 而同等长度的 []MyType 切片,其数据块的长度为 N * sizeof(MyType) 个字。

由此而知,两者的数据结构不一致,便无法赋值了。

如何解决?

创建等长的 []interface{} 切片,并逐一赋值。代码如下:

root@ubuntu:~/go_learn/example.com/hello# cat hello.go
package main

import "fmt"

func main() {
slice := []int{1, 2, 3}
sliceI := make([]interface{}, len(slice))
for i, val := range slice {
sliceI[i] = val
}
fmt.Println(sliceI)
}
root@ubuntu:~/go_learn/example.com/hello# 

 

root@ubuntu:~/go_learn/example.com/hello# go build -o hello .
root@ubuntu:~/go_learn/example.com/hello# ./hello 
[1 2 3]

 

 

nil 和 non-nil  

我们可以通过一个例子理解Go 语言的接口类型不是任意类型这一句话,下面的代码在 main 函数中初始化了一个 *TestStruct 类型的变量,由于指针的零值是 nil,所以变量 s 在初始化之后也是 nil

 

package main
import "fmt"

type TestStruct struct{}

func NilOrNot(v interface{}) bool {
return v == nil
}

func main() {
var s *TestStruct
fmt.Println(s == nil)      // #=> true
fmt.Println(NilOrNot(s))   // #=> false
}

 

 

root@ubuntu:~/go_learn/example.com/hello# ./hello 
true
false

 

我们简单总结一下上述代码执行的结果:

  • 将上述变量与 nil 比较会返回 true
  • 将上述变量传入 NilOrNot 方法并与 nil 比较会返回 false

出现上述现象的原因是 —— 调用 NilOrNot 函数时发生了隐式的类型转换,除了向方法传入参数之外,变量的赋值也会触发隐式类型转换。在类型转换时,*TestStruct 类型会转换成 interface{} 类型,转换后的变量不仅包含转换前的变量,还包含变量的类型信息 TestStruct,所以转换后的变量与 nil 不相等。

 

 

5、interface 的实现者的 receiver 如何选择

在我们上文的例子中调用 f 是 f(&s) 也就是 S 的指针类型,为什么不能是 f(s) 呢,如果是 s 会有什么问题?改成 f(s) 然后执行代码。

cannot use s (type S) as type I in argument to f:
    S does not implement I (Set method has pointer receiver)

这个错误的意思是 S 没有实现 I,哪里出了问题?关键点是 S 中 set 方法的 receiver 是个 pointer *S 。

interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver,上面代码中的 S 的 Set receiver 是 pointer,也就是实现 I 的两个方法的 receiver 一个是 value 一个是 pointer,使用 f(s)的形势调用,传递给 f 的是个 s 的一份拷贝,在进行 s 的拷贝到 I 的转换时,s 的拷贝不满足 Set 方法的 receiver 是个 pointer,也就没有实现 I。go 中函数都是按值传递即 passed by value

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s *S) Set(age int) {
    s.Age = age
}

//3
func f(i I){
    i.Set(10)
    fmt.Println(i.Get())
}

func main() {
    s := S{} 
    f(s)  //4
}

 

 

 

root@ubuntu:~/go_learn/example.com/hello# go build  -o hello .
# hello
./hello.go:30:6: cannot use s (type S) as type I in argument to f:
        S does not implement I (Set method has pointer receiver)

 

这个错误的意思是 S 没有实现 I,哪里出了问题?关键点是 S 中 set 方法的 receiver 是个 pointer *S 。

interface 定义时并没有严格规定实现者的方法 receiver 是个 value receiver 还是 pointer receiver,上面代码中的 S 的 Set receiver 是 pointer,也就是实现 I 的两个方法的 receiver 一个是 value 一个是 pointer,使用 f(s)的形势调用,传递给 f 的是个 s 的一份拷贝,在进行 s 的拷贝到 I 的转换时,s 的拷贝不满足 Set 方法的 receiver 是个 pointer,也就没有实现 I。go 中函数都是按值传递即 passed by value

那反过来会怎样,如果 receiver 是 value,函数用 pointer 的形式调用?

更改

func(s *S) Set(age int)
改成
func(s S) Set(age int)

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s S) Set(age int) {
    s.Age = age
}

//3
func f(i I){
    i.Set(10)
    fmt.Println(i.Get())
}

func main() {
    s := S{} 
    f(s)  //4
}

等于0不是10

root@ubuntu:~/go_learn/example.com/hello# go build  -o hello .
root@ubuntu:~/go_learn/example.com/hello# ./hello 
0
root

 

package main
import "fmt"
//1
type I interface {    
    Get() int
    Set(int)
}

//2
type S struct {
    Age int
}

func(s S) Get()int {
    return s.Age
}

func(s S) Set(age int) {
    s.Age = age
}

//3
func f(i I){
    i.Set(10)
    fmt.Println(i.Get())
}

func main() {
    s := S{} 
    f(s)  //4
    f(&s)  //4
}

 

 

root@ubuntu:~/go_learn/example.com/hello# go build  -o hello .
root@ubuntu:~/go_learn/example.com/hello# ./hello 
0
0

I 的实现者 SS 的方法 receiver 都是 value receiver,执行代码可以看到无论是 pointer 还是 value 都可以正确执行。

导致这一现象的原因是什么?

如果是按 pointer 调用,go 会自动进行转换,因为有了指针总是能得到指针指向的值是什么,如果是 value 调用,go 将无从得知 value 的原始值是什么,因为 value 是份拷贝。go 会把指针进行隐式转换得到 value,但反过来则不行

对于 receiver 是 value 的 method,任何在 method 内部对 value 做出的改变都不影响调用者看到的 value,这就是按值传递。

理解 Go interface 的 5 个关键点

posted on 2021-07-16 15:30  tycoon3  阅读(368)  评论(0编辑  收藏  举报

导航