目录

go 基础数据类型

Go 的类型系统

1. Go 中的数据类型

Go语言将数据类型分为四类:基础类型、复合类型、引用类型和接口类型。基础类型,包括:数字、字符串和布尔型。复合数据类型包括数组和结构体(通过组合简单类型,来表达更加复杂的数据结构)。引用类型包括指针、切片、字典、函数、通道,虽然数据种类很多,但它们都是对程序中一个变量或状态的间接引用。函数和通道并不属于我们通常所说的数据类型,我们放在后面相关章节来介绍。

对于大多数编程语言来说,基础类型以及它们之上的可用运算符都是类似,更加需要我们注意的是,编程语言提供给我们的数据容器以及操作它们的方式。因此我们分成以下几个部分来讲解 Go 的类型系统。

  1. 数值与布尔型
  2. 字符串与编码
  3. 数组与结构体
  4. 切片
  5. 字典

本节我们先来介绍 Go 中的基本数据类型,即数值,布尔值和字符串。在介绍这些数据类型之前,我们先来谈谈变量类型的含义,这有助于加深我们对编程语言本身的理解。

1.1 变量的类型

无论什么数据,在存储器内都是 0-1,那数据是数值还是字符完全取决于我们对这些二进制数据的解释。变量的类型就是用来定义对应存储值的属性特征,即它们在内部是如何表示的,支持的操作符,以及关联的方法集等。

而在一个编程语言类型系统中,除了内置的变量类型外,还有如下一些问题:

  1. 自定义类型
  2. 定义新的类型名称(类型重命名)
  3. 类型转换

1.2 自定义类型

自定义类型允许我们在编程语言底层类型的基础上定义更加复杂的类型,它是面向对象编程的基础。在 Go 中自定义类型就是使用结构体。

1.2 类型重命名

在任何程序中都会存在一些变量有着相同的内部结构,但是却表示完全不同的概念。例如,一个int类型的变量可以用来表示一个循环的迭代索引、或者一个时间戳、或者一个文件描述符。类型重命名就是为分隔不同概念的类型。新的类型名称使用类型声明语句创建。Go 的类型声明语法如下所示:

1
type 类型名字 底层类型

新的类型和底层类型具有相同的底层结构,支持和底层类型相同的运算符。但是新类型与底层类型以及基于相同底层类型的不同新类型,是完全不不同的数据类型。

1
2
3
4
5
6
7
8
import "fmt"
type Celsius float64 // 摄氏温度
type Fahrenheit float64 // 华氏温度

// 因为 Fahrenheit,float64,Celsius 是完全不同的类型,所以它们不能直接比较
// compile error: type mismatch
fmt.Println(Fahrenheit(1.0) == float64(1.0))
fmt.Println(Fahrenheit(1.0) == Celsius(1.0))

1.2 类型转换

对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型。如果T是指针类型,可能会需要用小括弧包装T,比如 (*int)(0)

在编程语言中,不同类型的变量之间是不能进行直接赋值和比较的,要这样做就需要显示或隐式的类型转换。对于不同编程语言而言,有不同的类型转换规则,但大多数规则都是类似。在 Go 中:

  1. 数值之间的转类型转换有一套特定规则,这个规则在不同的编程语言中是一样的,比如将浮点数转换为整数会损失小数部分
  2. 显示的类型转换T(x)要求 Tx 具有相同的底层基础类型或指向相同底层结构的指针类型;对于数据容器而言需要它们有类似的实现,比如可以将一个字符串转为 []byte类型
  3. 自定义的新类型名称,不会自动应用底层类型的隐式类型转换规则,一个命名类型的变量只能和另一个有相同类型的变量,或有着相同底层类型的未命名类型的值之间做比较;依赖相同底层类型的不同自定义类型之间想要进行比较或赋值必须进行显示的类型转换。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import "fmt"

