[Journey with golang] 3. Type system

cmsmdn 2020-01-10

golang的类型分为命名类型和未命名类型。命名类型包含预声明类型,可以通过标识符表示,用户自定义类型也是命名类型。未命名类型由预声明类型、关键字和操作符组合而成。未命名类型又称为类型字面量。array/slice/map/channel/pointer/function/struct/interface都属于类型字面量。其中,struct和interface是不使用type定义的那种。

未命名类型和类型字面量是等价的,通常所述的golang基本类型中的复合类型就是类型字面量,所以未命名类型、类型字面量和golang基本类型中的复合类型三者等价。

所有“类型”都有一个底层类型,底层类型规则为:

  • 预声明类型和类型字面量的底层类型是它们自身
  • 自定义类型中的底层类型是逐层递归向下查找的,直到查找的oldtype是预声明类型或类型字面量为止。

编译器在编译时会进行严格的类型校验。两个命名类型是否相同,参考如下:

  1. 两个命名类型相同的条件是两个类型声明的语句完全相同
  2. 命名类型和未命名类型永远不相同
  3. 两个未命名类型相同的条件是它们的类型声明字面量结构相同,而且内部元素类型相同
  4. 通过类型别名语句声明的两个类型相同

不同类型的变量之间一般不能直接相互赋值,除非满足一定的条件。类型为T1的变量a可以赋值给类型为T2的变量b,称为类型T1可以复制给类型T2。变量a可以赋值给变量b的条件为(至少满足一个):

  1. T1和T2的类型相同
  2. T1和T2具有相同的底层类型,并且T1和T2里面至少有一个是未命名类型
  3. T2是接口类型,T1是具体类型,T1的方法集是T2方法集的超集
  4. T1和T2都是通道类型,拥有相同的元素类型,并且T1和T2中至少有一个是未命名类型
  5. a是预声明标识符nil,T2是pointer/function/slice/map/channel/interface类型中的一个
  6. a是一个字面常量值,可以用来表示类型T的值

任意两个不相干的类型如果进行强制类型转换,则必须符合一定的规则。非常量类型的变量x可以强制转化并传递给类型T,需要满足如下任一条件:

  1. x可以直接赋值给T类型变量
  2. x的类型和T具有相同的底层类型
  3. x的类型和T都是未命名的指针类型,并且指针指向的类型具有相同的底层类型
  4. x的类型和T都是整形,或者都是浮点型
  5. x的类型和T都是复数类型
  6. x是整数值或[]byte类型的值,T是string类型
  7. x是一个字符串,T是[]byte/[]rune

需要注意的是,数值类型和string类型之间的相互转换可能造成部分值丢失,需要使用标准库strconv;其他类型的转换不会造成值的改变。golang没有语言机制支持指针和integer之间的转换,需要使用标准库的中的unsafe包处理。

用户自定义类型使用关键字type,语法格式为type newtype oldtype,oldtype可以是自定义类型、预声明类型、未命名类型中的任意一种。newtype是新类型的标识符,与oldtype具有相同的底层类型,并且都继承了底层类型的操作(不是方法)集合。

如果使用字面量表示struct,则该struct为未命名类型;若使用type语句声明,则这个新类型就是命名类型。

结构的字段可以是任意类型,基本类型、接口类型、指针类型、函数类型都可以作为struct的字段。结构字段的类型名必须唯一,struct字段类型可以是普通类型,也可以是指针。struct支持内嵌自身的指针。

在定义struct的过程中,如果字段只给出了字段类型,没有给出字段名,则称这样的字段为“匿名字段”。被匿名嵌入的字段必须是命名类型或其指针,类型字面量不能作为匿名字段使用。匿名字段的字段名默认就是类型名,如果匿名字段是指针类型,则默认的字段名就是指针指向的类型名。但一个结构体内不能同时存在某一类型及其指针类型的匿名字段,原因是二者字段名相等。如果嵌入到 字段来自其他包,则需要加上包名,而且必须是其他包可导出的类型。

golang的类型方法是一种对类型行为的封装,可以看做是特殊类型的函数,其显式地将对象实例或指针作为函数的第一个参数,而且参数名可以自己指定,这个对象实例或指针称为方法的接收者。golang的类型方法本质上就是一个函数,没有使用隐式的指针。可以把类型方法改写为常规函数。

