类型
15 分钟阅读
Types 类型
类型决定了一组值以及特定于这些值的操作和方法。如果类型具有类型名称,则可以用类型名称表示。如果类型是泛型,则后面必须跟类型参数。还可以使用类型字面量
指定类型,它由现有类型组成一个类型。
|
|
该语言预先声明了某些类型的名称。其他类型是通过类型声明或类型参数列表引入的。复合类型
:数组、结构体、指针、函数、接口、切片、映射和通道类型 —— 可以用类型字面量来构造。
预先声明的类型、已定义的类型和类型参数被称为命名类型
。如果别名声明中给出的类型是命名类型,则别名也表示一个(新的)命名类型。
Boolean types 布尔型
布尔型表示由预先声明的常量true
和false
表示的一组布尔真值。预先声明的布尔类型是bool
;它是一个已定义的类型。
Numeric types 数值型
整数类型、浮点类型或复数类型分别表示整数、浮点或复数的值的集合。它们被统称为数值类型
。预先声明的与体系结构无关的数值类型有:
|
|
The value of an n-bit integer is n bits wide and represented using two’s complement arithmetic. =>仍有疑问??
一个n位整数的值是n位宽,并用二进制补码运算法(two’s complement arithmetic)表示。
还有一组预先声明的整数类型,它们具有特定于实现的大小:
|
|
为了避免可移植性问题,所有的数值类型都是已定义的类型,因此除了 byte
(uint8
的别名)和 rune
(int32
的别名)之外,它们是截然不同的。 当不同的数值类型在表达式或赋值中混合使用时,需要进行显式转换。例如,int32和int不是相同类型,尽管它们在一个特定的体系结构上可能具有相同的大小。
String types 字符串型
字符串类型表示字符串值的集合。字符串值是字节序列(可能为空)。字节数
被称为字符串的长度
,并且永远不会是负数。字符串是不可变的:一旦创建,就不可能改变字符串的内容。预先声明的字符串类型是string
;它是一种已定义的类型。
可以使用内置函数 len
查找字符串 s
的长度。如果字符串是常量,那么长度就是编译时常量。字符串的字节可以通过整数索引0到len(s)-1
来访问。取这样一个元素的地址是非法的
;如果s[i]
是字符串的第i
个字节,那么&s[i]
是无效的。
Array types 数组型
数组是单类型的元素组成的编号序列,称为元素类型。元素的数量
被称为数组的长度
,并且永远不会是负数。
ArrayType = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .
长度是数组类型的一部分;它必须求值为一个非负常数,该常数可由 int 类型的值表示。数组a
的长度可以用内置函数len
发现。元素可以通过整数索引0到len(a)-1
进行寻址。数组类型总是一维的,但是可以组成多维类型。
|
|
Slice types 切片型
切片是底层数组的连续段的描述符,并提供对该数组中编号的元素序列的访问。切片类型表示其元素类型的所有数组切片的集合。元素的数量
被称为切片的长度
,并且永远不会是负数。一个未初始化的切片的值是nil
。
SliceType = "[" "]" ElementType .
切片s
的长度可以通过内置函数len
发现;与数组不同,它在运行过程中可能会发生变化。元素可以通过整数索引0到len(s)-1
进行寻址。给定元素的切片索引可能小于底层数组中同一元素的索引。
切片一旦被初始化,总是与保存其元素的底层数组相关联。因此,一个切片与它的底层数组和同一数组的其他切片共享存储;相反,不同的数组总是表示不同的存储。
切片的底层数组可以超过切片的末端。容量是对这一范围的衡量:它是切片的长度和切片之外的数组长度之和;可以通过从原始切片切割一个新的切片来创建一个达到这个容量的切片。使用内置函数 cap(a)
可以发现切片 a
的容量。
可以使用内置函数make
来创建一个给定元素类型T
的新的、初始化的切片值,该函数接受一个切片类型和指定长度和可选容量的参数。用make
创建的切片总是分配一个新的、隐藏的数组,返回的切片值指向该数组。也就是说,执行
|
|
产生的切片与分配一个数组并对其进行切片是一样的,所以这两个表达式是等同的:
|
|
和数组一样,切片总是一维的,但可以通过组合来构造更高维的对象。对于数组的数组,内部数组在结构上总是相同的长度;但是对于切片的切片(或切片的数组),内部长度可以动态变化。此外,内部切片必须被单独初始化
。
Struct types 结构体型
结构体是一个命名元素(称为字段
)的序列,,每个字段都有一个名称和一个类型。字段名可以显示地指定(IdentifierList)或隐含地指定(EmbeddedField)。在一个结构体中,非空白字段名必须是唯一的。
StructType = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName [ TypeArgs ] .
Tag = string_lit .
// An empty struct.
struct {}
// A struct with 6 fields.
struct {
x, y int
u float32
_ float32 // padding
A *[]int
F func()
}
一个声明了类型但没有明确字段名的字段被称为嵌入式字段
。嵌入字段必须被指定为一个类型名T
或一个指向非接口类型名*T
的指针,而且T
本身不能是一个指针类型。未限定类型名作为字段名。
|
|
下面的声明是非法的,因为字段名在一个结构体类型中必须是唯一的
。
|
|
如果x.f
是表示字段或方法f
的合法选择器,那么结构体x
中的嵌入式字段或方法f
被称为(自动)提升(的字段或方法)。
被提升的字段与结构体中的普通字段一样,只是它们不能在结构体的复合字面量中作为字段名使用。
给定一个结构体类型S
和一个命名类型T
,提升的方法被包含在结构体的方法集中,如下所示:
- 如果
S
包含一个嵌入式字段T
,那么S
和*S
的方法集都包括带有接收器T
的提升方法,*S
的方法集也包括带有接收器*T
的提升方法。 - 如果
S
包含一个嵌入式字段*T
,那么S
和*S
的方法集都包括带有接收器T
或*T
的提升方法。
一个字段声明后面可以有一个可选的字符串字面量标签
,它成为相应字段声明中所有字段的属性。一个空的标签字符串等同于一个不存在标签。标签通过反射接口可见,并参与结构体的类型标识,但在其他情况下被忽略。
|
|
Pointer types 指针型
指针类型表示指向给定类型(称为指针的基本类型)变量的所有指针的集合。一个未初始化的指针的值是nil
。
|
|
Function types 函数型
函数类型表示具有相同参数类型和结果类型的所有函数的集合。一个函数类型的未初始化变量的值是nil
。
|
|
在参数或结果的列表中,名称(IdentifierList)必须全部存在或全部不存在。如果存在(名称),每个名称代表指定类型的一个项(参数或结果),并且签名中所有非空白的名称必须是唯一的。如果不存在(名称),每个类型代表该类型的一个项。参数和结果列表总是用括号表示,但如果正好仅有一个未命名的结果,则可以写成未括号的类型。
在函数签名中的最后一个传入参数可以有一个类型前缀
...
。有这样一个参数的函数被称为 variadic (可变参数函数
),可以用零个或多个参数来调用该函数。
|
|
Interface types 接口型
接口类型定义了一个类型集。接口类型的变量可以存储该接口类型集中的任何类型的值。这样的类型被称为实现了该接口。未初始化的接口类型变量的值是nil
。
InterfaceType = "interface" "{" { InterfaceElem ";" } "}" .
InterfaceElem = MethodElem | TypeElem .
MethodElem = MethodName Signature .
MethodName = identifier .
TypeElem = TypeTerm { "|" TypeTerm } .
TypeTerm = Type | UnderlyingType .
UnderlyingType = "~" Type .
接口类型由接口元素列表指定。接口元素是一个方法或一个类型元素,其中类型元素是一个或多个类型术语的联合。类型术语可以是一个单一类型,也可以是一个单一的底层类型。
Basic interfaces 基本接口
在其最基本的形式中,接口指定了一个(可能是空的)方法列表。由这样一个接口定义的类型集是实现所有这些方法的类型集,而相应的方法集则完全由这个接口指定的方法组成。那些类型集可以完全由一个方法列表
来定义的接口被称为基本接口
。
|
|
|
|
多个类型可以实现一个(相同的)接口。例如,如果两个类型S1
和S2
的方法设置为
|
|
(其中T
代表S1
或S2
),那么File
接口就由S1
和S2
实现,而不管S1
和S2
可能有其他方法或共享什么其他方法。
作为接口类型集成员的每个类型都实现了该接口。任何给定的类型都可以实现几个不同的接口。例如,所有类型都实现空接口
(interface {}),它代表所有(非接口)类型的集合:
|
|
为了方便,预先声明的类型any
是空接口的别名
。
类似地,考虑这个接口规范,它出现在定义名为 Locker
的接口的类型声明中:
|
|
如果S1
和S2
也实现了
|
|
他们就实现了Locker
接口和File
接口。
Embedded interfaces 嵌入式接口
接口T
可以使用(可能是限定的)接口类型名称E
作为接口元素。这就是在 T
中嵌入接口 E
。T
的类型集是由T
的显式声明方法定义的类型集和T
的嵌入接口的类型集的交集
。换句话说,T
的类型集是实现T
的所有显式声明的方法以及E
的所有方法的所有类型的集合。
|
|
|
|
General interfaces 通用接口
在最通用的形式下,接口元素也可以是一个任意类型的术语T
,或者是一个指定底层类型T
的~T
形式的术语,或者是术语t1|t2|...|tn
的联合。与方法规范一起,这些元素能够精确地定义一个接口的类型集,如下所示:
- 空接口的类型集是
所有非接口类型的集合
。 - 非空接口的类型集是其接口元素的类型集的交集。
- 方法规范的类型集是其方法集包括该方法的
所有非接口类型的集合
。 - 非接口类型术语的类型集是仅由该类型组成的集合。
- 形式为
~T
的术语的类型集是其底层类型为T
的所有类型的集合。 - 术语
t1|t2|...|tn
的联合体类型集是各术语类型集的联合。
量化 “所有非接口类型的集合
“不仅指手头程序中声明的所有(非接口)类型,还指所有可能程序中的所有可能类型,因此是无限的。类似地,给定实现某个特定方法的所有非接口类型的集合
,这些类型的方法集的交集
将正好包含该方法,即使手头的程序中的所有类型总是将该方法与另一个方法配对。
通过构造,一个接口的类型集永远不会包含一个接口类型
。
|
|
在形式为~T
的术语中,T
的底层类型必须是它自己,而且T
不能是一个接口。
|
|
联合元素表示类型集的联合:
|
|
形式为T
或~T
的术语中的类型T
不能是类型参数,所有非接口术语的类型集必须是成对不相交的(类型集的成对交集必须为空)。给定一个类型参数P:
|
|
实现限制:联合(有多个术语)不能包含预先声明的标识符comparable
或指定方法的接口,或嵌入comparable
或指定方法的接口。
非基本接口只能作为类型约束使用,或者作为其他接口的元素作为约束使用。它们不能作为值或变量的类型,也不能作为其他非接口类型的组成部分。
|
|
接口类型 T
不能嵌入任何递归地包含或嵌入 T
的类型元素。
|
|
Implementing an interface 实现一个接口
如果类型T
实现了接口I
,则
T
不是接口,并且是I
类型集的元素;或者T
是接口,并且T
的类型集是I
的类型集的子集。
如果T
实现了一个接口,那么T
类型的值就实现了该接口。
Map types 映射型
映射是一个无序的元素组,由一种类型的元素(称为元素类型
)组成,由另一种类型的唯一键集(称为键类型
)进行索引。一个未初始化的映射的值是nil
。
MapType = "map" "[" KeyType "]" ElementType .
KeyType = Type .
比较运算符==
和!=
必须为键类型的操作数完全定义;因此键类型不能是函数、映射或切片
。如果键类型是接口类型,则必须为动态键值定义这些比较运算符;失败将导致运行时恐慌(run-time panic)。
|
|
映射元素的数量被称为它的长度
。对于一个map m
来说,它可以用内置函数len
来发现,并且在运行过程中可能会改变。在运行过程中可以用赋值添加元素,用索引表达式检索元素;可以用内置函数delete
删除元素。
使用内置函数 make
创建一个新的空 map 值,它使用 map 类型和一个可选的容量提示作为参数:
|
|
初始容量不限制其大小:映射会增长以容纳其中存储的项数,但nil
映射除外。nil
映射等同于空映射,只是不能添加任何元素
。
Channel types 通道型
通道为并发执行函数提供了一种机制,通过发送和接收指定元素类型的值进行通信。未初始化的通道的值是nil
。
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .
可选的<-
操作符指定了通道的方向:发送或接收。如果指定了方向,则该通道是定向的,否则是双向的。通过赋值或显式转换,通道可以被限制为仅发送或仅接收。
|
|
<-
操作符尽可能与最左边的 chan
相关联:
|
|
可以使用内置函数 make
创建一个新的、初始化的 channel 值,它以channel 类型和可选的容量作为参数:
|
|
容量(以元素数量为单位)设置通道中缓冲区的大小。如果容量为零或没有指定,则通道是无缓冲的,只有当发送方和接收方都准备好时,通信才会成功。否则,如果缓冲区不满(可继续发送)或不是空的(可继续接收) ,通道会将数据缓冲起来,并且通信在没有阻塞的情况下成功。一个nil
通道不能用于通信。
引用其他书籍
以下摘自《Go语言精进之路》第34条 了解channel的妙用 第348页。 与无缓冲channel 不同,带缓冲channel 可以通过带有 capacity 参数的内置make 函数创建:c:= make(chan T, capctity) 由于带缓冲channel 的运行时层实现带有缓冲区,因此对带有缓冲channel的发送操作在缓冲区未满、接收操作在缓冲区非空的情况下是异步的(发送或接收无需阻塞等待)。也就是说,对一个带缓冲channel,在缓冲区无数据或有数据但未满的情况下,对其进行发送操作的goroutine不会阻塞;在缓冲区已满的情况下,对其进行发送操作的goroutine会阻塞;在缓冲区为空的情况下,对其进行接收操作的goroutine亦会阻塞。
通道可以用内置函数close
来关闭。接收操作符的多值赋值形式可以用来判断数据是否在通道关闭之前发送出去。
任意数量的goroutines都可以通过发送语句、接收操作以及对内置函数cap
和len
的调用,来操作一个通道。通道是一个先入先出的队列。例如,如果一个goroutine在通道上发送数据,第二个goroutine接收这些数据,那么这些数据将按照发送的顺序被接收。