Go接口与动态类型

静态类型和动态类型

golang的接口类型同时支持静态类型和动态类型,其中:

  • 静态类型(static type):指编译期确定的类型,例如普通变量的类型,var a int/var b string,一旦定义类型就不可变更,不合法的赋值也会在编译期报错;

  • 动态类型(dynamic type):指运行时装入接口值的具体类型;

定义一个接口值(interface value),%T能够正常获取动态类型,默认打印%#v能够获取动态值:

1
2
3
4
5
6
var i interface{}
i = 2
fmt.Printf("%T %#v\n", i, i) // <int> <2>

i = "EdenMo"
fmt.Printf("%T %#v\n", i, i) // <string> <EdenMo>
从定义而言,interface{}就是i的静态类型,int和string就是程序运行时的动态类型,其可以在程序运行时进行切换,类比到C++,C++多态使用了类继承方法,程序运行时父类指针可以指向父类、也可以指向子类对象;

any

golang 1.18+提供了any作为interface{}的别名,定义一个接口:

1
2
3
4
5
var a any
a = 8
fmt.Printf("%T %#v\n", a, a) //int 8
a = "EdenMo"
fmt.Printf("%T %#v\n", a, a) //string "EdenMo"

接口

实现动态类型的一个目的是达到运行时多态的目标,即根据对象的动态类型,在运行时决定调用的函数方法,接口值在运行时包含动态类型信息和动态值数据。

在C++中,通过继承体系和override声明来定义多态行为,而在Go的结构化类型系统(structural typing)中,只要结构体含有接口的方法,就定义了接口实现,无需显示说明继承,也无需说明override,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package main

import "fmt"

//接口类
type Reader interface {
Read(p []byte) (int, error) //只声明,不定义
}


//MyReader
type MyReader struct{}
func (MyReader) Read(p []byte) (int, error) {
fmt.Println("MyReader Called")
return 0, nil
}

//MyReader_other
type MyReader_other struct{}
func (MyReader_other) Read(p []byte) (int, error) {
fmt.Println("MyReader_other Called")
return 0, nil
}

func main() {
var a Reader
a = MyReader{}
fmt.Printf("%T\n", a) //main.MyReader

s := "TEST"
b := []byte(s)
a.Read(b) //MyReader Called

a = MyReader_other{}
fmt.Printf("%T\n", a) //main.MyReader_other
a.Read(b) //MyReader_other Called
}
可见,调用什么方法,取决于对象的运行动态类型是什么,这被称为动态分发。go实现动态分发存在和C++类似的虚函数表,称为方法表,根据对象动态类型确定接口对应类型的接收者方法是否被实现,从而找到对应方法的函数地址入口,对比静态调用,会增加间接寻址、减少内联空间的性能开销。

eface与iface

根据接口的元数据和内部类型不同,接口类型分为两种:eface和iface。

eface:interface{}或者any,其表示不带方法的接口,内部数据由类型type和数据data组成:

1
2
3
4
type eface struct{
_type *_type
data unsafe.Pointer
}

iface:表示带方法集的接口,内部数据由适配类型itab和数据data组成,itab关联了接口的类型信息、具体类型信息以及函数方法的入口、偏移等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//不同版本,实现细节会有差异
type iface struct{
tab *itab
data unsafe.Pointer
}

type itab struct{
inter *interfacetype //接口类型元信息
_type *_type //具体类型元信息
link *itab
bad int32
inhash int32
fun [1]uintptr //对应的函数入口表,1字节占位,实际会分配更多空间;
}

// runtime/type.go
type interfacetype struct {
typ _type //
pkgpath name //
mhdr []imethod // 接口方法声明列表,按字典序排序
}

// 接口的方法声明
type imethod struct {
name nameOff // 方法名(数据区偏移)
ityp typeOff // 描述方法参数返回值等细节(数据区偏移)
}

interface的nil

由于interface类型存在动态类型和动态值两部分,只有二者均为nil时,接口类型才会等于nil:

1
2
3
4
5
6
7
8
//动态类型和值都是nil
var x interface{} = nil
fmt.Println(x == nil)