类型方法有如下特点:

  1. 可以为命名类型增加方法(除了接口),非命名类型不能自定义方法
  2. 为类型增加方法有一个限制,就是方法的定义必须和类型的定义在同一个包中
  3. 方法的命名空间的可见性和变量一样,大写字母开头的方法可以再包外被访问,否则只能在包内可见
  4. 使用type定义的自定义类型是一个新类型,新类型不能调用原有类型的方法,但是底层类型支持的运算可以被新类型继承

变量x的静态类型是T,M是类型T的一个方法,x.M被称为方法值。x.M是一个函数类型变量,可以赋值给其他变量,并像普通的函数名一样使用。方法值其实是一个带有闭包的函数变量,其底层实现原理和带有闭包的匿名函数类似,接收值被隐式地绑定到方法值的闭包环境中,后续调用不需要再显式地传递接收者。方法表达式相当于提供一种语法将类型方法调用显式地转换为函数调用,接收者必须显式地传递进去。golang的方法底层是基于函数实现的,只是语法格式不同,本质一样。

命名类型方法接收者有两种类型,一种是值类型,一种是指针类型。无论接收者是什么类型,方法和函数的实参传递都是值拷贝。如果接收者是值类型,则传递的是值的副本;如果接收者是指针类型,则传递的是指针的副本。

将接收者为值类型T的方法的集合记为S,将接收者为指针类型*T的方法的集合记为*S。类型的方法集总结如下:

  1. T类型的方法集为S
  2. *T类型的方法集为S和*S

在直接使用类型实例调用类型的方法时,无论值类型变量还是指针类型变量,都可以调用类型的所有方法,原因是编译器在编译期间能够识别出这种调用关系,做了自动的转换。如果通过类型字面量显式地进行值调用和表达时调用,编译器在这种情况下不会自动转换,会进行严格的方法集审查;通过类型变量进行值调用和表达式调用,在这种情况下,使用值调用方式调用时编译器会进行自动转换,使用表达式调用方式时编译器不会进行转换,会进行严格的方法集审查。

golang的struct与c语言的struct一样,内存分配按照字段顺序依次开辟连续的存储空间,没有插入额外的东西(除字段对齐外),不像cpp那样为了实现多态在对象内存模型里插入了虚函数指针。

命名结构类型可以嵌套其他命名类型的字段,外层的结构类型可以调用嵌入字段类型的方法。golang没有继承的概念,只有包含(或者更官方些,组合)关系。struct类型中的字段被称为“内嵌字段”。如果外层字段和内层字段有相同的方法,则使用简化模式访问外层的方法会覆盖内层的方法。组合结构的方法集有如下规则:

  1. 若类型S包含匿名字段T,则S的方法集包含T的方法集
  2. 若类型S包含匿名字段*T,则S的方法集包含T和*T的方法集
  3. 若类型S嵌入T或*T,*S的方法集必定包含T和*T的方法集

golang函数的调用实参都是值拷贝,方法调用参数传递也是一样的机制,具体类型变量传递给接口时也是值拷贝。如果传递给接口变量的是值类型,但滴啊用方法的接收者是指针类型,则程序运行时虽然能够将接收者转换为指针,但这个指针是副本的指针,并不是我们期望的原变量的指针;如果传递给接口的变量是指针类型,则接口调用的是值类型的方法,程序会自动转换为值类型,且不会带来副作用。

使用func FunctionName()语法格式定义的函数称为有名函数,定义时使用func()语法格式的函数称为匿名函数。函数类型也分两种,一种是函数字面量类型,一种是函数命名类型。有名函数和匿名函数的类型都属于函数字面量类型。有名函数的定义相当于初始化一个函数字面量类型后将其赋值给一个函数名变量;匿名函数的定义是直接初始化一个函数字面量类型,只是没有绑定到一个具体变量上。从golang类型系统角度来看,有名函数和匿名函数都是函数字面量类型的实例。

函数签名就是有名函数或匿名函数的字面量类型,函数签名没有函数名。通常说的函数类型指有名函数类型,函数签名指函数的字面量类型。很多地方把函数类型和函数签名等价使用,这是不严谨的。

golang没有函数声明的语义,准确来说,golang调用函数不需要声明,但golang调用汇编语言编写的函数还是要使用函数声明语句。

相关推荐