// 自定义类型与其底层类型不可比较
type tt int
fmt.Println(tt(1) > int(0) // compile error: type mismatch

var c Celsius
var f Fahrenheit
fmt.Println(c == 0) // "true"
fmt.Println(f >= 0) // "true"

// 依赖相同底层类型的不同自定义类型不可比较
fmt.Println(c == f) // compile error: type mismatch
fmt.Println(c == Celsius(f)) // "true"!

说了这么多,接下来我们开始正式讲解 Go 中的数据类型。

2. 数值

Go语言的数值类型包括几种不同大小的整数、浮点数和复数,还有一些为特定用途定义的类型别名。

2.1 整数

整数包括如下几种类型及类型别名:

类型 大小 含义
uint8 8 无符号 8 位整型
uint16 16 无符号 16 位整型
uint32 32 无符号 32 位整型
uint64 64 无符号 64 位整型
uint 32 或 64位 平台相关,取决于CPU平台机器字大小
int 32 或 64位 平台相关,取决于CPU平台机器字大小
int8 8 有符号 8 位整型
int16 16 有符号 16 位整型
int32 32 有符号 32 位整型
int64 64 有符号 64 位整型
byte 8, int8的别名 表示原始的二进制数据
rune 32, int32的别名 Unicode字符,表示一个Unicode码点
uintptr 无符号整数,没有明确指定大小 用于存放一个指针,GO 底层使用

其中int是应用最广泛的数值类型。内置的len函数返回一个有符号的int,虽然使用uint无符号类型似乎是一个更合理的选择。len函数返回有符号 int ,可以使我们像下面这样处理逆序循环。

1
2
3
4
medals := []string{"gold", "silver", "bronze"}
  for i := len(medals)  1; i >= 0; i‐‐ {
    fmt.Println(medals[i]) // "bronze", "silver", "gold"
}

所以尽管Go语言提供了无符号数和运算,并且在数值本身不可能出现负数的情况下,我们还是倾向于使用有符号的int类型。出于这个原因,无符号数往往只有在位运算或其它特殊的运算场景才会使用,就像bit集合、分析二进制文件格式或者是哈希和加密操作等。它们通常并不用于仅仅是表达非负数量的场合。

2.2 整数的运算符

Go 的整数支持如下操作符号,其中大多数与其他语言类似,只有一个比较特殊x &^ y,它表示将 x 中与 y 对应的且 y 中等于 1 的位置为 0,即位清空(AND NOT)

1
2
3
4
5
6
7
# 优先级递减
*   /    %               # 算数运算符
<<  >>   &   &^          # 位运算符
+    ‐                   # 算数运算符
|    ^                   # 位运算符
==  !=   <   <=  >  >=   # 比较运算符
&&(AND)  ||(or)          # 逻辑运算符

2.3 浮点数

Go语言提供了两种精度的浮点数,float32 和 float64 。浮点数的范围极限值可以在math包找到。常量math.MaxFloat32表示float32能表示的最大数值,对应的 float64 为 math.MaxFloat64

一个float32类型的浮点数可以提供大约6个十进制数的精度,而float64则可以提供约15个十进制数的精度;通常应该优先使用float64类型。小数点前面或后面的数字都可能被省略(例如.707或1.)。很小或很大的数最好用科学计数法书写,通过e或E来指定指数部分。

1
2
3
4
5
6
7
// float32的有效bit位只有23个,整数大于23bit表示范围时,将出现误差
var f float32 = 16777216 // 1 << 24
fmt.Println(f == f+1) // "true"!

const a = .909
const Avogadro = 6.02214129e23 // 阿伏伽德罗常数
const Planck = 6.62606957e34 // 普朗克常数

math包中除了提供大量常用的数学函数外,还提供了IEEE754浮点数标准中定义的特殊值的创建和测试。

1
2
3
4
5
6
7
8
9
v := math.Inf(1) // 返回正无穷
p := math.Inf(-1) // 返回负无穷

n := math.NaN() // 返回 NaN 非数,一般用于表示无效的除法操作结果0/0或Sqrt(­1).
t = math.IsNaN(n) // 测试是否为 NaN

// NaN和任何数都是不相等的
nan := math.NaN()
fmt.Println(nan == nan, nan < nan, nan > nan) // "false false false"

2.4 复数

Go语言提供了两种精度的复数类型:complex64complex128,分别对应 float32float64 两种浮点数精度。内置的complex函数用于构建复数,内建的realimag函数分别返回复数的实部和虚部。复数的字面量使用 i 后缀。

1
2
3
4
5
6
7
8
9
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
fmt.Println(x*y) // "(‐5+10i)"
fmt.Println(real(x*y)) // "‐5"
fmt.Println(imag(x*y)) // "10"

// 复数的字面量
x := 1 + 2i
y := 3 + 4i

3. 布尔值

Go 布尔类型的值只有两种:truefalseiffor 语句的条件部分都是布尔值。需要特别注意的是 在 Go 中布尔之值不会与其他任何类型作隐式转换,将其他类型的值用在 iffor 中作为条件判断时,必须作显示的类型转换。

1
2
3
4
5
6
7
func itob(i int) bool { return i != 0 }

b := 0
i := 0
if itob(b) {
  i = 1
}

4. 字符串

4.1 字符串操作

创建字符串最简单的方式是字符串字面量。在 Go 中,单个字符的字面量使用单引号,字符串字面量使用双引号,原生字符串使用反引号。所谓原生字符类似于 Python 中的 r"" 用于消除字符串中的所有转义操作。Go 的原生字符甚至可以消除换行,实现跨行,所以原生字符广泛使用再正则表达式,HTML模板、JSON面值以及命令行提示信息中。

与 Python 将大多数字符串操作作为字符串对象的方法不同,Go 大多数的字符串操作都在 strings 包,我们将这部分内容放在后面专门介绍,先来看看Go 提供的字符串基础操作。下面是一些代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// 原生字符串
const GoUsage = `Go is a tool for managing Go source code.
Usage:
go command [arguments]
`

s := "hello, world"
// 1. len 函数获取字符串长度
fmt.Println(len(s)) // "12"

// 2. 索引
fmt.Println(s[0], s[7]) // "104 119" ('h' and 'w')

// 3. 切片
fmt.Println(s[0:5]) // "hello

// 4. + 拼接
fmt.Println("goodbye" + s[5:]) // "goodbye, world"

// 5. 不可修改
s[0] = 'L' // compile error: cannot assign to s[0]

虽然字符串作为一个基本的数据类型被几乎所有的编程语言所支持,但是字符串本身确是很复杂。而复杂的地方至少有如下两点:

  1. 字符串的实现
  2. 字符的编码问题

4.1 字符串的实现

/images/go/grammar/string_base.png

上面是字符串以及切片操作结果的示意图,在 Go 中,字符串是一个不可改变的字节序列,底层是一个字符数组,一个字符串可认为由两个部分构成:指针、长度

  1. 指针指向第一个字符对应的底层数组元素的地址
  2. 长度对应字符串中字符的个数
  3. 字符串的底层数组位于受保护的内存中,不能被修改,因此字符串是不可变的
  4. 对字符串变量进行重新赋值,不会改变字符串的底层数组,而只是改变了字符串中的指针的指向

不变性意味两个字符串可以安全的共享相同的底层数据,这使得字符串复制和切片不会发生实际的复制行为,而是直接共享原有的底层字符数组,因此操作非常迅速。

4.3 字符集

在上面关于字符串的实现中,我们忽略了一个问题,即如何把字符串中的字符保存在一个数组中。我们知道在计算机上保存的数据只有二进制的 0 和 1,显然计算机没办法直接保存每个字符,于是就有了字符集的概念。

对于字符集以及字符的编码和解码,我是这样理解的:

  1. 字符集中最重要的概念就是码表,其作用是将每个字符与一个特定的数字对应起来,用特定的数字(又称码点)来表示特定的字符,因此码表就是字符集能表示的字符范围
  2. 有了码表,并没有解决保存字符的问题,显然就算是数字也要保存为整数的二进制格式。对于不同字符集而言,码点到特定的二进制也有一套特定的转换规则
  3. 因此,字符集实现了字符 --> 码点 ---> 码点二进制值的转换过程,码点 ---> 码点二进制值被称为编码,反过来就是解码

有了上面的说明,就能解释清楚下面两个问题:

  1. ASCII 字符集 与 Unicode 字符集区别: ASCII字符集使用7bit来表示一个码点,而 Unicode 使用32bit表示一个 Unicode 码点,Unicode 显然能表示更大的字符范围
  2. UTF8 编码与 UTF32 编码的区别: UTF32 编码直接将每个 Unicode 码点保存为 int32 的整数,而UTF8 会根据Unicode码点变长编码成二进制,它们都表示 Unicode 字符集,但是编码规则不同

4.4 字符串和 []rune

Go语言的源文件采用UTF8编码,因此程序运行之后,保存在字符数组内的是 UTF8 编码的二进制值。因此前面我们所讲的字符串基础操作,操作的其实是UTF8 编码的每个字节,并不是我们理解的字符。为了处理真实的字符,我们需要对字符串进行解码。Go 将 Unicode 码点表示为 rune 整数类型,因此字符串解码后的类型就是 []rune。下面就是Go 中字符编码解码的一些代码示例:

/images/go/grammar/string_unicode.png

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 1. 字符串基础操作操作的是 UTF8 中的字节
import "unicode/utf8"
s := "Hello, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"

// 2. unicode 提供了 UTF8 的解码函数
for i := 0; i < len(s); {
  r, size := utf8.DecodeRuneInString(s[i:])
  fmt.Printf("%d\t%c\n", i, r)
  i += size
}

// 3. range 会自动对字符串解码
for i, r := range "Hello, 世界" {
  fmt.Printf("%d\t%q\t%d\n", i, r, r)
}

// 4. []rune 字符串的类型转换
s := "プログラム"
fmt.Printf("% x\n", s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0"

// 字符串 --> []rune
r := []rune(s)
fmt.Printf("%x\n", r) // "[30d7 30ed 30b0 30e9 30e0]

// string 函数: []rune ---> 字符串
fmt.Println(string(r)) // "プログラム

// 5. 生成Unicode码点字符的UTF8字符串
fmt.Println(string(65)) // "A", not "65"
fmt.Println(string(0x4eac)) // "京"

4.5 字符串和 []byte

一个字符串是包含的只读字节数组,一旦创建,是不可变的。相比之下,一个字节slice(即 []byte,下一节我们会详述)的元素则可以自由地修改。字符串和字节slice之间可以相互转换:

1
2
3
s := "abc"
b := []byte(s)
s2 := string(b)

4.6 字符串相关类型的包

标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包

  1. strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
  2. bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型
  3. strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换
  4. unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类,每个函数有一个单一的rune类型的参数,然后返回一个布尔值

下面是字符串与数值转换的代码示例,我们会在后面专门讲解这些包的实现和使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 数值转字符串
x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"

// 数值的进制转换
fmt.Println(strconv.FormatInt(int64(x), 2)) // "1111011"
s := fmt.Sprintf("x=%b", x) // "x=1111011

// 字符串转数值
x, err := strconv.Atoi("123") // x is an int
y, err := strconv.ParseInt("123", 10, 64) // base 10, up to 64 bits

5. 常量

5.1 常量的类型

在讲常量之前,先问大家一个问题,你知道字面量,常量,变量,字面量类型之间的区别么?

字面量是编程语言提供的用来创建特定值的快捷方式,因此字面量也有类型,特定的字面量代表什么类型,完全有编程语言决定。因此对于像下面的赋值语句来说,在字面量类型和变量类型之间发生了类型转换。

1
var f float64 = 3

常量和变量都是变量,但是相比与变量,常量有以下特点:

  • 常量的值不可变,并且常量的类型只能是基础类型:boolean、string或数字
  • 常量表达式的值在编译期计算,而不是在运行期,因此常量可以是构成类型的一部分,例如用于指定数组类型的长度

因为常量也是变量,所以常量通常有确定的类型,但Go语言的常量有个不同寻常之处, Go 中的常量可以没有一个明确的基础类型。

首先在 Go 中,有六种无类型的字面量,分别是无类型的布尔型、无类型的整数、无类型的字符、无类型的浮点数、无类型的复数、无类型的字符串。例如0、0.0、0i和’\u0000’分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符。

其次在如下不带类型声明的常量声明语句中,不会发生隐式类型转换,常量的类型依旧为无类型的整数。

1
const deadbeef = 0xdeadbeef // untyped int with value 3735928559

为了便于描述下面我们将无类型的字面量和常量统称为无类型常量,这些无类型常量有诸多好处。

编译器为这些无类型常量提供了比基础类型更高精度的算术运算。通过延迟明确常量的具体类型,无类型的常量不仅可以提供更高的运算精度,而且可以直接用于更多的表达式而不需要显式的类型转换。

只有常量可以是无类型的。当一个无类型的常量被赋值给一个变量的时候,或者出现在有明确类型的变量声明的右边,无类型的常量将会被隐式转换为对应的类型,如果转换合法的话。对于一个没有显式类型的变量声明(包括简短变量声明),字面量的形式将隐式决定变量的默认类型,Go 有一个明确的转换规则。如果要给变量一个不同的类型,我们必须显式地将无类型的常量转化为所需的类型,或给声明的变量指定明确的类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 常量可以无类型,无类型常量可以提供更高的精度
const (
  deadbeef = 0xdeadbeef // untyped int with value 3735928559
  a = uint32(deadbeef) // uint32 with value 3735928559
  b = float32(deadbeef) // float32 with value 3735928576 (rounded up)
  c = float64(deadbeef) // float64 with value 3735928559 (exact)
  d = int32(deadbeef) // compile error: constant overflows int32
  e = float64(1e309) // compile error: constant overflows float64
  f = uint(1) // compile error: constant underflows uint
)

// 2. 无类型常量,可以直接应用在更多的表达式中,无需显示类型转换
var f float64 = 3 + 0i // untyped complex ‐> float64
f = 2 // untyped integer ‐> float64
f = 1e123 // untyped floating‐point ‐> float64
f = 'a' // untyped rune ‐> float64

// 3. 有类型声明时,无类型常量将根据类型隐式类性转换
var x float32 = math.Pi
var y float64 = math.Pi
var z complex128 = math.Pi

// 4. 无类型声明时,根据字面量形式,决定变量类型
i := 0 // untyped integer; implicit int(0)  
r := '\000' // untyped rune; implicit rune('\000')
f := 0.0 // untyped floating‐point; implicit float64(0.0)
c := 0i // untyped complex; implicit complex128(0i)

var i = int8(0)
var i int8 = 0

5.2 常量批量声明

最后,Go 为常量的批量声明提供了一些便捷方式,下面是代码示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 1. 批量声明多个常量
const (
  e = 2.71828182845904523536028747135266249775724709369995957496696763
  pi = 3.14159265358979323846264338327950288419716939937510582097494459
)

const (
  a = 1
  b      // 省略初始化表达式,表示使用前面常量的初始化表达式写法, b=1
  c = 2
  d     // d=2
)

// 2. iota常量生成器初始化,用于生成一组以相似规则初始化的常量
type Weekday int
  const (
  Sunday Weekday = iota  // 在第一个声明的常量所在的行,iota将会被置为0,
  Monday                 // 然后在每一个有常量声明的行加一, 1
  Tuesday               // 2
  Wednesday             // 3
  Thursday
  Friday
  Saturday
)

const (
  _ = 1 << (10 * iota)
  KiB // 1024
  MiB // 1048576
  GiB // 1073741824
  TiB // 1099511627776 (exceeds 1 << 32)
  PiB // 1125899906842624
  EiB // 1152921504606846976
  ZiB // 1180591620717411303424 (exceeds 1 << 64)
  YiB // 1208925819614629174706176
)