//仅动态值为nil
var p *int = nil
var x1 interface{} = p
fmt.Printf("%t, %T, %#v", x1 == nil, x1, x1) //false, *int, (*int)(nil)

接口类型取值

为了获取interface中指定类型的值,golang提供了若干种途径,如类型断言、类型开关、反射等:

类型断言:支持直接取用(失败会panic)和ok判断两种接口,如下

1
2
3
4
5
6
7
8
9
10
11
12
var x interface{}
x = 30
i := x.(int)
fmt.Println(i) //30

// s := x.(string)
// fmt.Println(s) //error! panic: interface conversion: interface {} is int, not string

s1, ok := x.(string)
if !ok { //类型不是string
fmt.Println("s1 not string")
}

类型开关: 在流程控制switch已经介绍,switch直接支持接口值的类型萃取,如下

1
2
3
4
5
6
7
8
9
10
11
12
func g(x interface{}) {
switch v := x.(type) {
case nil:
fmt.Println("nil")
case int:
fmt.Println("int:", v)
case string:
fmt.Println("string:", v)
default:
fmt.Printf("other: %T\n", v)
}
}

golang的多文件编译

同包编译

若同个目录下,存在多个go文件,可以直接使用同一个包,编译整个目录即可,如:

1
2
3
4
alphaGo/
├── go.mod (module alphaGo)
├── main.go
└── test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//test.go
package main

type Greeter interface {
SayHello() string
SaySorry() string
}

type greeterImpl struct{}

func (g greeterImpl) SayHello() string {
return "Default Hello"
}

func (g greeterImpl) SaySorry() string {
return "Default Sorry"
}

func NewGreeter() Greeter {
return greeterImpl{}
}
1
2
3
4
5
6
7
8
9
10
//main.go
package main

import "fmt"

func main() {
greeter := NewGreeter()
fmt.Println(greeter.SayHello()) //Default Hello
fmt.Println(greeter.SaySorry()) //Default Sorry
}

因为Code-Runner插件的go默认是go run,只会编译单个文件,需要修改配置,设置中搜索:code-runner.executorMap对应的go指令,修改成目录编译:

1
"cd $dir && go run ."

异包导入

不同包编译,假设test.go现在独享一个目录:

1
2
3
4
5
alphaGo/
├── go.mod (module alphaGo)
├── main.go (package main)
└── test/
└── test.go (package test)
那么main要引入这个文件,就要按包引入了,test代码的包名修改为test,main对应的函数增加test的显式前缀调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//test.go
package test //独立自己的包

type Greeter interface {
SayHello() string
SaySorry() string
}

type greeterImpl struct{}

func (g greeterImpl) SayHello() string {
return "Default Hello"
}

func (g greeterImpl) SaySorry() string {
return "Default Sorry"
}

func NewGreeter() Greeter {
return greeterImpl{}
}
1
2
3
4
5
6
7
8
9
10
11
12
//main.go
package main
import (
"alphaGo/test" //按包引入
"fmt"
)

func main() {
greeter := test.NewGreeter() //显式使用test包的函数
fmt.Println(greeter.SayHello()) //Default Hello
fmt.Println(greeter.SaySorry()) //Default Sorry
}

反射

反射的机制是赋予程序获取RTTI(Run-Time Type Info, 运行时类型信息),C++实现反射的道路可谓道阻且长,直到去年年底,C++委员会终于决定将静态反射加入C++ 26,这需要克服许多过往的冲突问题,如C++一贯支持的零开销抽象、最小运行时等原则,但模板元编程的推力还是使得这一能力入库尘埃落定。而对C++另一个分支Qt而言(注:详见Qt Core Application:元对象/属性系统/反射),moc编译器允许为QObject对象生成QMetaObject元数据表,运行时通过按名称发现和调用实现动态查询、调用成员变量、函数、属性系统等,以适配各种Qt组件运行时绑定的需求。对于自实现的各种C++反射库,也需要各种宏和容器来保存类型信息;

