go语言的类型系统和接口
【导读】本文梳理了 Go 语言的类型,并结合了反射进行介绍。
类型
go语言的类型分为内置类型和自定义类型,以及类型别名。
内置类型
go语言的内置类型是语言层面的底层实现,语法规定好的一些类型,具体有哪些内置类型可以看源码中用于文档的虚拟的builtin包:
https://github.com/golang/go/blob/master/src/builtin/builtin.go
自定义类型
-
自定义结构体类型; -
自定义函数类型; -
自定义接口类型; -
依据已有类型自定义新类型;
自定义类型表达式的关键字为type
,自定义结构体还需要配合struct
关键字,自定义函数类型还需要配合func
关键字,自定义接口类型还需要配合interface
关键字,样例如下:
// 自定义结构体类型 cType 即为类型名
// cType类型
type cType struct {
A int
}
// 依据已有自定义类型自定义一个新类型
// 类型cType1
// 特别注意的是cType类型如果定义有方法,那么cType1上是不会带过来的
type cType1 cType
// 依据已有内置类型自定义一个新类型
// 类型cInt
type cInt int
// go语言中func也是一种类型
// 因为func也可以作为值传递
type handler func(err error) error
// 自定义接口类型 iFace 即为类型名
type iFace interface {
DoSomething() bool // 接口是一组方法定义的集合,接口的方法定义不需要func关键词只需要方法签名
}
method
:内置类型是不能自定义方法的;接口类型是无效的方法接受者即interface
关键字定义的接口类型只能申明方法而不能定义方法。除了上述两个限制其他的自定义类型都是可以为其定义方法的即类型作为方法接收者。
类型别名
go语言本身内置类型就有别名的使用,如下:
// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte = uint8
// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune = int32
那想自定义一个类型别名写法就参考官方写法咯。需要注意的是:
-
类型的别名就是类型本身; -
内置类型的别名是无法自定义方法的:因为内置类型无法自定义方法; -
跨包类型的别名是无法自定义方法的:言外之意就是在同一个包内给类型定义别名是可以在别名上定义方法的,本质上就是在对被取别名的类型定义方法;
别名机制的写法是1.9引入的(参考资料①)。
类型元数据-MetaData
go运行时(runtime
)为每种类型定义了底层的类型描述信息数据,这些描述信息共同构成了类型元数据。runtime._type
这个结构体就是类型元数据的基石,记录了类型名称、大小、内存对齐边界、gc相关标识符等类型描述信息。
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
当然这个结构里的字段也有对应的类型,tflag
是一个基于uint8的自定义类型、typeOff
、nameOff
是基于int32的自定义类型(题外话:这两个字段中的Off并不是关闭的意思而是offset即偏移量的中二简写形式),然后注意equal
这个字段是一个函数类型。
那么type Integer int
这种基于某个类型新建类型,两个类型为何能快捷的强制转换就好理解了:新建类型Integer
有自己的runtime._type
结构,然后另外一个字段通过指针指向基于int
的runtime._type
类型元数据(因为int是内置类型,具体的元数据表达在源码中我们就看不到了)。
runtime._type
这个结构体还有一个不可导出的方法func (t *_type) uncommon() *uncommontype
返回的是一个*uncommontype
类型结构体,结构如下:
type uncommontype struct {
pkgpath nameOff // 包路径
mcount uint16 // number of methods
xcount uint16 // number of exported methods
moff uint32 // offset from this uncommontype to [mcount]method
_ uint32 // unused
}
uncommontype类型记录的是自定义类型的包路径、自定义方法的数量、可导出方法数量以及相对于方法元数据的偏移量。这就意味着自定义类型的方法也有所谓元数据的概念,方法元数据表达结构如下:
type nameOff int32
type typeOff int32
type textOff int32
type method struct {
name nameOff
mtyp typeOff
ifn textOff
tfn textOff
}
uncommontype通过偏移量字段与方法元数据进行关联,这样类型的自定义方法就与类型有了联系。
接口类型
接口类型元数据
接口类型也有元数据表达结构runtime.interfacetype
:
type imethod struct {
name nameOff
ityp typeOff
}
type interfacetype struct {
typ _type
pkgpath name
mhdr []imethod
}
runtime.interfacetype
结构中typ
字段就是一个runtime._type
,pkgpath
字段则指向了所在包的包路径,mhdr
则是接口声明的方法列表,使用一个imethod
结构体表达。
接口类型分为空接口和非空接口,对应运行时表达结构如下:
// 非空接口运行时表达
type iface struct {
tab *itab
data unsafe.Pointer
}
// 空接口运行时表达 e --- empty
type eface struct {
_type *_type
data unsafe.Pointer
}
空接口
空接口就是没有任何方法集的接口。因为没有任何方法,go语言汇总任何类型都实现了空接口(duck语法),就意味着空接口变量可以用来接收任何类型的数据,从而可以实现看起来类型的动态化。
很明显空接口里一个字段指向了类型元数据的表达即runtime._type
这个结构体变量的指针;另外一个字段则指向了动态类型变量的值。
runtime._type
这个结构体关联到了可找到定义的类型方法的runtime.uncommontype
结构,所以赋值后的空接口并没有丢失类型和类型方法信息。
非空接口
非空接口即有方法申明的接口类型,与空接口一样data字段指向动态类型的值,iface
再通过一个包含除了runtime._type
之外还有其他更多信息的tab字段对应的itab
结构表达类型和类型方法,这个结构如下:
type itab struct {
inter *interfacetype
_type *_type
hash uint32 // copy of _type.hash. Used for type switches.
_ [4]byte
fun [1]uintptr // variable sized. fun[0]==0 means _type does not implement inter.
}
Conversions、Type switch和Interface conversions and type assertions
1、类型分支选择
go语言提供的获取动态类型接口变量的实际类型的语法,以下是官方样例代码:
var t interface{}
t = functionOfSomeType()
switch t := t.(type) { // 这是核心语法,返回的是类型然后对类型做switch判断
default:
fmt.Printf("unexpected type %T\n", t) // %T prints whatever type t has
case bool:
fmt.Printf("boolean %t\n", t) // t has type bool
case int:
fmt.Printf("integer %d\n", t) // t has type int
case *bool:
fmt.Printf("pointer to boolean %t\n", *t) // t has type *bool
case *int:
fmt.Printf("pointer to integer %d\n", *t) // t has type *int
}
核心写法value.(type)
,value是动态类型接口变量,后面点、括号和type关键字都是固定写法。
2、接口类型转换和断言
go语言对interface{}
接口类型提供了断言和转换语法:
value.(typeName)
value是动态类型的接口变量,后面点和括号是固定写法,typeName这是需要断言的具体类型或非空接口类型的类型名称。返回值有两个,第一个是转换后的具体类型的变量后一个是布尔值,如果转换成功返回true转换失败返回false;然后需要注意的是如果转换失败第一个返回值是指定断言的类型的零值;官方提供的样例代码如下:
var value interface{}
str, ok := value.(string) // 接口类型转换和断言写法
if ok {
fmt.Printf("string value is: %q\n", str)
} else {
fmt.Printf("value is not a string\n")
}
当然go语法特性上述代码是可以简写的,不再赘述。
3、类型显式转换
类型转换用于将一种数据类型的变量转换为另外一种类型的变量。Go 语言类型转换基本格式如下:
type_name(expression)
// type_name为类型的名字,expression为表达式最终是一个需要转的值字面量
go语言是不支持隐式转换的,如下第四行没有通过显式转换表达式进行转换,编译期间就会报错。
func main() {
var a int64 = 888
var b int32
b = a
fmt.Printf("b 为 : %d", b)
}
第四行代码改成显式转换表达式:b = int32(a)
就可以编译通过了。
类型系统面试题一则
最后来一段面试题代码:这段代码能通过编译么?如果能通过输出是什么?请讲解原因。
package main
type funcType func(p1 int, err error) error
func (f funcType) Save() {
println("execute")
}
type funcTypeAlias = funcType
func main() {
// ①
var fVal = func(p1 int, err error) error {
return nil
}
// ②
if aa, ok := interface{}(fVal).(funcType); ok {
aa.Save()
} else {
println("not execute")
}
// ③
funcType(fVal).Save()
// ④
var fVal1 funcType = func(p2 int, err error) error {
return nil
}
if aa, ok := interface{}(fVal1).(funcType); ok {
aa.Save()
} else {
println("not execute")
}
// ⑤
var fVal2 funcTypeAlias = func(p3 int, err error) error {
return nil
}
fVal2.Save()
}
答案是:能编译通过输出,输出的内容是:
-
not execute
-
execute
-
execute
-
execute
-
自定义函数类型也是可以定义方法的;
-
①位置定义的是一个匿名类型的变量,与funcType这个具名类型是两种类型,虽然类型描述是一致的,所以②位置类型断言失败;
-
③就是类型显式转换,因为具名类型和匿名类型都是基于
func(p1 int, err error) error
,而且函数类型的方法签名中参数名称是不影响函数类型的,只有参数的类型和顺序才影响,能转换成功; -
④定义的变量显式指定了这个变量的类型,指定了类型就是这种类型,然后转换为空接口而空接口通过类型元数据字段记录原始类型的元数据,再去断言必定是成功的;
-
⑤定义的是类型别名的变量,类型别名就是类型本身;
blog.jjonline.cn/golang/255.html
- EOF -
1、
2、
3、
Go 开发大全
参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。
关注后获取
回复 Go 获取6万star的Go资源库
分享、点赞和在看
支持我们分享更多好文章,谢谢!