vlambda博客
学习文章列表

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. 类型的别名就是类型本身;
  2. 内置类型的别名是无法自定义方法的:因为内置类型无法自定义方法;
  3. 跨包类型的别名是无法自定义方法的:言外之意就是在同一个包内给类型定义别名是可以在别名上定义方法的,本质上就是在对被取别名的类型定义方法;

别名机制的写法是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的自定义类型、typeOffnameOff是基于int32的自定义类型(题外话:这两个字段中的Off并不是关闭的意思而是offset即偏移量的中二简写形式),然后注意equal这个字段是一个函数类型。

那么type Integer int这种基于某个类型新建类型,两个类型为何能快捷的强制转换就好理解了:新建类型Integer有自己的runtime._type结构,然后另外一个字段通过指针指向基于intruntime._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._typepkgpath字段则指向了所在包的包路径,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()
}

答案是:能编译通过输出,输出的内容是:

  1. not execute

  2. execute

  3. execute

  4. execute

  5. 自定义函数类型也是可以定义方法的;

  6. ①位置定义的是一个匿名类型的变量,与funcType这个具名类型是两种类型,虽然类型描述是一致的,所以②位置类型断言失败;

  7. ③就是类型显式转换,因为具名类型和匿名类型都是基于func(p1 int, err error) error,而且函数类型的方法签名中参数名称是不影响函数类型的,只有参数的类型和顺序才影响,能转换成功;

  8. ④定义的变量显式指定了这个变量的类型,指定了类型就是这种类型,然后转换为空接口而空接口通过类型元数据字段记录原始类型的元数据,再去断言必定是成功的;

  9. ⑤定义的是类型别名的变量,类型别名就是类型本身;

blog.jjonline.cn/golang/255.html

 - EOF -

推荐阅读(点击标题可打开)

1、

2、

3、


Go 开发大全

参与维护一个非常全面的Go开源技术资源库。日常分享 Go, 云原生、k8s、Docker和微服务方面的技术文章和行业动态。

关注后获取

回复 Go 获取6万star的Go资源库



分享、点赞和在看

支持我们分享更多好文章,谢谢!