golang的设计哲学一开始就没有沿着C++的旧路发展,golang需要自动管理gc、用户手动干预内存布局的能力很低,使得golang天然具有完整的RTTI信息,结合接口类型记录的动态类型能力,基本覆盖了动态信息相关大量的动态需求。

Type与Value

对应接口值的type和data,反射使用两个基础函数获取数据类型和运行时表示:

  • reflect.TypeOf: 获取对象的动态类型信息;

  • reflect.ValueOf:获取数据的运行时表示;

反射包的其他函数基本都围绕这两个函数及其方法展开。

reflect.Type是反射包定义的一个iface接口,这个接口含有一系列获取和判断类型相关的方法集,这里强推霜神的博客文章,摘录如下:

reflect.Type相关通用方法

这些方法适用于所有定义的数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
//reflect.Type可以使用operator==比较
type Type interface{
//返回该类型在内存占的字节数
Align() int

//返回该类型在结构体中的字节数
FieldAlign() int

//返回方法集第i个方法(按字典排序),i∈[0,NumMethod()], 否则panic
Method(int) Method

//返回方法集中名称为string的方法,返回方法集和是否找到
MethodByName(string) (Method, bool)

//返回可以使用Method访问的方法数量
NumMethod() int

//对定义类型,返回包中的类型名称;对于非定义类型,返回空字符串
Name() string

//返回定义类型的包路径
PkgPath() string

//返回存储给定类型的值的字节数
Size() uintptr

//返回类型的字符串表示
String() string

//返回类型的底层类型表示
Kind() Kind

//表示该类型是否实现了接口类型u
Implements(u Type) bool

//表示该类型是否可分配给类型u
AssignableTo(u Type) bool

//表示该类型值能否转换至u类型
ConvertibleTo(u Type) bool

//表示该类型是否可比较
Comparable() bool
}

通用方法细节

以下通过一系列例子详细说明了通用方法的各种反射用法和注意事项。

Align与FieldAlign

前者指示内存分配字节数的对齐字节,后者表示结构体字段对齐后的字节数,一般而言二者是相等的,只有极少架构上或者调用cgo才不同:

1
2
3
4
5
self := Self{name: "EdenMo", age: 18}
align_size := reflect.TypeOf(self).Align()
filed_size_name := reflect.TypeOf(self.name).FieldAlign()
filed_size_age := reflect.TypeOf(self.age).FieldAlign()
fmt.Println(align_size, filed_size_name, filed_size_age) // 8, 8, 8
此处,对string对象而言,本身也是结构体:
1
2
3
4
5
type StringHeader struct{
Data uintptr //指向底层数组指针
Len int //字符串长度
Cap int //容量
}
指针和int都是8字节对齐,故string的FieldAlign输出8,int也输出8;故Self结构体的最大对齐长度也是8字节,也输出8;

NumMethod、Method与MethodByName

首先需要先了解一下golang方法的导入和导出,以下。

导出(Exported)方法、未导出(Unexported)方法

golang使用方法名首字母的大小写来区分一个方法是否能被导出,未导出的方法只能在包内使用,其他包无法引用该包的未导出方法,如将test的SaySorry改成小写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package test

type Greeter interface {
SayHello() string
saySorry() string
}

type greeterImpl struct{}

func (g greeterImpl) SayHello() string {
return "Default Hello"
}

func (g greeterImpl) saySorry() string {
return "Default Sorry"
}

func NewGreeter() Greeter {
return greeterImpl{}
}
那么main无法调用Greeter的saySorry函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

import (
"alphaGo/test"
"fmt"
)

type MyGreeter struct {
test.Greeter
}

func (g MyGreeter) SayHello() string {
return "This is New Hello"
}

func main() {
base := test.NewGreeter()
fmt.Println(base.SayHello()) //Default Hello
//fmt.Println(base.saySorry()) //error! base.saySorry undefined (cannot refer to unexported method saySorry)

g := MyGreeter{}
fmt.Println(g.SayHello()) //This is New Hello
//fmt.Println(g.saySorry()) //error! g.saySorry undefined (cannot refer to unexported method saySorry)
}

