基本数据
基本数据
毫无疑问, 计算机底层全是位, 而实际操作则是基于大小固定的单元中的数值, 称为字, 这些值可解释为整数, 浮点数, 位集或内存地址等, 进而构成更大的聚合体, 以表示数据包, 像素, 文件, 诗集, 以及其他种种.
Go
的数据类型分为四大类: 基础类型, 聚合类型, 引用类型和接口类型. 基础类型, 包括数字, 字符串和布尔型. 聚合类型包括, 数组和结构体. 引用类型, 包括指针, slice
, map
, 函数, 以及通道, 它们的共同点都是全部间接指向程序变量或状态, 这意味着对任一引用类型数据的修改都会影响所有该引用的拷贝.
整型
Go
语言的数值类型包括几种不同大小的整数, 浮点数和复数. 各种数值类型分别有自己的大小, 对正负号支持也各异.
Go
同时具备有符号整数和无符号整数. 有符号整数分为四种大小: 8位, 16位, 32位, 64位, 用int8
, int16
, int32
, int64
表示, 对应的无符号整数是uint8
, uint16
, uint32
, uint64
.
此外还有两种类型int
和uint
. 在特定平台上, 其大小和原生的有符号数\
无符号整数相同, 或等于该平台上运算效率最高的值. int
是目前使用最广泛的数值类型. 者两种类型大小相等, 都是32位或64位, 版不能认为它们就一定是32位, 或一个就是64位; 即使在同样的硬件平台上, 不同的编译器也可能选用不同的大小.
rune
类型是int32
类型的同义词, 常常用于指明一个值是Unicode
码点. 这两个名称可互换使用. 同样, byte
类型是uint8
类型的同义词, 强调一个值是原始数据, 而非量值.
最后, 还有一种无符号整数uintptr
, 其大小并不明确, 但足以完整存放指针. uintptr
类型仅仅用于底层编码, 例如在Go
程序与C
程序库或操作系统接口相互交互的地方.
int
, uint
和uintptr
都是不同类型的兄弟类型, 也必须进行显式地类型操作转换.
有符号数以补码表示.
算术运算符+
, -
, *
, /
可应用于整数, 浮点数和复数, 而取模运算符%
仅能用于整数. 取模运算符%
的行为因编程语言而异. 就Go
而言, 取模余数的正负号总是与被除数一致, 于是-5%3
和-5%-3
都得-2
. 除法运算(/)
的行为取决于操作数是否都为整形, 整数相除, 商会舍弃掉小数部分.
不论是有符号数还是无符号数, 若表达算术运算结果所需的位超出该类型的范围, 就称为溢出. 溢出的高位部分会无提示地丢弃.
实际上, 全部基本类型的值(布尔值, 数值, 字符串)都可以比较, 这意味着两个相同类型的值可用==
和!=
运算符比较. 整数, 浮点数和字符串还能根据比较运算符排序.
如果作为二元运算符, 运算符^
表示按位”异或”(XOR)
; 若作为一元前缀运算符, 则它表示按位取反或按位取补, 运算结果就是操作数逐位取反. 运算符&^
是按位清除(AND NOT)
: 表达式z=x&^y
中, 若y的某位是1, 则z的对应位等于0; 否则, 他就等于x的对应位.
在位移运算符x<<n
和x>>n
中, 操作数你决定位移量, 而且n必须为无符号型; 操作数x可以是有符号型也可以是无符号型. 算术上, 左移运算x<<n
等价于x乘以$2^n$; 而右移运算x>>n
等价于x除以2^n
, 向下取整.
左移以0填补右边空位, 无符号数右移同样以0填补左边空位, 但是有符号数的右移操作是按符号位的值填补空位. 因此, 当进行位运算的时候, 尽量使用无符号数.
因此, 无符号整数往往只用于位运算符和特定算术运算符, 如实现位集时, 解析二进制格式的文件, 或散列和加密. 一般而言, 无符号整数极少用于表示非负值.
通常, 将某种类型的值转换成另一种, 需要显式转换. 对于算术和逻辑(不含移位)的二元运算符, 其操作数的类型必须相同. 虽然这有时会导致表示式相对冗长, 但是一整类错误得以避免, 程序也更容易理解.
浮点型转成整形, 会舍弃小数部分, 趋零截尾(正值先下取整, 负值先上取整). 如果有些转换的操作数的值超出了目标类型的取值范围, 就应当避免这种转换, 因为其行为依赖具体实现.
任何大小的整数字面值都能写成十进制数, 也能写成八进制数(以0开头), 或十六进制(以0x或0x)开头.当前, 八进制数似乎仅有一种用途, 表示POSIX
文件系统的权限, 而十六进制数广泛用于强调其位模式, 而非数值大小.
字符面值通过一对单引号直接包含字符. 用%c
输出文字符号, 如果希望输出带有单引号则用%q
.
浮点数
Go
具有两种大小的浮点数float32
和float64
. 这两个类型的值可从超细微到超宏大. math
包给出了浮点值的极限.
十进制下, float32
的有效数字大约是6位, float64
的有效数字大约15位. 绝大多数情况下, 应优先选用float64
, 因为除非格外小心, 否则float32
的运算会迅速累积误差. 另外, float32
能精确表示的正整数范围有限.
小数点前的数字可以省略(.707)
, 后面的也可以省去(1.)
. 非常小或非常大的数字最好使用科学计数法表示, 此方法在数量级指数前写字母e
或E
.
使用Printf
函数的%g
输出, 该谓词会自动保持足够的精度, 并选择最简介的表示方式, 但是对于数据表, %e
(有指数)或%f
(无指数)的形式可能更合适.
除了大量常见的数学函数之外, math
包还有函数用于创建和判断IEEE 754
标准定义的特殊值: 正无穷大和负无穷大, 它表示超出最大许可值的数和除零的结果; 以及NAN
, 它表示数学意义上无意义的运算结果.
math.isNaN
函数判断其参数是否是非数值, math.NaN
函数则返回非数值. 在数字运算中, 我们倾向于将NaN
当作信号值, 不拿它来进行比较操作, 因为NaN
和任何数的比较操作都为false
(除了!=
为true
). 在浮点数中, NaN
, 正无穷大和负无穷大都不是唯一的, 每个都有非常多种的bit
模式表示.
如果一个函数返回的浮点数结果可能失败, 最好的做法是用单独的标志报告失败.
复数
Go
具有两种大小的复数complex64
和complex128
, 二者分别由float32
和float64
构成. 内置的complex
函数根据给定的实部和虚部创建复数, 内置的real
函数和imag
函数则分别提取复数的实部和虚部.
如果一个浮点数面值或一个十进制整数面值后面跟着一个i
, 例如3.14i
或2i
, 它将构成一个复数的虚部, 复数的实部是0.
可以用==
或!=
判断复数是否相等. 若两个复数的实部和虚部都相等, 则它们相等.
布尔值
bool
型的值或布尔值只有两种可能: 真true
和假false
. 布尔值可以和&&
和||
操作符结合, 并且会发生短路行为.
布尔值无法隐式转换成数值(如0或1), 反之也不行.
字符串
字符串是不可变的字节序列, 它可以包含任意数据, 包括0值字节. 习惯上, 文本字符串被解读成按UTF-8
编码的Unicode
码点(文字符号)序列.
内置的len
函数返回字符串的字节数(并非文字符号的数目), 下标访问操作s[i]
则取的第i
个字符, 其中$0 \le i \le len(s)$. 试图访问许可范围以外的字节会触发panic
异常.
子串生成操作s[i:j]
产生一个新字符串, 内容取自原字符串的字节, 下标从i
(含边界值)开始, 直到j
(不含边界值). 结果的大小是j-i
个字节, 若下标越界, 或者j
的值小于i
, 将触发panic
异常.
操作数i
与j
的默认值分别是0
(字符串起始位置)和len(s)
(字符串终止位置), 若省略i
或j
, 或两者, 则取默认值.
加号(+)
运算符连接两个字符串而生成一个新字符串.
字符串可以通过比较运算符做比较, 如==
和<
; 比较运算按字节进行, 结果服从本身字典排序.
字符串值无法改变, 不可变意味着两个字符串能安全地共用同一段底层内存, 使得复制任何长度字符串的开销都很低廉.
字符串字面量
字符串的值可以直接写成字符串字面量, 形式上就是带双引号的字节序列. 源码中的字符串也可以包含十六进制或八进制的任意字节.
原生的字符串字面量的书写形式使用反引号而不是双引号. 原生的字符串字面量内, 转义序列不起作用; 实质内容与字面写法严格一致, 包括反斜杠和换行符, 因此, 在程序源码中, 原生的字符串字面量可以展开多行. 唯一的特殊处理是回车符会被删除(换行符会保留), 使得同一字符串在所有平台上的值都是相同的.
正则表达式往往含有大量反斜杠, 可以方便地写成原生的字符串字面量. 原生的字面量也适用于HTML
模板, JSON
字面量, 命令行提示信息, 以及需要多行文本表达的场景.
Unicode
Unicode是一种字符集, 它攘括了世界上所有文书体系的全部字符, 对它们各自赋予一个叫Unicode码点的标准数字. 在Go
的术语中, 这些字符记号称为文字符号.
我们可以将文字符号的序列表示成int32
值序列, 这种表示方式称作UTF-32
或UCS-4
, 每个Unicode码点的编码长度相同, 都是32位.
UTF-8
UTF-8
以字节为单位对Unicode码点做变长编码. 每个文字字符用1~4
个字节表示, ASCII
字符的编码仅占1个字节, 而其他常用的文字字符的编码只是2或3个字节.
变长编码的字符串无法按下标直接访问第n个字符. 同时UTF-8
编码的顺序和Unicode码点的顺序一致, 因此可以直接排序UTF-8
编码.
Go
语言字符串字面值中的的Unicode转义字符让我们可以通过Unicode码点输入特殊的字符. 有两种形式, \uhhhh
表示16位码点值, \uhhhhhhhh
表示32位码点值, 其中每个h
代表要一个十六进制数值. 这两种形式都以UTF-8
编码表示给定的码点.
1 | "世界" |
码点值小于256的文字符号可以写成单个十六进制转义的形式, 如A
写成\x41
; 而更高的码点值则必须使用\u
或\U
转义. 这就导致'\xe4\xb8\x96\xe7\x95\x8c'
不是合法的文字符号, 虽然这三个字节构成了某个有效的UTF-8
编码的码点.
所幸, Go
的range
循环也适用于字符串, 按UTF-8
隐式解码. 注意, 对于非ASCII
文字符号, 下标增量大于1.
每次UTF-8
解码器读入一个不合理的字节, 无论是显式调用utf8.DecodeRuneInString
, 还是在range
循环内隐式读取, 都会产生一个专门的Unicode字符\uFFFD
替换它, 其输出通常是个黑色六角形或类似钻石的形状, 里面有个白色问号. 如果程序碰到这个文字符号值, 通常意味着, 生成字符串数据的系统上游部分在处理文本编码方面存在瑕疵.
UTF-8
是一种分外便捷的交互格式, 而在程序内部使用文字字符类型可能更加方便, 因为它们大小一致, 便于在数组和slice
中用下标访问.
当[]rune
转换作用于UTF-8
编码字符串时, 返回该字符串的Unicode
码点序列.
如果是将一个[]rune
类型的Unicode字符slice
或数组转为string
, 则对它们进行UTF8
编码.
将一个整数转型为字符串意思是生成以只包含对应Unicode码点字符的UTF8
字符串.
如果对应码点的字符是无效的, 则用\uFFFD
无效字符作为替换.
字符串和字节slice
4个标准包对字符串操作特别重要: bytes
, strings
, strconv
和unicode
.
strings
包提供了许多函数, 用于搜索, 替换, 比较, 截断, 拆分和连接字符串.
bytes
包也有类似的函数, 用于操作字节slice
. 由于字符串不可变, 因此按增量方式构建字符串会导致多次内存分配和复制. 在这种情况下, 使用bytes.Buffer
类型会更高效.
strconv
包具备的函数, 主要用于转换布尔值, 整数, 浮点数为与之对应的字符串形式, 或者把字符串转换为布尔值, 整数, 浮点数, 另外还提供了双引号转义相关的转换.
unicode
包具备判别文字符号特征的函数, 如IsDisgit
, IsLetter
, IsUpper
和IsLower
. 每个函数一以单个文字符号值作为参数, 并返回布尔值. 若文字符号是英文字母, 转换函数(如ToUpper
和ToLower
)将其转换成指定的大小写. 上面所有函数都遵循Unicode标准对字母数字等的分类原则. strings
包也有类似的函数, 函数名也是ToUpper
和ToLower
, 它们对原字符的每个字符做指定变换, 生成并返回一个新字符串.
path
包和path/filepath
包提供提取basename
的函数, 用来操作文件路径等具有层次结构的名字. path
包处理以斜杠/
分段的路径字符串, 不分平台. 它不适用于处理文件名, 却适合其他领域, 像URL
地址的路径部分. 相反地, path/filepath
包根据宿主平台的规则处理文件名. 例如POSIX
系统使用/
, 而Microsoft Windows
系统使用\
.
一个字符串是包含的只读字节数组, 一旦创建, 是不可变的. 相比之下, 一个字节slice
的元素则可以自由地修改.
从概念上讲, 一个[]byte(s)
转换是分配一个新的字节数组用于保存字符串数据的拷贝, 然后引用这个底层的字节数组. 编译器的优化可以避免在一些场景下分配和复制字符串数据, 但是总的来说需要确保slice
变量被修改的情况下, 原始的字符串也不会改变. 将一个字节slice
转到字符串string(b)
从操作则是构造一个字符串拷贝, 以确保字符串是只读的.
bytes
包还提供了Buffer
类型用于字节slice
的缓存. 一个Buufer
开始是空的, 但是随着string
, byte
或[]byte
等数据类型的写入可以动态增长, 一个bytes.Buffer
变量并不需要处理化, 因为零值也是有效的.
当向bytes.Buffer
添加任意字符的UTF0-8
编码时, 最好使用bytes.Buffer
的WriteRune
方法, 但是WriteByte
方法对写入类似[
和]
等ASCII
字符则会更加有效.
字符串和数字的相互转换
要将整数转换成字符串, 一种选择是使用fmt.Sprintf
, 另一种做法是用函数strconv.Itoa()
.
FomatInt
和FomatUint
可以按不同进制位格式化数字.
如要要将要给字符串解析位整数, 可以使用strconv
包的Atoi
或ParseInt
函数, 还有用于解析无符号整数的ParseUint
函数.
有时候也会使用fmt.Scanf
来解析输入的字符串和数字, 特别是当字符串和数字混在一行的时候, 它可以灵活处理不完整或不规则的输入.
常量
常量是一种表达式, 其可以保证在编译阶段就可以计算出表达式的值, 并不需要等到运行时, 从而使编译器以知晓其值. 所有常量本质上都是属于基本类型: 布尔型, 字符串或数字.
对于常量操作数, 所有数学运算, 逻辑运算和比较运算的结果依然是常量, 对常量的类型转换操作或以函数调用都是返回常量结果: len
, cap
, real
, imag
, complex
和unsafe.Sizeof
.
因为它们的值是在编译期就确定的, 因此常量可以是构成类型的一部分, 例如用于指定数组类型的长度.
常量声明可以同时指定类型和值, 如果没有显式指定类型, 则类型根据右边的表达式推断. 若同时声明一组变常量, 除了第一项之外, 其他项在等号右侧的表达式都可以省略, 这意味着会复用前面一项的表达式以及类型.
常量生成器iota
常量的声明可以使用常量生成器iota
, 它创建一系列相关值, 而不是逐个值显式写出. 常量声明中, iota
从0
开始取值, 逐项加1
.
无类型常量
常量可以任何基本数据类型, 也包括具名的基本类型, 但是许多常量并没有一个明确的基本类型. 编译器为这些没有明确的基础恶劣性的数字常量提供了比基础类型更高精度的算术运算; 你可以认为至少有256bit
的运算精度. 这里有六种未明确类型的常量类型, 分别是无类型的布尔型, 无类型的整数, 无类型的字符, 无类型的浮点数, 无类型的复数, 无类型的字符串.
通过延迟明确常量的具体类型, 无类型的常量不仅可以提供更高的运算精度, 而且可以直接用于更多的表达式而不需要显式地类型转换.
只有常量可以是无类型的. 当一个无类型的常量被赋值给一个变量的时候, 无类型的常量将会被隐式转换为对应的类型, 如果转换合法的话.
对于一个没有显式类型的变量声明语法(包括短变量声明语法), 无类型的常量会被隐式转为默认的变量类型.
常用的方法
1 | 打印8位的表示 |