数据类型
本文档详细介绍Go语言的类型系统,包括基本类型、复合类型、类型声明和类型转换等核心概念。
📋 目录
类型系统概述
类型的定义
在Go语言中,类型是一个值的集合以及定义在这些值上的操作和方法的集合。类型决定了:
- 值的存储方式
- 可以进行的操作
- 内存占用大小
- 值的表示范围
类型的表示方式
Go语言中类型可以通过以下方式表示:
Type = TypeName | TypeLit | "(" Type ")" .
TypeName = identifier | QualifiedIdent .
TypeLit = ArrayType | StructType | PointerType | FunctionType | InterfaceType |
SliceType | MapType | ChannelType .类型表示示例:
// 类型名
int, string, bool
// 类型字面值
[]int // 切片类型
map[string]int // 映射类型
chan int // 通道类型
*int // 指针类型
func(int) string // 函数类型底层类型
每个类型都有一个底层类型:
- 如果是预定义类型或类型字面值,底层类型就是自身
- 如果是通过类型声明定义的,底层类型是声明时引用的类型
// 类型别名
type (
A1 = string // A1是string的别名
A2 = A1 // A2也是string的别名
)
// 新类型定义
type (
B1 string // B1是基于string的新类型
B2 B1 // B2是基于B1的新类型
B3 []B1 // B3是B1切片类型
B4 B3 // B4是基于B3的新类型
)
// 底层类型分析:
// string, A1, A2, B1, B2 的底层类型都是 string
// []B1, B3, B4 的底层类型都是 []B1类型分类
Go语言的类型可以分为两大类:
| 类别 | 类型 | 特点 |
|---|---|---|
| 基本类型 | bool, 数值类型, string | 预定义,不可分解 |
| 复合类型 | array, struct, pointer, function, interface, slice, map, channel | 由基本类型组合而成 |
方法集
类型可能会有一个与之关联的方法集。接口类型的方法集就可以使用自身表示。对于其他类型,类型 T 的方法集由所有接收者类型为 T 的方法组成。而对应指针类型 *T 的方法集由所有接收者类型为 T 或 *T 的方法组成。如果是结构体类型且含有嵌入字段,那么方法集中可能还会包含更多的方法,具体请看结构体类型章节。其他类型的方法集都为空。方法集中的每个方法都有唯一且不为空的方法名。
类型的方法集用来确定类型实现的接口和以类型作为接收者能够调用的方法。
布尔类型
布尔类型表示预定义常量 true 和 false 表示布尔真实值的集合。预定义的布尔类型为 bool;它是通过类型声明创建的。
数字类型
一个数字类型相当于整型和浮点型的所有值的集合。预定义的数字类型包括:
uint8 8 位无符号整数集合 (0 to 255)
uint16 16 位无符号整数集合 (0 to 65535)
uint32 32 位无符号整数集合 (0 to 4294967295)
uint64 64 位无符号整数集合 (0 to 18446744073709551615)
int8 8 位有符号整数集合 (-128 to 127)
int16 16 位有符号整数集合 (-32768 to 32767)
int32 32 位有符号整数集合 (-2147483648 to 2147483647)
int64 64 位有符号整数集合 (-9223372036854775808 to 9223372036854775807)
float32 IEEE-754 32 位浮点数集合
float64 IEEE-754 64 位浮点数集合
complex64 实部虚部都为 float32 的复数集合
complex128 实部虚部都为 float64 的复数集合
byte uint8 的别名
rune int32 的别名n 位整数的值具有 n 比特的宽度并用补码表示。
以下几种预定义类型由具体平台实现指定长度:
uint 32 或 64 位
int 和 uint 位数相同
uintptr 能够容纳指针值的无符号整数为了避免移植性问题,除了被 uint8 的别名 byte 和 int32 的别名 rune,其他所有的数字类型都是通过类型声明定义。当在表达式中使用不同的数字类型需要进行类型转换。例如:int32 和 int 不是相同的类型,即使他们在指定的平台上是相等的。
字符串类型
字符串类型表示字符串的值类型。字符串的值是一个字节序列(有可能为空)。字符串一旦创建就无法修改它的值。预定义的字符串类型是 string,它是通过类型声明定义的。
可以使用内置函数 len 获取字符串长度。如果字符串是常量那么它的长度在编译时也为常量。可以通过数字下标 0~len(s)-1 访问字符串字节。获取字符串的地址是非法操作;如果 s[i] 是字符串的第 i 个字节,那么 &s[i] 是无效的。
数组类型
数组是一定数量的单一类型元素序列,而这个单一类型叫做元素类型。元素的个数表示元素的长度,它永远不是负数。
ArrayType = "[" ArrayLength "]" ElementType .
ArrayLength = Expression .
ElementType = Type .长度是数组类型的一部分;它是一个类型为 int 的非负常量。可以用内置函数 len 获取数组的长度。元素可以通过下标 0~len(a)-1 访问。数组一般都是一维的,不过也可以是多维的。
[32]byte
[2*N] struct { x, y int32 }
[1000]*float64
[3][5]int
[2][2][2]float64 // same as [2]([2]([2]float64))切片类型
切片描述了底层数组的一个连续片段并提供对连续片段内元素的访问。切片类型表示元素类型的数组的所有切片的集合。没有被初始化的切片用 nil 表示。
SliceType = "[" "]" ElementType .与数组一样,切片的可以使用索引访问并且有长度,切片的长度可以通过内置的 len 函数获取;与数组不同的是它的长度在运行时是可以变化的。我们可以通过下标 0~len(s)-1 来访问切片内的元素。切片的索引可能会小于相同元素再底层数组的索引。
切片一旦初始化,那么就有一个与之对应的底层数组保存切片中的元素。切片和底层的数组还有其他指向该数组的切片共享相同的储存空间;而不同的数组总是有着不同的存储空间。
切片的底层数组可能会延伸到切片末尾以外,切片的容积等于切片现在的长度加上数组中切片还没使用的长度;可以从原始切片中切出一个长度与容量相等的切片。切片的容量可以通过内置的 cap(a) 函数来获取。可以通过函数make来创建一个T类型的新切片。
使用内置函数 make 可以出实话给定元素类型 T 的切片。make 函数接收三个参数:切片类型、切片长度、切片容积,其中切片容积是可选参数。make 创建的切片会在底层分配一个切片所引用的新数组。
make([]T, length, capacity)make 的作用就是创建新数组并切分它,所以下面两种写法是等价的:
make([]int, 50, 100)
new([100]int)[0:50]与数组相同,切片一般是一维的,不过也可以复合成多维。数组中的数组都必须是相同的长度,但是切片中的切片长度是动态变化的,不过切片中的切片需要单独初始化。
结构体类型
结构体是一个命名元素序列,命名元素也叫做字段,每个字段都对应一个名称和类型,字段的名字可以是显式指定的(标识符列表)也可以是隐式的(嵌入字段)。在结构体中非空字段具有唯一性。
StructType = "struct" "{" { FieldDecl ";" } "}" .
FieldDecl = (IdentifierList Type | EmbeddedField) [ Tag ] .
EmbeddedField = [ "*" ] TypeName .
Tag = string_lit .// 空结构体.
struct {}
// 6个字段的结构体.
struct {
x, y int
u float32
_ float32 // padding
A *[]int
F func()
}一个指定了类型而没有指定名称的字段叫做嵌入字段,嵌入字段必须指定类型名 T 或指向非接口类型的指针类型 *T,其中 T 不能为指针类型。或者一个非接口类型的指针。并且T本身不能为指针类型。这种情况下会把类型名作为字段的名字。
// 一个包含 4 个嵌入字段 T1, *T2, P.T3 和 *P.T4 的结构体
struct {
T1 // 字段名为 T1
*T2 // 字段名为 T2
P.T3 // 字段名为 T3
*P.T4 // 字段名为 T4
x, y int // 字段名为 x 和 y
}以下声明是错误的因为字段名称必须唯一。
struct {
T // 嵌入字段 *T 与 *P.T 冲突
*T // 嵌入字段 T 与 *P.T 冲突
*P.T // 嵌入字段 T 与 *T 冲突
}如果 x.f 是表示该字段或方法 f 的合法选择器,则会调用结构 x 中嵌入字段的字段或方法 f。
从嵌入字段组合来的字段与结构体原来的字段行为基本相同,只是不能在结构体的复合字面值中直接使用。
给定一个结构体 S 和一个类型 T,依据以下规则生成组合后的方法集:
- 如果 S 包含嵌入字段 T,则 S 和 *S 的方法集包括接收者为 T 的方法集,而 *S 包括 接收者为 *T 的方法集。
- 如果 S 包含字段 T。那么S和S均包含接收者为 T 和 *T 的所有方法集。
声明字段时可以给该字段添加一个字符串的 tag。这个 tag 将会成为它所对应字段的一个属性。空 tag 和缺省 tag 是相同的。tag 的值可以通过反射的接口获取,可以作为类型结构体的类型定义的一部分,也可以忽略。
struct {
x, y float64 "" // 空 tag 和缺省 tag 相同
name string "any string is permitted as a tag"
_ [4]byte "ceci n'est pas un champ de structure"
}
// 结构体对应一个 TimeStamp 的 protocol buffer.
// tag 字符串中定义了 protocol buffer 字段对应的数字;
// 一般使用 reflect 包读取他们.
struct {
microsec uint64 `protobuf:"1"`
serverIP6 uint64 `protobuf:"2"`
}指针类型
指针类型表示所有指向给定类型变量的指针集合。这个指定的类型叫做指针的基础类型。没有初始化的指针值为nil。
PointerType = "*" BaseType .
BaseType = Type .*Point
*[4]int函数类型
函数类型可以表示所有具有相同参数类型和返回值类型的函数。未初始化的函数类型值为 nil。
FunctionType = "func" Signature .
Signature = Parameters [ Result ] .
Result = Parameters | Type .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
ParameterList = ParameterDecl { "," ParameterDecl } .
ParameterDecl = [ IdentifierList ] [ "..." ] Type .在参数和返回值列表中,标识符列表必须同时存在或缺省。如果存在,那么每个名字都表示指定类型的一个参数/返回值,这些标识符必须非空并且不能重复。如果缺省,指定类型的参数/返回值使用对应的类型表示。参数列表和返回值列表一般都是需要加括号,不过在只有一个缺省返回值时,它可以不使用括号。
函数的最后一个参数可以添加前缀 ...。包含这种参数的函数叫做变参函数,它可以接收零个或多个参数。
func()
func(x int) int
func(a, _ int, z float32) bool
func(a, b int, z float32) (bool)
func(prefix string, values ...int)
func(a, b int, z float64, opt ...interface{}) (success bool)
func(int, int, float64) (float64, *[]int)
func(n int) func(p *T)接口类型
接口类型指定了一个方法集。一个接口类型变量可以保存任何方法集是该接口超集的类型。我们可以认为类型实现了接口。没有初始化的接口类型值为 nil。
InterfaceType = "interface" "{" { MethodSpec ";" } "}" .
MethodSpec = MethodName Signature | InterfaceTypeName .
MethodName = identifier .
InterfaceTypeName = TypeName .在接口类型的方法集中,每个方法的名称必须是非空且唯一。
// A simple File interface
interface {
Read(b Buffer) bool
Write(b Buffer) bool
Close()
}接口可以由多个类型实现,例如:类型 S1 和类型 S2 都有以下方法集:
func (p T) Read(b Buffer) bool { return … }
func (p T) Write(b Buffer) bool { return … }
func (p T) Close() { … }(这里的类型 T 可以表示 S1 也可以表示 S2 ) S1 和 S2 都实现了接口 File,而不用管类型是否还有其他方法。
一个类型实现了任何方法集的为其子集的接口。因此它可能实现了多个不同接口。例如:所有的类型都实现了空接口:
interface{}与之相似,思考下面这个定义为 Locker 的接口:
type Locker interface {
Lock()
Unlock()
}如果 S1 和 S2 也实现了它:
func (p T) Lock() { … }
func (p T) Unlock() { … }那它们就实现了两个接口 Locker 和 File。
一个接口 T 可以使用另一个接口 E 来指定方法。这种方式叫做将接口 E 嵌入进接口 T。它把 E 中所有的方法(包括导出和未导出的方法)全部添加进接口 T。
type ReadWriter interface {
Read(b Buffer) bool
Write(b Buffer) bool
}
type File interface {
ReadWriter // 与添加 ReadWriter 接口中的方法是等价的
Locker // 与添加 Locker 接口中的方法是等价的
Close()
}
type LockedFile interface {
Locker
File // 无效: Lock, Unlock 不是唯一的
Lock() // 无效: Lock 不是唯一的
}接口 T 不能递归的嵌入进自己或已经嵌入过它的接口。
// 无效: Bad 不能嵌入它自己
type Bad interface {
Bad
}
// 无效: Bad1 不能嵌入已经引用它的 Bad2
type Bad1 interface {
Bad2
}
type Bad2 interface {
Bad1
}Map类型
map 类型是一种以唯一值作为键的无序集合。
MapType = "map" "[" KeyType "]" ElementType .
KeyType = Type .map的键类型必须能使用比较运算符 == 和 != 进行比较。因此它的键类型不能是函数,map,或者切片。如果键是接口类型,那么比较运算符必须能比较他的动态值。如果不能会抛出一个运行时错误。
map[string]int
map[*T]struct{ x, y float64 }
map[string]interface{}map中元素的个数叫做它的长度。对于一个map m。它的长度可以通过内置函数 len 获得,而且它的长度可能再运行时发生变化。map 可以再运行时添加和取回元素,页可以使用内置函数 delete移除元素。
可以使用内置函数 make 初始化一个新的且为空的 map。它能指定 map 的类型和预留的空间:
make(map[string]int)
make(map[string]int, 100)map 的预留空间不会固定住 map 的长度;它可以通过添加一定数量的元素来增加自己的长度(nil map 不能添加元素)。nil map 和空 map 是相等的,只是 nil map 不能添加元素。
Channel类型
channel提供一种手段在并发执行的函数间发送和接收指定类型的值。没有初始化的 channel 是nil。
ChannelType = ( "chan" | "chan" "<-" | "<-" "chan" ) ElementType .操作符 <- 可以指定 channel 的数据流动方向。如果没有指定方向,channel 默认是双向的。channel 可以通过转换和赋值来限制只读和只写。
chan T // 可以接收和发送 T 类型的数据
chan<- float64 // 只能发送 float64 类型的值
<-chan int // 只能接收<- 与最左侧的 chan 关联:
chan<- chan int // 等价于 chan<- (chan int)
chan<- <-chan int // 等价于 chan<- (<-chan int)
<-chan <-chan int // 等价于 <-chan (<-chan int)
chan (<-chan int)可以通过内置的 make 函数初始化 channel。make 函数可以指定channel的类型和容量。
make(chan int, 100)容量是设置了最大能缓存元素的数量。如果没有设置容量或值为 0,channel 就是没有缓存的,这时只有当发送者和接收者都准备好后才会传输数据。而带缓存的 channel 在缓存没有满的时候依然可以成功发送数据,当缓存不为空的时候可以成功接收到数据,值为 nil 的 channel 不能传输数据。
可以通过内置函数 close 关闭 channel。在接收端的第二个返回值可以用来提示接收者在关闭的 channel 是否还包含数据。
channel 可以在发送语句,接收操作中使用。可以不考虑同步性直接在多个 goroutine 中对 channel 调用内置函数 len 和 cap 。channel 的行为和 FIFO 队列相同。举个例子,一个 goruntine 发送数据,另一个 goruntine 接收他们,接收数据的顺序和发送数据的顺序是相同的。
类型的属性和值
类型标识
两个类型可能相同也可能不同。
定义的类型都是不同类型。如果两个类型的底层类型在结构上是相同的,那它们也是相等的。总的来说:
2 个数组的长度和元素类型相同,那么它们就是相同类型。
如果两个切片的元素类型相同那么它们就是相同类型。
如果两个结构体字段顺序相同,并且字段名称、字段类型和 tag 都相同那么它们就是相等的。非导出字段的字段名在不同的包中总是不同的。
如果两个指针的基础类型相同那么他们具有相同类型。
如果两个函数具有相同的参数和返回值列表,并且他们的类型相同那么他们就是相同的,参数的名称不一定要相同。
如果两个接口的方法集完全相同(方法的顺序)。
如果两个 map 类型的键类型和值类型相同那它们就是相等的。
如果两个 channel 类型包含的对象类型和 channel 的方向都是相同的那它们就是相同的。
给出下列声明:
type (
A0 = []string
A1 = A0
A2 = struct{ a, b int }
A3 = int
A4 = func(A3, float64) *A0
A5 = func(x int, _ float64) *[]string
)
type (
B0 A0
B1 []string
B2 struct{ a, b int }
B3 struct{ a, c int }
B4 func(int, float64) *B0
B5 func(x int, y float64) *A1
)
type C0 = B0这些类型是相等的:
A0, A1, and []string
A2 and struct{ a, b int }
A3 and int
A4, func(int, float64) *[]string, and A5
B0, B0, and C0
[]int and []int
struct{ a, b *T5 } and struct{ a, b *T5 }
func(x int, y float64) *[]string, func(int, float64) (result *[]string), and A5B0 和 B1 不是一种类型因为它们是通过类型定义方式分别定义的;func(int, float64) *B0 和 func(x int, y float64) *[]string 是不同的,因为 B0 和 []string 不是相同类型。
可分配性
在以下情况下,可以将 x 分配给类型为 T 的变量(把 x 分配给 T):
x 的类型为 T
x 的类型 V 和 T 有相同的底层类型并且类型 T 或 V 至少一个定义的类型
T 是一个接口类型并且 x 实现了 T
x 是一个 channel,并且 T 是channel类型,类型V和类型T有相同的元素类型,并且 2 种类型至少有一种不是定义的类型
x 等于 nil 并且 T 是一个指针,函数,切片,map,channel 或接口类型
x 是一个可以表示 T 类型值的无类型常量
代表性
满足以下条件时可以用 T 类型的值表示常量 x:
T 值的集合包括 x
T 是浮点型,而 x 在没有溢出的情况下能够近似成 T 类型。近似规则使用
IEEE 754 round-to-even,负零和无符号的零相同。需要注意的是,常量的值不会为负零,NaN,或无限值。T 为复数类型,并且 x 的
real(x)和imag(x)部分由复数类型对应的浮点类型(float32或float64)组成。
x T x 可以表示 T 的值,因为:
'a' byte 97 在 byte 类型值的集合中
97 rune rune 是 int32 的别名,97 在 32 位整型值的集合中
"foo" string "foo" 在字符串值的集合中
1024 int16 1024 在 16 位整型值的集合中
42.0 byte 42 在 8 位无符号整型值的集合中
1e10 uint64 10000000000 在 64 位无符号整型值的集合中
2.718281828459045 float32 2.718281828459045 的近似值 2.7182817 在 float32 类型值的集合中
-1e-1000 float64 -1e-1000 的近视值 IEEE -0.0,等于 0
0i int 0 是整型值
(42 + 0i) float32 42.0 (0 虚部) 在 float32 类型值的集合中x T x 不能表示 T 的值,因为:
0 bool 0 不在布尔值的集合中
'a' string 'a' 是 rune 类型, 它不在字符串类型的值集合中
1024 byte 1024 不在 8 位无符号整型值的集合中
-1 uint16 -1 不在 16 位无符号整型值的集合中
1.1 int 1.1 不是整型值
42i float32 (0 + 42i) 不在 float32 类型值的集合中
1e1000 float64 1e1000 取近似值时会溢出成 IEEE