NumMethod

NumMethod统计方法数:对非接口类型,只会统计能导出的方法;对接口类型,未导出的方法也会进行计数,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"reflect"
)

type Greeter interface {
SayHello() string
saySorry() string
}

type GreeterImpl struct {
Greeter
}

func main() {
greeter_interface_type := reflect.TypeOf((*Greeter)(nil)).Elem() //接口类型,统计包含未导出方法
fmt.Println(greeter_interface_type.NumMethod()) //2

fmt.Println(reflect.TypeOf(GreeterImpl{}).NumMethod()) //1, 非接口类型,仅统计导出方法
}
此处有个小语法糖,golang为了实例化一个接口对象,将nil指针转换成Greeter接口类型指针(*main.Greeter,Kind=ptr); 为了解引用,使用Elem()从指针获取接口对象的类型(main.Greeter, Kind=interface),在1.22+可以直接使用reflect.TypeFor:
1
2
3
4
5
6
//方法一:不太雅观
greeter_interface_type := reflect.TypeOf((*Greeter)(nil)).Elem() //接口类型,统计包含未导出方法
fmt.Println(greeter_interface_type.NumMethod()) //2

//1.22+:直接使用typefor避免指针转换:
fmt.Println(reflect.TypeFor[Greeter]().NumMethod())

Method

相关事项:

  • Method按字典排序,如果i访问越界会panic;

  • 对接口类型:返回Method的方法名、方法签名(无接收者)、Func字段为nil,无法调用,方法签名含未导出方法;

  • 对非接口类型:返回Method方法名、方法签名(带接收者),Func字段可调用,方法签名不含未导出方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package main

import (
"fmt"
"reflect"
)

// 接口声明
type Greeter interface {
SayHello(string, int) string
saySorry(string, int) string
}

// 具体实现
type GreeterImpl struct {
Greeter
}

func (g GreeterImpl) SayHello(name string, age int) string {
fmt.Println("SayHello Called", name, age)
return "OK"
}

func (g GreeterImpl) saySorry(name string, age int) string {
fmt.Println("saySorry Called", name, age)
return "Done"
}

func main() {
greeter_interface_type := reflect.TypeOf((*Greeter)(nil)).Elem()
impl := GreeterImpl{}

//匿名函数:传入反射类型、接收者实例和说明,打印返回结果
check := func(t reflect.Type, receiver any, declare string) {
for i := 0; i < t.NumMethod(); i++ {
m := t.Method(i)
fmt.Println(declare, i, "=", m.Name, m.Type)
if m.Func.IsValid() {
//此处,手动拼接接收者,参数包裹为reflect.Value类型
args := []reflect.Value{reflect.ValueOf(receiver), reflect.ValueOf("Eden"), reflect.ValueOf(18)}
result := m.Func.Call(args)
fmt.Println(result[0].String()) //只有一个string返回参数
} else {
fmt.Println("you f**king pass a interface not impl\n")
}
}
}

//接口类型:打印两个接口签名,但Func无效不可调用
// Interface Method 0 = SayHello func(string, int) string
// you f**king pass a interface not impl
// Interface Method 1 = saySorry func(string, int) string
// you f**king pass a interface not impl
check(greeter_interface_type, nil, "Interface Method")

//非接口类型:打印可导出接口的签名(NumMethod限制),Func可调用
// impl Method 0 = SayHello func(main.GreeterImpl, string, int) string
// SayHello Called Eden 18
// OK
impl_type := reflect.TypeOf(impl)
check(impl_type, impl, "impl Method")
}

MethodByName

MethodByName返回的的方法在接口和非接口类型上和上述Method函数表现是一致的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
impl := GreeterImpl{}
check := func(receiver any, declare string) {
t := reflect.TypeOf(receiver)

if m, ok := t.MethodByName("SayHello"); ok {
args := []reflect.Value{reflect.ValueOf(receiver), reflect.ValueOf("Eden"), reflect.ValueOf(18)}
result := m.Func.Call(args)
fmt.Println(declare, result[0].String())
} else {
fmt.Println(declare, "you f**king pass a interface not impl\n")
}
}
//SayHello Called Eden 18
// Impl MethodByName OK
check(impl, "Impl MethodByName")

