Go 数据类型
Go 语言是一种 静态类型 的编程语言,在 Go 编程语言中,数据类型用于声明常量和变量。数据类型 是指 数据 的类型 ,规定数据的存放格式和所占空间,所占空间称为大小或尺寸,一般使用字节(byte)做为尺寸的度量单位,一个字节相当于 8 个比特。编译器在进行编译的时候,就要知道每个值的类型,这样编译器就知道要为这个值分配多少内存,并且知道这段分配的内存表示什么。
一、基本数据类型(basic type)
基本数据类型是 Go 语言内置的数据类型,内置类型也称为预声明类型,包括:
- 字符串类型:string
- 布尔类型:bool
数值类型:
- 整型:int8、uint8(byte)、int16、uint16、int32(rune)、uint32、int64、uint64、int、uint、uintptr
- 浮点型:float32、float64
- 复数:complex64、complex128
基本数据类型说明:
类型 | 描述 |
---|---|
uint | 32 位或 64 位 |
uint8 | 无符号 8 位整型 (0 到 255) |
uint16 | 无符号 16 位整型 (0 到 65535) |
uint32 | 无符号 32 位整型 (0 到 4294967295) |
uint64 | 无符号 64 位整型 (0 到 18446744073709551615) |
int | 32 位或 64 位 |
int8 | 有符号 8 位整型 (-128 到 127) |
int16 | 有符号 16 位整型 (-32768 到 32767) |
int32 | 有符号 32 位整型 (-2147483648 到 2147483647) |
int64 | 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
byte | uint8 的别名 (type byte = uint8) |
rune | int32 的别名(type rune = int32),表示一个 unicode 码 |
uintptr | 无符号整型,用于存放一个指针是一种无符号的整数类型,没有指定具体的 bit 大小但是足以容纳指针。 |
float32 | IEEE-754 32 位浮点型数 |
float64 | IEEE-754 64 位浮点型数 |
complex64 | 32 位实数和虚数 |
complex128 | 64 位实数和虚数 |
注意 :
- 17 种内置基本类型(type)各自属于一种 Go 中的类型种类(kind)
- byte 是 uint8 的一个内置别名,rune 是 int32 的一个内置别名
- uintptr 类型只有在底层编程是才需要,特别是 Go 语言和 C 语言函数库或操作系统接口相交互的地方
- 字符串一旦赋值了,就不能修改了,在 Go 中字符串是不可变的,可以赋新值给字符串变量或者使用字节数组
- uintptr、int 以及 uint 类型的值的尺寸依赖于具体编译器实现。在 64 位的架构上,int 和 uint 类型的值是 64 位的;编译器必须保证 uintptr 类型的值的尺寸能够存下任意一个内存地址
二、组合类型(composite type)
- 指针类型 - 类 C 指针
- 结构体类型 - 类 C 结构体
- 函数类型 - 函数类型在 Go 中是一种一等公民类别
容器类型,包括:
- 数组类型 - 定长容器类型
- 切片类型 - 动态长度和容量容器类型
- 映射类型(map)- 也常称为字典类型。在标准编译器中映射是使用哈希表实现的。
- 通道类型 - 通道用来同步并发的协程
- 接口类型 - 接口在反射和多态中发挥着重要角色
组合类型可以用它们各自的字面表示形式来表示。下面是一些各种不同类型的组合类型字面表示形式的例子:
// 假设T为任意一个类型,Tkey为一个支持比较的类型。
*T // 一个指针类型
[5]T // 一个元素类型为T、元素个数为5的数组类型
[]T // 一个元素类型为T的切片类型
map[Tkey]T // 一个键值类型为Tkey、元素类型为T的映射类型
// 一个结构体类型
struct {
name string
age int
}
// 一个函数类型
func(int) (bool, string)
// 一个接口类型
interface {
Method0(string) int
Method1() (int, bool)
}
// 几个通道类型
chan T
chan<- T
<-chan T
1、指针类型
指针 是 Go 中的一种类型分类(kind)。一个指针是某个指针类型的一个值。一个指针可以存储一个内存地址。事实上,我们经常称一个指针为一个内存地址,或者称一个内存地址为一个指针。通常地,一个指针此中存储的内存地址为另外一个值的地址。和 C 指针不一样,为了安全起见,Go 指针有很多限制。
在 Go 中,一个数据类型的字面形式为 T,其中 T 为一个任意类型, 称此 T(在类型之前加一个星号)为 T 的指针类型,类型 T 称为指针类型 T 的基类型(base type)。如果一个指针类型的 底层类型 是 *T,则它的基类型为 T。指针类型的零值的字面形式使用预声明的 nil 来表示。一个 nil 指针(常称为空指针)中不存储任何地址。
如果一个指针类型的基类型为 T,则此指针类型的值只能存储类型为 T 的值的地址。
虽然我们可以声明定义指针类型,但是一般不推荐这么做。非定义指针类型的可读性更高。
*int // 一个基类型为int的非定义指针类型。
**int // 一个多级非定义指针类型,它的基类型为*int。
type Ptr *int // Ptr是一个定义的指针类型,它的基类型为int。
type PP *Ptr // PP是一个定义的多级指针类型,它的基类型为Ptr。
2、结构体类型
每个结构体类型的字面形式均由 struct 关键字开头,后面跟着用一对大括号 {},其中包裹着的一系列 字段(field)声明。一般来说,每个字段声明由一个字段名和字段类型组成。一个结构体类型的字段数目可以为 0。下面是一个非定义结构体类型的字面形式:
struct {
title string
author string
pages int
}
上面这个结构体类型含有三个字段。前两个字段(title 和 author)的类型均为 string。最后一个字段 pages 的类型为 int。
有时字段也称为成员变量。 相邻的同类型字段可以声明在一起。比如上面这个类型也可表示成下面这样:
struct {
title, author string
pages int
}
3、容器类型和容器值概述
每个 容器 用来表示和存储一个元素(element)序列或集合。一个容器中的所有元素的类型是相同的,此相同的类型称为容器的元素类型。
存储在一个容器中的每个元素值都关联着着一个键值(key)。每个元素可以通过它的键值而被访问到。一个映射类型的键值类型必须为一个可比较类型。数组和切片类型的键值类型均为内置类型 int。一个数组或切片的一个元素对应的键值总是一个非负整数下标,此非负整数表示该元素在该数组或切片所有元素中的顺序位置。此非负整数下标亦常称为一个元素索引(index)。
每个容器值有一个长度属性,用来表明此容器中当前存储了多少个元素。一个数组或切片中的每个元素所关联的非负整数索引键值的合法取值范围为左闭右开区间 [0, 此数组或切片的长度)。一个映射值类型的容器值中的元素关联的键值可以是任何此映射类型的键值类型的任何值。
这三种容器类型的值在使用上有很多的差别。这些差别多源于它们的内存定义的差异。一个数组或者切片的所有元素紧挨着存放在一块连续的内存中。映射是使用哈希表算法来实现的,所以一个映射中的所有元素也均存放在一块连续的内存中,但是映射中的元素并不一定紧挨着存放
容器类型的字面表示形式如下:
- 数组类型:[N]T
- 切片类型:[]T
- 映射类型:map[K]T 其中,
- T 可为任意类型。它表示一个容器类型的元素类型。某个特定容器类型的值中只能存储此容器类型的元素类型的值。
- N 必须为一个非负整数常量。它指定了一个数组类型的长度,或者说它指定了此数组类型的任何一个值中存储了多少个元素。一个数组类型的长度是此数组类型的一部分。比如 [5]int 和[8]int 是两个不同的类型。
- K 必须为一个可比较类型。它指定了一个映射类型的键值类型。
4、通道类型
和数组、切片以及映射类型一样,每个 通道类型 也有一个元素类型。一个通道只能传送它的(通道类型的)元素类型的值。 通道可以是双向的,也可以是单向的。
- 字面形式 chan T 表示一个元素类型为 T 的双向通道类型。编译器允许从此类型的值中接收和向此类型的值中发送数据。
- 字面形式 chan<- T 表示一个元素类型为 T 的单向发送通道类型。编译器不允许从此类型的值中接收数据。
- 字面形式 <-chan T 表示一个元素类型为 T 的单向接收通道类型。编译器不允许向此类型的值中发送数据。 双向通道 chan T 的值可以被隐式转换为单向通道类型 chan<- T 和 <-chan T,但反之不行(即使显式也不行)。类型 chan<- T 和 <-chan T 的值也不能相互转换。
每个通道值有一个容量属性。一个容量为 0 的通道值称为一个非缓冲通道(unbuffered channel),一个容量不为 0 的通道值称为一个缓冲通道(buffered channel)。通道类型的零值也使用预声明的 nil 来表示。一个非零通道值必须通过内置的 make 函数来创建。比如 make(chan int, 10)将创建一个元素类型为 int 的通道值。第二个参数指定了欲创建的通道的容量。此第二个实参是可选的,它的默认值为 0。
5、函数类型
一个函数类型的字面表示形式由一个 func 关键字和一个 函数签名 字面表示形式组成。一个 函数签名 由一个 输入参数类型列表 和一个 输出结果类型列表 组成。参数名称和结果名称可以出现函数签名的字面表示形式中,但是它们并不重要。
鉴于函数类型的字面形式和函数签名字面表示形式的相似性(仅相差一个 func 关键字),这两个概念常常可以混淆使用。 下面是一个函数类型的字面形式:
func (a int, b string, c string) (x int, y int, z bool)
连续的同类型参数和结果可以声明在一块儿。所以上面的字面形式等价于:
func (a int, b, c string) (x, y int, z bool)
参数名称和结果名称并不重要,只要它们不重名即可。上面两个字面形式等价于下面这个:
func (x int, y, z string) (a, b int, c bool)
参数名和结果名可以是空标识符_。上面的字面形式等价于:
func (_ int, _, _ string) (_, _ int, _ bool)
函数参数列表中的参数名或者结果列表中的结果名可以同时省略(即匿名)。上面的字面形式等价于:
func (int, string, string) (int, int, bool) // 标准函数字面形式
func (a int, b string, c string) (int, int, bool)
func (x int, _ string, z string) (int, int, bool)
func (int, string, string) (x int, y int, z bool)
func (int, string, string) (a int, b int, _ bool)
6、接口类型
一个接口类型指定了一个方法原型的集合。换句话说,一个接口类型定义了一个方法集。事实上,我们可以把一个接口类型看作是一个方法集。接口类型中指定的任何方法原型中的方法名称都不能为空标识符_。和结构体类似,接口类型的字面形式均由 interface 关键字开头,后面跟着用一对大括号 {},其中包裹着的一系列 方法 声明,一些接口类型的例子:
// 一个非定义接口类型。
interface {
About() string
}
// ReadWriter是一个定义的接口类型。
type ReadWriter interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
}
// Runnable是一个非定义接口类型的别名。
type Runnable = interface {
Run()
}
特别地,一个没有指定任何方法原型的接口类型称为一个空接口类型。下面是两个空接口类型:
// 一个非定义空接口类型。
interface{}
// 类型I是一个定义的空接口类型。
type I interface{}
三、类型定义(type definition declaration)
在 Go 中,我们可以用如下形式来定义新的类型。在此语法中,type 为一个关键字,新的类型名必须为标识符。
// 定义单个类型。
type NewTypeName SourceType
// 定义多个类型。
type (
NewTypeName1 SourceType1
NewTypeName2 SourceType2
)
- 一个新定义的类型和它的源类型为两个不同的类型。
- 在两个不同的类型定义中的定义的两个类型肯定为两个不同的类型。
- 一个新定义的类型和它的源类型的底层类型(将在下面介绍)一致并且它们的值可以相互显式转换。
- 类型定义可以出现在函数体内。
四、类型别名声明
我们可以使用下面的语法来声明自定义类型别名。此语法和类型定义类似,但是请注意其中多了一个等号 =。
type (
Name = string
Age = int
)
type table = map[string]int
type Table = map[Name]Age
类型别名也必须为标识符。同样地,类型别名可以被声明在函数体内。
事实上,文字表示形式 map[string]int 和 map[Name]Age 也表示同一类型。所以,table 和 Table 一样表示同一个类型。
注意,尽管两个别名 table 和 Table 表示同一个类型,但 Table 是导出的,所以它可以被其它包引入使用,而 table 却不可以。
五、底层类型(underlying type)
在 Go 中,每个类型都有一个底层类型。规则:
- 一个内置类型(包括 unsafe 标准库包中定义的 Pointer 类型)的底层类型为它自己。
- 一个组合类型的底层类型为它自己。
在一个类型声明中,新声明的类型和源类型共享底层类型。一个例子:
// 这四个类型的底层类型均为内置类型int。 type ( MyInt int Age MyInt ) // 下面这三个新声明的类型的底层类型各不相同。 type ( IntSlice []int // 底层类型为[]int MyIntSlice []MyInt // 底层类型为[]MyInt AgeSlice []Age // 底层类型为[]Age ) // 类型[]Age、Ages和AgeSlice的底层类型均为[]Age。 type Ages AgeSlice
如何溯源一个声明的类型的底层类型?规则很简单,在溯源过程中,当遇到一个内置类型或者非定义类型时,溯源结束。以上面这几个声明的类型为例,下面是它们的底层类型的溯源过程:
MyInt → int Age → MyInt → int IntSlice → []int MyIntSlice → []MyInt → []int AgeSlice → []Age → []MyInt → []int Ages → AgeSlice → []Age → []MyInt → []int
在 Go 中,
底层类型为内置类型 bool 的类型称为布尔类型;
底层类型为任一内置整数类型的类型称为整数类型;
底层类型为内置类型 float32 或者 float64 的类型称为浮点数类型;
底层类型为内置类型 complex64 或 complex128 的类型称为复数类型;
整数类型、浮点数类型和复数类型统称为数字值类型;
底层类型为内置类型 string 的类型称为字符串类型。
底层类型这个概念在类型转换、赋值和比较规则中扮演着重要角色。
六、值(value)
一个类型的一个 实例 称为此类型的一个 值。一个类型可以有很多不同的值,其中一个为它的 零值 。同一类型的不同值共享很多相同的属性。每个类型有一个零值。一个类型的零值可以看作是此类型的 默认值 。预声明的标识符 nil 可以看作是切片、映射、函数、通道、指针(包括非类型安全指针)和接口类型的零值的字面表示形式。
在源代码中,值可以呈现为若干种形式,包括字面形式、有名常量、变量和表达式。前三中形式可以看作是最后一种形式的特例。值分为 类型确定 的和 类型不确定 的。
除了各种基本类型的值的字面表示形式,Go 中还有另外两种值的字面表示形式:组合字面表示形式(composite literal)和函数字面表示形式。
- 组合字面表示形式用来表示结构体类型值和容器类型(数组、切片和映射)值。
- 函数字面表示形式用来表示函数值。事实上,一个函数声明是由一个标识符(函数名)和一个函数字面表示形式组成。
注意:指针类型、通道类型和接口类型的值没有字面表示形式。
七、可比较类型和不可比较类型
下面这些类型的值不支持(使用 == 和!= 运算标识符)比较。这些类型称为不可比较类型。
切片类型
映射类型
函数类型
任何包含有不可比较类型的字段的结构体类型和任何元素类型为不可比较类型的数组类型。
其它类型称为可比较类型。注意:映射类型的键值类型必须为可比较类型。