介绍
数据类型指定在编写程序时特定变量将存储的值的种类。 数据类型还决定了可以对数据执行哪些操作。
在本文中,我们将介绍 Go 原生的重要数据类型。 这不是对数据类型的详尽调查,但会帮助您熟悉 Go 中可用的选项。 了解一些基本数据类型将使您能够编写更清晰、高效执行的代码。
背景
考虑数据类型的一种方法是考虑我们在现实世界中使用的不同类型的数据。 现实世界中的数据示例是数字:例如,我们可以使用整数(0、1、2、...)、整数(...、-1、0、1、...)和无理数(π)。
通常,在数学中,我们可以组合不同类型的数字,并得到某种答案。 我们可能想在 π 上加 5,例如:
5 + π
我们可以保留等式作为解决无理数的答案,或者将 π 舍入为一个小数位数缩写的数字,然后将这些数字相加:
5 + π = 5 + 3.14 = 8.14
但是,如果我们开始尝试用另一种数据类型(例如单词)来评估数字,事情就变得不那么有意义了。 我们将如何求解以下方程?
shark + 8
对于计算机来说,每种数据类型都非常不同——比如单词和数字。 因此,我们必须小心我们如何使用不同的数据类型来分配值以及我们如何通过操作来操作它们。
整数
与数学一样,计算机编程中的 整数 是整数,可以是正数、负数或 0(...、-1、0、1、...)。 在 Go 中,整数被称为 int
。 与其他编程语言一样,您不应该在四位或更多的数字中使用逗号,因此当您在程序中写 1,000 时,将其写为 1000
。
我们可以像这样简单地打印出一个整数:
fmt.Println(-459)
Output-459
或者,我们可以声明一个变量,在这种情况下,它是我们正在使用或操作的数字的符号,如下所示:
var absoluteZero int = -459 fmt.Println(absoluteZero)
Output-459
我们也可以在 Go 中用整数做数学运算。 在下面的代码块中,我们将使用 :=
赋值运算符来声明和实例化变量 sum
:
sum := 116 - 68 fmt.Println(sum)
Output48
如输出所示,数学运算符 -
从 116
中减去整数 68
,得到 48
。 您将在 为变量声明数据类型 部分了解有关变量声明的更多信息。
整数可以在 Go 程序中以多种方式使用。 随着您继续学习 Go,您将有很多机会使用整数并建立在您对这种数据类型的知识的基础上。
浮点数字
浮点数或浮点数用于表示实数,不能表示为整数。 实数包括所有有理数和无理数,因此,浮点数可以包含小数部分,例如 9.0 或 -116.42。 考虑到 Go 程序中的浮点数,它是一个包含小数点的数字。
就像我们对整数所做的那样,我们可以像这样简单地打印出一个浮点数:
fmt.Println(-459.67)
Output-459.67
我们还可以声明一个代表浮点数的变量,如下所示:
absoluteZero := -459.67 fmt.Println(absoluteZero)
Output-459.67
就像整数一样,我们也可以在 Go 中使用浮点数进行数学运算:
var sum = 564.0 + 365.24 fmt.Println(sum)
Output929.24
对于整数和浮点数,重要的是要记住 3 ≠ 3.0,因为 3 指的是整数,而 3.0 指的是浮点数。
数值类型的大小
除了整数和浮点数之间的区别之外,Go 还有两种类型的数字数据,它们通过大小的静态或动态特性来区分。 第一种类型是 架构无关 类型,这意味着数据的大小(以位为单位)不会改变,无论运行代码的机器如何。
今天的大多数系统架构要么是 32 位要么是 64 位。 例如,您可能正在为现代 Windows 笔记本电脑进行开发,该笔记本电脑上的操作系统在 64 位架构上运行。 但是,如果您正在为健身手表之类的设备进行开发,您可能正在使用 32 位架构。 如果您使用像 int32
这样的体系结构无关类型,则无论您编译的体系结构如何,该类型都将具有恒定大小。
第二种类型是 特定于实现的 类型。 在这种类型中,位大小可以根据构建程序的体系结构而有所不同。 例如,如果我们使用 int
类型,当 Go 编译为 32 位架构时,数据类型的大小将为 32 位。 如果程序是为 64 位架构编译的,则变量的大小将是 64 位。
除了具有不同大小的数据类型之外,像整数这样的类型还有两种基本类型:signed 和 unsigned。 int8
是有符号整数,取值范围为 -128 到 127。 uint8
是无符号整数,只能是 0 到 255 之间的正值。
范围基于位大小。 对于二进制数据,8 位总共可以表示 256 个不同的值。 因为 int
类型需要同时支持正值和负值,所以 8 位整数 (int8
) 的范围为 -128 到 127,总共有 256 个唯一的可能值。
Go 具有以下与体系结构无关的整数类型:
uint8 unsigned 8-bit integers (0 to 255) uint16 unsigned 16-bit integers (0 to 65535) uint32 unsigned 32-bit integers (0 to 4294967295) uint64 unsigned 64-bit integers (0 to 18446744073709551615) int8 signed 8-bit integers (-128 to 127) int16 signed 16-bit integers (-32768 to 32767) int32 signed 32-bit integers (-2147483648 to 2147483647) int64 signed 64-bit integers (-9223372036854775808 to 9223372036854775807)
浮点数和复数也有不同的大小:
float32 IEEE-754 32-bit floating-point numbers float64 IEEE-754 64-bit floating-point numbers complex64 complex numbers with float32 real and imaginary parts complex128 complex numbers with float64 real and imaginary parts
还有一些别名编号类型,它们为特定的数据类型分配有用的名称:
byte alias for uint8 rune alias for int32
byte
别名的目的是明确您的程序何时使用字节作为字符串元素中的常见计算度量,而不是与字节数据度量无关的小整数。 尽管 byte
和 uint8
在程序编译后是相同的,但 byte
通常用于以数字形式表示字符数据,而 uint8
旨在成为你程序中的一个数字。
rune
别名有点不同。 其中byte
和uint8
是完全相同的数据,一个rune
可以是单字节也可以是4字节,范围由int32
决定。 rune
用于表示 Unicode 字符,而只有 ASCII 字符可以单独由 int32
数据类型表示。
此外,Go 具有以下特定于实现的类型:
uint unsigned, either 32 or 64 bits int signed, either 32 or 64 bits uintptr unsigned integer large enough to store the uninterpreted bits of a pointer value
特定于实现的类型的大小将由编译程序的体系结构定义。
选择数值数据类型
选择正确的大小通常与您正在编程的目标架构的性能有关,而不是您正在使用的数据的大小。 但是,无需了解程序性能的具体影响,您可以在刚开始时遵循其中的一些基本准则。
正如本文前面所讨论的,存在与体系结构无关的类型和特定于实现的类型。 对于整数数据,Go 中通常使用 int
或 uint
等实现类型,而不是 int64
或 uint64
。 这通常会为您的目标架构带来最快的处理速度。 例如,如果您使用 int64
并编译为 32 位架构,则处理这些值所需的时间至少是跨架构移动数据所需的额外 CPU 周期的两倍。 如果您改用 int
,程序会将其定义为 32 位架构的 32 位大小,并且处理速度会明显加快。
如果您知道不会超出特定的大小范围,那么选择与体系结构无关的类型既可以提高速度又可以减少内存使用量。 例如,如果您知道您的数据不会超过 100
的值,并且只会是一个正数,那么选择 uint8
将使您的程序更高效,因为它需要更少记忆。
现在我们已经查看了数值数据类型的一些可能范围,让我们看看如果我们在程序中超出这些范围会发生什么。
溢出对比 环绕
当您尝试存储大于设计存储的数据类型的值时,Go 可能会同时出现 overflow 一个数字和 wraparound 一个数字,具体取决于该值是否计算为编译时或运行时。 当程序在尝试构建程序时发现错误时,会发生编译时错误。 程序编译后发生运行时错误,而它实际上正在执行。
在以下示例中,我们将 maxUint32
设置为其最大值:
package main import "fmt" func main() { var maxUint32 uint32 = 4294967295 // Max uint32 size fmt.Println(maxUint32) }
它将编译并运行,结果如下:
Output4294967295
如果我们在运行时将 1
添加到该值,它将环绕为 0
:
Output0
另一方面,让我们更改程序,在我们分配变量时,在编译前将 1
添加到变量中:
package main import "fmt" func main() { var maxUint32 uint32 = 4294967295 + 1 fmt.Println(maxUint32) }
在编译时,如果编译器可以确定一个值太大而无法保存在指定的数据类型中,它将抛出 overflow
错误。 这意味着计算的值对于您指定的数据类型来说太大了。
因为编译器可以确定它会溢出值,所以它现在会抛出一个错误:
Outputprog.go:6:36: constant 4294967296 overflows uint32
了解数据的边界将帮助您避免将来程序中的潜在错误。
现在我们已经介绍了数字类型,让我们看看如何存储布尔值。
布尔值
boolean 数据类型可以是 true
或 false
两个值之一,在将其声明为数据类型时定义为 bool
。 布尔值用于表示与数学逻辑分支相关的真值,它为计算机科学中的算法提供信息。
值 true
和 false
将始终分别使用小写的 t
和 f
,因为它们是 Go 中预先声明的标识符。
数学中的许多运算为我们提供了评估结果为真或假的答案:
- 大于
- 少于
- 等于
与数字一样,我们可以将布尔值存储在变量中:
myBool := 5 > 8
然后我们可以通过调用 fmt.Println()
函数来打印布尔值:
fmt.Println(myBool)
由于 5
不大于 8
,我们将收到以下输出:
Outputfalse
随着您在 Go 中编写更多程序,您将更加熟悉布尔值的工作方式以及对 true
或 false
求值的不同函数和操作如何改变程序的进程。
字符串
字符串是一个或多个字符(字母、数字、符号)的序列,可以是常量或变量。 字符串存在于 Go 中的反引号 `
或双引号 "
中,并且根据您使用的引号具有不同的特征。
如果您使用反引号,您将创建一个 raw 字符串文字。 如果您使用双引号,您将创建一个 interpreted 字符串文字。
原始字符串文字
原始字符串文字是反引号之间的字符序列,通常称为反引号。 在引号内,任何字符都将显示在反引号之间,但反引号字符本身除外。
a := `Say "hello" to Go!` fmt.Println(a)
OutputSay "hello" to Go!
通常,反斜杠用于表示字符串中的特殊字符。 例如,在解释字符串中,\n
将表示字符串中的新行。 但是,反斜杠在原始字符串文字中没有特殊含义:
a := `Say "hello" to Go!\n` fmt.Println(a)
因为反斜杠在字符串文字中没有特殊含义,所以它实际上会打印出 \n
的值,而不是换行:
OutputSay "hello" to Go!\n
原始字符串文字也可用于创建多行字符串:
a := `This string is on multiple lines within a single back quote on either side.` fmt.Println(a)
OutputThis string is on multiple lines within a single back quote on either side.
在前面的代码块中,新行从字面上从输入传递到输出。
解释的字符串文字
解释的字符串文字是双引号之间的字符序列,如 "bar"
。 在引号内,可以出现除换行符和非转义双引号外的任何字符。 要在解释字符串中显示双引号,可以使用反斜杠作为转义字符,如下所示:
a := "Say \"hello\" to Go!" fmt.Println(a)
OutputSay "hello" to Go!
您几乎总是会使用解释字符串文字,因为它们允许在其中包含转义字符。 有关使用字符串的更多信息,请查看 An Introduction to Working with Strings in Go。
带有 UTF-8 字符的字符串
UTF-8 是一种编码方案,用于将可变宽度字符编码为一到四个字节。 Go 支持开箱即用的 UTF-8 字符,无需任何特殊设置、库或包。 诸如字母 A
之类的罗马字符可以用数字 65 之类的 ASCII 值来表示。 但是,对于特殊字符,例如 世
的国际字符,将需要 UTF-8。 Go 对 UTF-8 数据使用 rune
别名类型。
a := "Hello, 世界"
您可以在 for
循环中使用 range
关键字来索引 Go 中的任何字符串,甚至是 UTF-8 字符串。 for
循环和 range
将在本系列后面更深入地介绍; 现在,重要的是要知道我们可以使用它来计算给定字符串中的字节数:
package main import "fmt" func main() { a := "Hello, 世界" for i, c := range a { fmt.Printf("%d: %s\n", i, string(c)) } fmt.Println("length of 'Hello, 世界': ", len(a)) }
在上面的代码块中,我们声明了变量 a
并将 Hello, 世界
的值赋给它。 分配的文本中包含 UTF-8 字符。
然后我们使用标准的 for
循环以及 range
关键字。 在 Go 中,range
关键字将通过一次返回一个字符的字符串以及该字符在字符串中的字节索引进行索引。
使用 fmt.Printf
函数,我们提供了 %d: %s\n
的格式字符串。 %d
是数字(在本例中为整数)的打印动词,而 %s
是字符串的打印动词。 然后我们提供了 i
的值,它是 for
循环的当前索引,以及 c
的值,它是 for
循环中的当前字符.
最后,我们使用内置的 len
函数打印了变量 a
的整个长度。
前面我们提到过,rune 是 int32
的别名,可以由一到四个字节组成。 世
字符需要三个字节来定义,并且在遍历 UTF-8 字符串时索引会相应移动。 这就是i
打印出来的时候不连续的原因。
Output0: H 1: e 2: l 3: l 4: o 5: , 6: 7: 世 10: 界 length of 'Hello, 世界': 13
如您所见,长度比遍历字符串所花费的次数要长。
您不会总是使用 UTF-8 字符串,但是当您使用时,您现在就会明白为什么它们是符文而不是单个 int32
。
声明变量的数据类型
现在您已经了解了不同的原始数据类型,我们将介绍如何将这些类型分配给 Go 中的变量。
在 Go 中,我们可以使用关键字 var
定义一个变量,后跟变量名和所需的数据类型。
在下面的示例中,我们将声明一个名为 pi
的变量,类型为 float64
。
关键字 var
是首先声明的内容:
var pi float64
后面是我们的变量名,pi
:
var pi float64
最后是数据类型 float64
:
var pi float64
我们也可以选择指定一个初始值,例如 3.14
:
var pi float64 = 3.14
Go 是一种 静态类型 语言。 静态类型意味着在编译时检查程序中的每个语句。 这也意味着数据类型绑定到变量,而在动态链接语言中,数据类型绑定到值。
例如,在 Go 中,类型是在声明变量时声明的:
var pi float64 = 3.14 var week int = 7
如果您以不同的方式声明它们,则这些变量中的每一个都可能是不同的数据类型。
这与 PHP 等语言不同,后者的数据类型与值相关联:
$s = "sammy"; // $s is automatically a string $s = 123; // $s is automatically an integer
在前面的代码块中,第一个 $s
是一个字符串,因为它被赋值为 "sammy"
,第二个是一个整数,因为它的值是 123
。
接下来,让我们看看更复杂的数据类型,如数组。
数组
array 是元素的有序序列。 数组的容量是在创建时定义的。 一旦数组分配了它的大小,就不能再改变大小。 因为数组的大小是静态的,这意味着它只分配一次内存。 这使得数组有点难以使用,但会提高程序的性能。 因此,优化程序时通常使用数组。 接下来介绍的 Slices 更加灵活,并且构成了您在其他语言中所认为的数组。
数组是通过声明数组的大小来定义的,然后是在大括号 { }
之间定义值的数据类型。
字符串数组如下所示:
[3]string{"blue coral", "staghorn coral", "pillar coral"}
我们可以将数组存储在变量中并打印出来:
coral := [3]string{"blue coral", "staghorn coral", "pillar coral"} fmt.Println(coral)
Output[blue coral staghorn coral pillar coral]
如前所述,切片类似于数组,但更加灵活。 让我们看一下这种可变数据类型。
切片
slice 是可以改变长度的有序元素序列。 切片可以动态增加它们的大小。 当您向切片添加新项目时,如果切片没有足够的内存来存储新项目,它将根据需要向系统请求更多内存。 因为可以在需要时扩展切片以添加更多元素,所以它们比数组更常用。
切片是通过在开始和结束方括号 []
之前声明数据类型并在大括号 { }
之间具有值来定义的。
一片整数看起来像这样:
[]int{-3, -2, -1, 0, 1, 2, 3}
一片花车看起来像这样:
[]float64{3.14, 9.23, 111.11, 312.12, 1.05}
一段字符串如下所示:
[]string{"shark", "cuttlefish", "squid", "mantis shrimp"}
让我们将字符串切片定义为 seaCreatures
:
seaCreatures := []string{"shark", "cuttlefish", "squid", "mantis shrimp"}
我们可以通过调用变量来打印它们:
fmt.Println(seaCreatures)
输出看起来与我们创建的列表完全相同:
Output[shark cuttlefish squid mantis shrimp]
我们可以使用 append
关键字向我们的切片添加一个项目。 以下命令会将 seahorse
的字符串值添加到切片中:
seaCreatures = append(seaCreatures, "seahorse")
您可以通过打印它来验证它是否已添加:
fmt.Println(seaCreatures)
Output[shark cuttlefish squid mantis shrimp seahorse]
如您所见,如果您需要管理未知大小的元素,切片将比数组更通用。
地图
map 是 Go 的内置哈希或字典类型。 地图使用 keys 和 values 作为一对来存储数据。 这在编程以通过索引或在本例中为键快速查找值时很有用。 例如,您可能希望保留一张用户地图,按用户 ID 编制索引。 键是用户 ID,用户对象是值。 使用关键字 map
后跟方括号 [ ]
中的键数据类型,然后是值数据类型和花括号中的键值对来构造映射。
map[key]value{}
通常用于保存相关数据,例如 ID 中包含的信息,映射如下所示:
map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"}
您会注意到,除了大括号之外,整个地图中还有冒号。 冒号左边的词是键。 密钥可以是 Go 中任何 可比 类型。 可比较类型是原始类型,如 strings
、ints
等。 原始类型是由语言定义的,而不是通过组合任何其他类型而构建的。 虽然它们可以是用户定义的类型,但最好的做法是让它们保持简单以避免编程错误。 上面字典中的键是:name
、animal
、color
和location
。
冒号右边的词是值。 值可以由任何数据类型组成。 上面字典中的值是:Sammy
、shark
、blue
和ocean
。
让我们将地图存储在一个变量中并打印出来:
sammy := map[string]string{"name": "Sammy", "animal": "shark", "color": "blue", "location": "ocean"} fmt.Println(sammy)
Outputmap[animal:shark color:blue location:ocean name:Sammy]
如果我们想隔离 Sammy 的颜色,我们可以通过调用 sammy["color"]
来实现。 让我们打印出来:
fmt.Println(sammy["color"])
Outputblue
由于地图提供了用于存储数据的键值对,它们可以成为 Go 程序中的重要元素。
结论
至此,您应该对 Go 中可用的一些主要数据类型有了更好的了解。 当您使用 Go 语言开发编程项目时,这些数据类型中的每一种都将变得很重要。
一旦你牢牢掌握了 Go 中可用的数据类型,你可以学习如何转换数据类型,以便根据情况更改数据类型。