Name、PkgPath、Size

Name():非定义类型,类似数组、slice、map、chan、func、匿名结构体、指针等,其Name()函数均返回空字符串;

PkgPath(): 自己type定义的类型、结构体才会打印包路径,预先声明的类型都是空字符串;

Size(): 返回内存占的字节数,但注意不代表容器的容量占用,如下输出的是slice的切片头描述的24个字节,无论slice的元素多少都是24字节;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type MyInt int
var i MyInt
var ii int
s := []int{1, 2, 3, 4, 5}
g := GreeterImpl{}

check := func(a any) {
t := reflect.TypeOf(a)
fmt.Println("Name:", t.Name(), "PkgPath:", t.PkgPath(), "Size:", t.Size())
}
check(i) //Name: MyInt PkgPath: main Size: 8
check(ii) //Name: int PkgPath: Size: 8
check(s) //Name: PkgPath: Size: 24
check(g) //Name: GreeterImpl PkgPath: main Size: 16

Implements

Implements: 手动完整实现接口会返回true,但如果直接匿名结构体嵌入也会返回true,因为接口提升,编译器会默认生成转发的实现方法,这样的代码能够通过编译,但在运行期会因为越界访问而报错:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"fmt"
"reflect"
)

// 接口声明
type Greeter interface {
SayHello(string, int) string
saySorry(string, int) string
}

type GreeterImpl struct {
Greeter
}
//默认生成类似:
// func (g GreeterImpl) saySorry(s string, i int) string {
// return g.Greeter.saySorry(s, i)
// }

func main() {
greet_interface_type := reflect.TypeFor[Greeter]()
impl := GreeterImpl{}

fmt.Println(reflect.TypeOf(impl).Implements(greet_interface_type)) //true

impl.saySorry("EdenMo", 19) //panic: runtime error: invalid memory address or nil pointer dereference
}

AssignableTo和ConvertibleTo

底层类型相同,不能赋值但可以转换:

1
2
3
4
5
6
7
type MyInt int
var i MyInt
var ii int
ti := reflect.TypeOf(i)
tii := reflect.TypeOf(ii)
fmt.Println(tii.AssignableTo(ti)) //false
fmt.Println(tii.ConvertibleTo(ti)) //true

reflect.Type类型专有方法

reflect.Value

不同的是,reflect.Value在反射包中被定义成一个结构体,结构体内部字段没有被暴露,只定义了一些获取和写入数据的方法:

1
2
3
4
type Value struct{...}
func (v Value) Addr()Value
func (v Value) Bool() bool
func (v Value) Bytes() []byte

reflect.TypeOf和reflect.ValueOf分别从接口的类型和接口的数据中取值并且转换成reflect.Type和reflect.Value,通过String()接口即可打印对应的类型和数据信息

1
2
3
4
5
var x any
x = "EdenMo"
t := reflect.TypeOf(x)
data := reflect.ValueOf(x)
fmt.Println(t.String(), data.String()) //string EdenMo
fmt.Print格式化显然也是用了反射信息,上述不显示使用String()接口也是OK的。

在golang中,类型定义是无限的,不同长度的数组、不同定义的结构体、不同嵌套的容器和结构体、或者使用type自行定义的都能创造新的类型,但其底层都是来自golang的26种类型,因此golang也支持使用Kind()访问对象的底层类型:

1
2
3
4
5
6
7
8
var x Self
fmt.Println(reflect.TypeOf(x)) //main.Self
fmt.Println(reflect.TypeOf(x).Kind()) //struct

type MyInt int
var i MyInt
fmt.Println(reflect.TypeOf(i)) //main.MyInt
fmt.Println(reflect.TypeOf(i).Kind()) //int

参考链接:

  1. Go reflection 三定律与最佳实践

  2. 图解 Go 语言:静态类型与动态类型

  3. 4.3 反射