第 2 章 基本数据类型
Go 的数值类型包括了不同大小的整数 、浮点数 、复数; 各种数值类型分别有自己的大小,对正负号支持也各不相同; |
1. 整数(OK)
整数类型(整型) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
整数类型 | Go 语言同时支持 有符号整数 、无符号整数 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
有符号整数 | 有符号整数分 4 种大小:8 位 、16 位 、32 位 、64 位
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
无符号整数 | 无符号整数也分 4 种大小:8 位 、16 位 、32 位 、64 位
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
平台 默认整型 | 还有两种类型
在特定平台上,其大小与原生的有符号整数 \ 无符号整数相同, 或等于该平台上的运算效率最高的值 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
" int " 是目前使用最广泛的数值类型 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
" int " 和 " uint " 在同一台机器上的大小相等,要么都是 32 位,要么都是 64 位; 但不能认为一定就是 32 位,或一定就是 64 位; 即使在同样的硬件平台上,不同的编译器可能选用不同的大小 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
两个类型 同义词 | " rune " 类型是 int32 类型的同义词 常用于指明一个值是 Unicode 码点(code point) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
" byte " 类型是 uint8 类型的同义词 强调一个值是原始数据,而非量值 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rune 和 int32 可以互换使用 byte 和 uint8 可以互换使用 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
特殊 无符号整数 | 还有一种无符号整数 " uintptr " ,其大小并不明确,但足以完整存放指针 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
" uintptr " 类型仅仅用于底层编程 例如,在 Go 程序与 C 程序库或操作系统的接口界面 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
类型有别 显式转换 | " int " 、" uint " 、" uintptr " 都有别于其他大小明确的相似类型的类型; 也就是说," int " 和 " int32 " 是不同类型,尽管 " int " 天然的大小就是 32 位 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
如果 " int " 值要当做 " int32 " 来使用,必须显式转换;反之亦然 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
取值范围 | 有符号整数在机器中以 " 补码 " 表示,保留最高位作为符号位; 位类型的取值范围是 ~ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
无符号整数由全部位构成非负数值; 位类型的取值范围是 ~ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
有符号 类型示例 | 有符号整型 int8 占 8 位,最高位是符号位,剩余 7 位表示数值; 表示范围是 -128 ~ 127 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
有符号类型的 三种表示 | 有符号整数用最高位作为符号位,0 代表正数,1 代表负数 此处用 int8 作为示例: 由于最高位作为符号位,只有 7 个位能用来表示数值 原码表示法: 0,000 0000 表示 +0 0,111 1111 表示 +127 1,000 0000 表示 -0 1,111 1111 表示 -127 反码表示法: 原码的符号位不变,剩余数值位,均取反;数值位进位,但不影响符号位 0,111 1111 表示 +0 0,000 0000 表示 +127 1,111 1111 表示 -0 1,000 0000 表示 -127 补码表示法: 符号位不变,反码加 1 0,111 1111 + 1 -> 0,000 0000 表示 +0 即为 0 0,000 0000 + 1 -> 0,000 0001 表示 +127 1,111 1111 + 1 -> 1,000 0000 表示 -0 ,不看符号位,值为 128 ,重定义为 -128 1,000 0000 + 1 -> 1,000 0001 表示 -127 从意义上看,-127 的二进制数减 1 后应该是 -128 ,也就是上面的 -0 , 所以将 -0 定义为 -128 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
无符号 类型示例 | 无符号整型 uint8 占 8 位,全部位数用来表示数值 0000 0000 :0 1111 1111 : 255 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
运算符 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
二元运算符 | Go 的二元操作符包括了 算术 、逻辑 、比较 等运算; 按优先级降序排列如下:左高右低,上高下低 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
运算符 优先级表 |
| |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
说明 | 如上表所示,二元运算符分五大优先级; 同级别的运算符满足左结合律,为求清晰,可能需要圆括号来指定计算次序; 示例: mask & (1 << 28) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
复合赋值 运算符 | 上述列表中前两行的运算符(如加法运算 +)都有对应的赋值运算符(如 +=), 用于简写赋值语句 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
运算符 的 应用场景 | 算术运算符 + 、- 、* 、/ 可用于整数 、浮点数 、复数 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
取模运算 | 取模运算符 % 只能用于整数 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
取模运算符的行为因编程语言而异 在 Go 语言中,取模余数的符号总是与被除数保持一致 示例: -5 % 3 = -1 ... -2 -5 % (-3) = 1 ... -2 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
除法运算 | 除法运算( / )的行为取决于操作数是否都为整数 整数相除,商会舍弃小数部分 示例: 5.0 / 4.0 -> 1.25 5 / 4 -> 1 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
溢出 | 无论是有符号数还是无符号数,若表示算术运算结果所需的位超出该类型的范围, 就称为 "溢出" 溢出的高位部分会无提示地丢弃 假如原本的计算结果是有符号类型,且最左侧位是 1 ,则会形成负值, 以 int8 为例: var u uint8 = 255 fmt.Ptintln(u, u+1, u*u) // " 255 0 1 " var i int8 = 127 fmt.Println(i, i+1, i*i) // " 127 -128 1 " | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
比较运算 | 下列二元比较运算符用于比较两个类型相同的整数; 比较表达式本身的类型是布尔型 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
(i). 实际上,所有基本类型的值(布尔值 、数值 、字符串)都可以比较, 这意味着两个相同类型的值,可以用 " == " 和 " != " 运算符比较 (ii). 整数 、浮点数 、字符串能根据比较运算进行排序 许多其他类型的值是不可比较的,也无法排序 后面介绍每种类型时,将会分别说明比较规则 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
一元运算 | Go 语言支持一元加法和一元减法运算符 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
对于整数,+x 是 0+x 的简写,而 -x 是 0-x 的简写; 对于浮点数和复数,+x 就是 x ,-x 是 x 的相反数 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
位运算 | Go 语言支持下列 " 位运算符 " | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
异或 ^ | (i). 如果作为二元运算符,运算符 " ^ " 表示按位 " 异或(XOR) " , 即两数相同返回 0 ,两数不同返回 1 (ii). 如果作为一元前缀运算符,运算符 " ^ " 表示按位取反或按位取补, 运算结果就是操作数逐位取反 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
与反 &^ | 运算符 " &^ " 的作用是按位清除(AND NOT) " &^ " 两侧操作数的对应位,先进行 " 与 " 操作,再执行 " 取反 " 操作 表达式 z = x &^ y 中,若 y 的某位是 1,则 z 的对应位等于 0 ; 否则,就等于 x 的对应位 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
位运算示例 | 下面的代码说明了如何用位运算,将一个 uint8 的值当作位集(bitset)处理, 其含有 8 个独立的位,高效且紧凑 Printf 用谓词 %b 以二进制形式输出数值, 副词 08 在这个输出结果前补零,补够 8 位 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
var x uint8 = 1 << 1 | 1 << 5 var y uint8 = 1 << 1 | 1 << 2 fmt.Printf("%08b\n",x) // "00100010",集合 {1,5} fmt.Printf("%08b\n",y) // "00000110",集合 {1,2} fmt.Printf("%08b\n",x&y) // "00000010",交集 {1} fmt.Printf("%08b\n",x|y) // "00100110",并集 {1,2,5} fmt.Printf("%08b\n",x^y) // "00100100",对称差 {2,5} fmt.Printf("%08b\n",x&^y) // "00100000",差集 {5} for i := uint(0);i < 8;i++ { if x&(1<<i) != 0 { // 元素判定 fmt.Printf(i) // "1" "5" } } fmt.Printf("%08b\n",x<<1) // "01000100",集合 {2,6} fmt.Printf("%08b\n",x>>1) // "00010001",集合 {0,4} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
左移 << 右移 >> | 在移位运算 x << n 和 x >> n 中: (i). 操作数 n 决定操作数 x(二进制形式)左移/右移的位数,且 n 必须为无符号整型 (ii). 操作数 x 可以是有符号整型,也可以是无符号整型 (iii). 算术意义上 : 左移运算 x << n 等价于 x * 2^n 右移运算 x >> n 等价于 x / (2^n) ,最终结果舍弃小数部分 (iv). 左移用 0 填补右边空位; 无符号整数右移用 0 填补左边空位,有符号整数右移用符号位的值填补左边空位 注意:如果将整数以位模式进行处理,须使用无符号整型 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
说明 | 尽管 Go 具备无符号整型和相关算术运算,也尽管某些量值不可能为负, 但是我们往往采用有符号整型数,如数组的长度(尽管长度不可能为负) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
示例 | 下面例子从后往前输出奖牌名称,循环里面用到了内置的 len 函数, len 返回有符号整数 medals := []string{ "gold","silver","bronze" } for i := len(medals) - 1;i >= 0;i-- { fmt.Println(medals[i]) // "bronze","silver","gold" } | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
无符号整型 运算易导致 严重错误 | 相反,假如 len 返回的结果是无符号整型,就会导致严重错误, 因为 i 随之也成为无符号整型,而根据定义,条件 i >= 0 将恒成立; 第 3 轮迭代后,有 i == 0 ,语句 i-- 使得 i 变成无符号整型的最大值,比如 而非 -1 ; 导致 medals[i] 试图越界访问元素,超出 slice 范围,引发运行失败或宕机 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Tips | 无符号整数往往只用于位运算符和特定算术运算符,如实现位集 、 解析二进制格式的文件 、散列 、加密 一般而言,无符号整数极少用于表示非负值 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
类型转换 | 通常,将某种类型的值转换成另一种类型,需要显示转换 算术和逻辑(不含移位)的二元运算符,其操作数的类型必须相同 虽然这样有时会导致表达式相对冗长,但是一个系列的错误得以避免,程序更易理解 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
示例 | var apples int32 = 1 var oranges int16 = 2 var compote int = apples + oranges // 编译错误:不同类型无法在一起运算 尝试编译这三个声明将产生错误消息: 非法操作:apples + oranges ( int32 与 int16 类型不匹配 ) 类型不匹配(+ 的问题)有几种方法修正,最直接地,将全部操作数转换成同一类型: var compote = int(apples) + int(oranges) | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
类型转换 | 对于某种类型 T ,若允许转换,操作 T(x) 会将 x 的值转换成类型 T; var x X = x1 var t T = t1 t = T(x) 很多整型 - 整型转换( int16 <-> int32 )不会引起值的变化, 仅告知编译器如何解读该值; 不过,缩减大小的整型转换( int64 -> int16 ), 以及整型与浮点型的相互转换( int64 <-> float64 ), 可能会改变值或损失精度 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
示例 | f := 3.141 // a float64 i := int(f) fmt.Println(f,i) // " 3.141 3 " f = 1.99 fmt.Println(int(f)) // " 1 " | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
类型转换 | 浮点型转换为整型,会舍弃小数部分,即趋零截尾(正值向下取整,负值向上取整),最终的结果相比之前,都更接近零 如果有些转换的操作数的值,超出了目标类型的表示范围 ( float64 -> int8 ), 就应当避免这种转换,因为其行为依赖于具体实现 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
示例 | f := 1e100 // a float64 i := int(f) // 结果依赖于具体实现 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
整型 的 三种进制 | 无论有无大小和符号限制 : (i). 源码中的整数都能写成常见的十进制数; (ii). 也能写成八进制数,以 0 开头,如 0888 (iii). 还能写成十六进制数,以 0x 或 0X 开头,如 0xdeadBEEF 十六进制数中的 x 或 a 到 f ,大小写皆可 (iv). 当前,八进制数似乎只有一种用途 -- 表示 POSIX 文件系统的权限 而十六进制数,广泛用于强调其位模式,而非数值大小 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
示例 | 如下面的例子所示,如果使用 fmt 包输出数字,可以用谓词 %d 、%o 、%x 指定 进位制基数和输出格式: o := 0666 fmt.Printf("%d %[1]o %#[1]o\n",o) // " 438 666 0666 " x := int64(0xdeadbeef) fmt.Printf("%d %[1]x %#[1]X\n",x) // 输出: // 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
说明 | 注意 fmt 的两个技巧: (i). 通常 Printf 的格式化字符串含有多个 % 谓词,这要求提供相同数目的操作数, 而 % 后面的副词 [1] 告诉 Printf 函数,重复使用第一个操作数 (ii). 其次,%o 、%x 或 %X 之前的副词 # ,则告诉函数 Printf 输出相应的前缀 0 、 0x 、0X | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
说明 | 在源码中,文字符号(rune literal)的形式是字符写在一对单引号内; 最简单的例子就是 ASCII 码字符,如 'a' ,但也可以直接使用 Unicode 码点(codepoint)或码值转义 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
示例 | 用 %c 输出文字符号,如果希望输出带有单引号,则用 %q ascii := 'a' unicode := '国' newline := '\n' fmt.Printf("%d %[1]c %[1]q\n",ascii) // " 97 a 'a' " fmt.Printf("%d %[1]c %[1]q\n",unicode) // " 22269 国 '国' " fmt.Printf("%d %[1]q\n",newline) // " 10 '\n' " |
2. 浮点数
浮点数类型 | |
两种浮点类型 | Go 语言支持两种大小的浮点数 :float32 和 float64 |
其算术特性遵循从 IEEE 754 标准,所有新式 CPU 都支持该标准 | |
浮点型 表示范围 | 这两个类型的值可从极细微到超宏大 |
math 包给出了浮点值的极限; 常量 math.MaxFloat32 是 float32 的最大值,大约为 3.4e38,最小值为 1.4e-45 常量 math.MaxFloat64 是 float64 的最大值,大约为 1.8e308,最小值为 4.9e-324 | |
十进制下,float32 的有效数字大约是 6 位,float64 的有效数字大约是 15 位; 绝大多数情况下,应优先选用 float64 ,因为除非格外小心,否则 float32 的运算 会迅速累积误差; 另外,float32 能精确表示的正整数范围有限: | |
示例 | var f float32 = 16777216 // 1 << 24 fmt.Println( f == f+1 ) // "true" 在源码中,浮点数可以写成小数,如: const e = 2.71828 // ( 近似值 ) |
特别技巧 | (i). 小数点前的数字可以省略( .707 即 0.707),小数点后面的也可以省略( 1. ) 省去的部分都为 0 (ii). 非常小或非常大的数字最好使用科学计数法表示,此方法在数量级指数前 写字母 e 或 E const Avogadro = 6.02214129e23 const Planck = 6.62606957e-34 |
浮点数的 格式化输出 | 浮点值能方便地通过 Printf 的谓词 %g 输出,该谓词会自动保持足够的精度, 并选择最简洁的表示方式,但是对于数据表,%e(有指数)或 %f(无指数)的形式可能更合适。 这三个谓词( %g 、%e 、%f )都能掌控输出宽度和数值精度 %n.xf n 表示输出一个占多少个位的长度,x 表示小数点后保留多少位 |
示例 | for x := 0;x < 8;x++ { fmt.Ptintf("x = %d = %8.3f",x,math.Exp(float64(x))) } 上面的代码按 8 个字符的宽度输出自然对数 e 的各个幂方,结果保留三位小数 x = 0 = 1.000 x = 1 = 2.718 x = 2 = 7.389 x = 3 = 20.086 x = 4 = 54.598 x = 5 = 148.413 x = 6 = 403.429 x = 7 = 1096.633 |
特殊值 | 除了大量常见的数学函数之外,math 包还有一些函数,用于创建和判断 IEEE 754 标准定义的特殊值: 正无穷大和负无穷大,表示超出最大许可值的数 、以及除以零的商 NaN(Not a Number),它表示数学上无意义的运算结果(如 0/0 或 Sqrt(-1) ) |
math.IsNaN 函数判断其参数是否为非数值,math.Nan 函数则返回非数值( NaN ) 在数字运算中,我们倾向于将 NaN 当做信号值( sentinel value ),但直接判断具体的计算结果是否为 NaN 可能导致潜在错误,因为与 NaN 的比较总不成立(除了 != ,它总是与 == 相反) | |
示例 | nan := math.Nan() fmt.Println(nan == nan,nan < nan,nan > nan) // "false false false" |
特别注意 | 一个函数的返回值是浮点型且它有可能出错,那么最好单独报错,如下: |
示例 | func compute() (value float64,ok bool) { // ... if failed { return 0,false } return result,true } |
下面的程序以浮点绘图运算为例;它根据传入两个参数的函数 z = f(x,y),绘出 三维的网线状曲面,绘制过程中运用了可缩放矢量图形(Scalabe Vector Graphics,SVG),绘制线条的一种标准 XML 格式;图 3-1 是函数 sin(r)/r 的图形输出样例, 其中 r 为 sqrt(x*x + y*y) | |
代码 | // surface 函数根据一个三维曲面函数计算并生成 SVG package main import ( "fmt" "math" ) const ( width,height = 600,320 // 以像素表示的画布大小 cells = 100 // 网格单元的个数 xyrange = 30.0 // 坐标轴的范围(-xyrange ~ +xyrange) xyscale = width / 2 / xyrange // x 或 y 轴上每个单位长度的像素 zscale = height * 0.4 // z 轴上每个单位长度的像素 angle = math.Pi / 6 // x 、y 轴的角度( =30 ) ) var sin30,cos30 = math.Sin(angle),math.Cos(angel) // sin(30),cos(30) func main() {
|
3. 复数(OK)
复数类型 | |
两种复数类型 | Go 语言中内置了两种大小的复数 complex64 和 complex128 , 两者分别由 float32 和 float64 构成 |
complex | 内置的 complex 函数,根据给定的实部和虚部创建复数 |
real 、imag | 内置的 real 函数和 imag 函数,则分别提取复数的实部和虚部 |
示例 | 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" |
虚数 | 如果在浮点数或十进制整数后面紧接着字母 " i " ,如 3.1415926i 或 2i , 它就变成了一个虚数,表示一个实部为 0 的复数 |
fmt.Println( 1i * 1i ) // " (-1 + 0i) " , | |
复数常量 | 根据常量运算规则,复数常量可以和其他常量相加(整型或浮点型,实数和虚数皆可) |
我们可以自然地写出复数,如 1+2i ,或等价地,2i+1 前面 x 和 y 的声明可以简写为: x := 1 + 2i y := 3 + 4i | |
相等性比较 | 可以用 " == " 或 " != " 判断两个复数是否相等; 若两个复数的实部相等且虚部相等,则这两个复数相等 |
库扩展 | math / cmplx 包提供了复数运算所需的库函数; 例如,复数的平方根函数 、复数的幂函数 |
示例:下面的程序用 complex128 运算生成一个 Mandelbrot 集
// madelbrot 函数生成一个 PNG 格式的 Mandelbrot 分形图
package mainimport ("image""image/color""image/png""math/cmplx""os"
)func main() {const (xmin, ymin, xmax, ymax = -2, -2, +2, +2width, height = 1024, 1024)img := image.NewRGBA(image.Rect(0, 0, width, height))for py := 0; py < height; py++ {y := float64(py)/height * (ymax-ymin) + yminfor px := 0; px < width; px++ {x := float64(px)/width * (xmax-xmin) + xminz := complex(x, y)// 点 (px, py) 表示复数数值 zimg.Set(px, py, mandelbrot(z))}}png.Encode(os.Stdout, img)
}func mandelbrot(z complex128) color.Color {const iterations = 200const contrast = 15var v complex128for n := uint8(0); n < iterations; n++ {v = v*v + zif cmplx.Abs(v) > 2 {return color.Gray(255 - contrast*n)}}return color.Black
}
4. 布尔值(OK)
布尔类型 | |
布尔类型声明 | var a bool var b bool = false |
取值范围 | true(真)或 false(假) |
使用场景 | if 和 for 语句中的条件就是布尔值, 比较操作符(如 == 和 < )也能得出布尔值结果 |
取反操作 | 一元操作符(!)表示逻辑取反,因此 !true 就是 false , 或者 (!true == false) == true |
判断简写 | 考虑到代码风格,布尔表达式 x == true 相对冗长,简化为 x if (x == true) 替换为 if (x) ,其中 x 是布尔类型 |
短路概念 | 布尔值可以由运算符 &&(AND)以及 ||(OR)组合运算,这会引起短路行为: 如果逻辑运算符左侧的表达式已经能确定最终结果, 那么逻辑运算符右侧的表达式不会被再次计算 |
逻辑运算规则 | &&(与)运算规则:只要有一个表达式结果为假,则最终的结果就是假 || (或)运算规则:只要有一个表达式结果为真,则最终的结果就是真 |
逻辑运算示例 | 下面的表达式是安全的: s != "" && s[0] == 'x' 若字符串 s 为空字符串,则 s != "" 表达式返回 false , 则不会再判断 s[0] == 'x' 若 s 为空字符串,像 s[0] == 'x' 这样使用 s[0] ,会触发宕机异常 |
优先级 | && 较 || 优先级更高(助记窍门:&& 表示逻辑乘法,|| 表示逻辑加法) |
优先级示例 | 所以如下形式的条件组合无须加圆括号: if 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' { // ... } |
零值 | 布尔类型的零值(默认值)为 false |
布尔类型无法与 其他类型相互转换 | 布尔值无法隐式转换成数值(如 true 转化为 1 ,false 转换为 0); 数值也无法隐式转换成布尔值(如 0 转换为 false ,非 0 数如 5 转换为 true) 总结:布尔型无法参与数值运算 |
不允许强制将数值类型转换为布尔型,不允许强制将布尔型转换为数值类型 | |
布尔型无法与其他类型相互转换 | |
简单解决办法 | i := 0 if b { i = 1 } 说明:用变量 i 的值代替布尔变量 b 的值 布尔变量 b 不能参与数值运算,根据 b 的 true 还是 false 决定 i 的值为 1 还是 0 让整型变量 i 参与数值运算 |
专用转换函数 | 假如转换操作常常会用到,那就应该专门为此写个函数 // 如果 b 为真,btoi 返回 1 ;如果 b 为假,btoi 返回 0 // 布尔值转数值 func btoi(b bool) int { if b { return 1 } return 0 } |
// 数值转布尔值 func itob(i int) bool { return i != 0 } |
5. 字符串
字符串 | |
字符串概念 | 字符串是不可变的字节序列,它可以包含任意数据,包括 0 值字节, 但主要是人类可读的文本 |
习惯上,文本字符串被解读成按 UTF-8 编码的 Unicode 码点(文字符号)序列 | |
字符串操作 | 内置的 函数返回字符串的字节数(并非文字符号的数目) |
下标访问操作 则取得第 个字符,其中 | |
示例 | s := "hello,world" fmt.Println(len(s)) // " 12 " fmt.Println(s[0],s[7]) // " 104 119 " ('h' and 'w') |
越界访问 触发宕机 | 试图访问许可范围以外的字节,会触发宕机异常 |
示例 | c := s[len(s)] // 宕机 :下标越界 |
特别注意 | 字符串的第 个字节,不一定就是第 个字符, 因为非 ASCII 字符的 UTF-8 码点需要两个字节或多个字节 |
提取子串 切片操作 | 子串生成操作 s[i:j] 产生一个新字符串,内容取自原字符串的字节,下标从 (含边界值)开始,直到 (不含边界值),即 ;子串的大小是 个字节 |
示例 | fmt.Println(s[0:5]) // "hello" |
再次强调 | 若下标越界,或者 的值小于 ,将触发宕机异常 |
子串默认 起始位置 | 操作数 与 的默认值分别是 (字符串起始位置)和 (字符串终止位置), 若省略 或 ,或两者都省略,则取默认值 |
示例 | fmt.Println(s[:5]) // "hello" fmt.Println(s[7:]) // "world" fmt.Println(s[:]) // "hello,world" |
字符串连接 | 加号( + )运算符,连接两个字符串而生成一个新字符串 |
示例 | fmt.Println("goodbye" + s[5:]) // "goodbye,world" |
字符串比较 | 字符串可以通过比较运算符,来比较两个字符串,如 " == " 和 " < " ; 比较运算按字节进行比较,结果服从本身的字典排序 |
字符串 不可改变 | 尽管肯定可以将新值赋予字符串变量,但是字符串值无法改变: 字符串值本身所包含的字节序列永远不会变(参考 python 字符串性质) |
因为字符串不可改变,所以字符串内部的数据不允许修改: s[0] = 'L' // 编译错误:s[0] 无法赋值 | |
字符串 追加 | 要在一个字符串后面添加另一个字符串,可以像下面这样编写代码: |
s := "left foot" t := s s += ",right foot" | |
上面这样的操作,不会改变 原有的字符串值,只是将 " += " 语句生成的新字符串 赋值给 ;同时, 仍然持有旧的字符串值 | |
fmt.Println(s) // " left foot,right foot " fmt.Println(t) // " left foot " | |
共用 底层内存 | 不可改变意味着两个字符串能安全地共用同一段底层内存,使得复制任何长度字符串 的开销都低廉;类似地,字符串 s 及其子串(如 s[7:] )可以安全地共用数据,因此, 子串生成操作的开销也低廉;这两种情况(复制字符串 、提取子串)都没有分配内存 |
3.5.1 字符串字面量 | |
字符串 字面量 | 字符串的值,可以直接写成字符串字面量(string literal),形式上就是: 带双引号的字节序列 |
示例 | "Hello,世界" |
任意字符 | 因为 Go 的源文件总是按 UTF-8 编码,并且习惯上 Go 的字符串会按 UTF-8 解读, 所以在源码中,可以将 Unicode 码点写入字符串字面量 var s string = "我们のworld" |
转义字符 | 在带双引号的字符串字面量中,转义序列以反斜杠( \ )开始,可以将任意值的字节 插入字符串中; |
下面是一组转义符,表示 ASCII 控制码,如换行符 、回车符 、制表符 \a "警告" 或响铃 \b 退格符 \f 换页符 \n 换行符(直接跳到下一行的同一位置) \r 回车符(返回行首) \t 制表符 \v 垂直制表符 \' 单引号(仅用于文字字符字面量 ' \' ') \" 双引号(仅用于 "..." 字面量内部) \\ 反斜杠 | |
其他进制 数字字节 | 源码中的字符串也可以包含十六进制或八进制的任意字节 |
十六进制的转义字符写成 ' \xhh ' 的形式,' h ' 是十六进制数字(大小写皆可以), 且必须是两位 | |
八进制的转义字符写成 ' \ooo ' 的形式,必须使用三位八进制数字(0 ~ 7), 且不能超过 '\377 ' | |
这两者都表示单个字节,内容是给定值 | |
原生字符串 | 原生的字符串字面量的书写形式是 `...` ,使用反引号而不是双引号; |
原生的字符串字面量中,转义序列不起作用 (i). 也就是说,实质内容与字面写法严格一致,包括反斜杠和换行符 (ii). 因此,在程序源码中,原生的字符串字面量可以展开多行 (iii). 唯一的特殊处理是回车符会被删除(换行符会保留),使得同一字符串在所有 平台上的值都相同,包括习惯在文本文件存入换行符的系统 | |
使用场景 | 正则表达式往往含有大量反斜杠,可以方便地写成原生的字符串字面量; 原生的字符串字面量也适用于 HTML 模板 、JSON 字面量 、命令行提示信息 , 以及需要多行文本表达的场景 |
多行文本 | const GoUsage = `Go is a tool managing Go source code. Usage: go command [arguments] ...` |
3.5.2 Unicode | |
背景与问题 | 此前,软件只须处理一个字符集:ASCII(美国信息交换标准码) ASCII( 或更确切地说,US-ASCII )码使用 7 位来表示 128 个 "字符" : 大小写英文字母 、数字 、各种标点 、设备控制符 这对早期的计算机行业已经足够了,但是让世界上众多使用其他语言的人无法在计算机 上使用自己的文字书写体系;随着互联网的兴起,包含纷繁语言的数据屡见不鲜; 到底怎样才能应付语言的繁杂多样,还能兼顾效率? |
解决方法 与 新问题 | 答案是 Unicode(unicode.org),它囊括了世界上所有文字书写体系的全部字符,还有重音符和其他变音符,控制码(如制表符和回车符),以及许多特有文字,对它们各自赋予一个叫 Unicode 码点的标准数字; 在 Go 的术语中,这些字符记号称为文字符号( rune ) |
Unicode 第 8 版定义了超过一百种语言文字的 12 万个字符的码点; 它们在计算机程序和数据中如何表示?天然适合保存单个文字符号的数据类型就是 int32 ,被 Go 所采用;正因如此,runne 类型作为了 int32 类型的别名 | |
我们可以将文字符号的序列表示成 int32 值序列,这种表示方式称作 UTF-32 或 UCS-4 ,每个 Unicode 码点的编码长度相同,都是 32 位;这种编码简单划一, 可能因为大多数面向计算机的可读文本是 ASCII 码,每个字符只需 8 位,也就是 1 字节,导致了不必要的存储空间的消耗; 而广泛使用的字符数目也少于 65536 个,字符用 16 位就能容纳,我们能做改进吗? | |
3.5.3 UTF-8 | |
UTF-8 编码 | UTF-8 以字节为单位对 Unicode 码点作变长编码 |
UTF-8 是现行的一种 Unicode 标准, 由 Go 的两位创建者 Ken Thompson 和 Rob Pike 发明 | |
每个文字符号用 1 ~ 4 个字节表示,ASCII 字符的编码仅占 1 个字节, 而其他常用的文字字符的编码只占 2 或 3 个字节 | |
一个文字符号编码的首字母的高位,指明了后面还有多少字节; (i). 若最高位为 0 ,则标示着它是 7 位的 ASCII 码,其文字符号的编码仅占 1 字节,这样就与传统的 ASCII 码一致 (ii). 若最高几位是 110 ,则文字符号的编码占用 2 个字节,第二个字节以 10 开始; 更长的编码以此类推 | |
0xxxxxxx 文字符号 0 ~ 127(ASCII) 110xxxxx 10xxxxxx 128 ~ 2047 少于 128 个未使用的值 1110xxxx 10xxxxxx 10xxxxxx 2048 ~ 65535 少于 2048 个未使用的值 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 65536 ~ 0x10ffff 其他未使用的值 | |
缺点 | 变长编码的字符串无法按下标直接访问第 个字符 |
优点 | 然而有失有得,UTF-8 换来许多有用的特性: (i). UTF-8 编码紧凑,兼容 ASCII (ii). 自同步 :最多追溯 3 个字节,就能定位一个字符的起始位置 (iii). 前缀编码,能从左向右编码而不产生歧义,也无须超前预读; 于是查找文字符号仅需搜索它自身的字节,不必考虑前文内容 (iv). 文字符号的字典字节顺序与 Unicode 码点顺序一致(Unicode 设计如此), 因此按 UTF-8 编码排序自然就是对文字符号排序 (v). UTF-8 编码本身不会嵌入 NUL 字节( 0 值 ),这便于某些程序语言用 NUL 标记字符串结尾 |
Go 对 UTF-8 的支持 | Go 的源文件总是以 UTF-8 编码,同时,需要用 Go 程序操作的文本字符串也优先采用 UTF-8 编码; unicode 包具备针对单个文字符号的函数(例如区分字母和数字,转换大小写), 而 unicode/utf8 包,则提供了按 UTF-8 编码和解码文字符号的函数 |
用码点表示 Unicode字符 | 许多 Unicode 字符难以直接从键盘输入;有的看起来十分相似几乎无法辨认; 有些甚至不可见 在 Go 语言中,字符串字面量的转义让我们得以用码点的值来指明 Unicode 字符; 有两种形式 : (i). \uhhhh 表示 16 位码点值 (ii). \uhhhhhhhh 表示 32 位码点值 其中,每个 h 代表一个十六进制数字;32 位形式的码点值几乎不需要用到 这两种形式都以 UTF-8 编码表示出给定的码点 |
示例 | 因此,下面几个字符串字面量都表示长度为 6 个字节的相同串: "世界" "\xe4\xb7\x96\xe7\x95\x8c" "\u4e16\u754c" "\U00004e16\U0000754c" 后面三行的转义序列用不同形式表示第一行的字符串,但实质上它们的字符串值都一样 Unicode 转义符也能用于文字符号;下列字符是等价的: '世' '\u4e16' '\U00004e16' |
特别注意 | 码点值小于 256 的文字符号可以写成单个十六进制数转义的形式,如 'A' 写成 '\x41' , 而更高的码点值则必须使用 \u 或 \U 转义 这就导致,'\xe4\xb8\x96' 不是合法的文字符号,虽然这三个字节构成某个有效的 UTF-8 编码码点 |
UTF-8 的 操作示例 | 由于 UTF-8 的优良特性,许多字符串操作都无须解码; |
// 我们可以直接判断某个字符串是否为另一个的前缀: func HasPrefix( s ,prefix string ) bool { return len(s) >= len(prefix) && s[:len(prefix)] == prefix } // 是否为另一个字符串的后缀 func HasSuffix( s ,suffix string ) bool { return len(s) >= len(suffix) && s[len(s) - len(suffix) :] == suffix } // 是否为另一个字符串的子串 func Contains( s ,substr string ) bool { for i := 0;i < len(s);i++ { if HasPrefix( s[i:] ,substr ) { return true } } return false }
| |
说明 | 按 UTF-8 编码的文本的逻辑同样也适用于原生字节序列,但其他编码则无法如此; (上面的函数取自 strings 包,其实 Contains 函数的具体实现,使用了散列方法,让搜索更高效) |
另一方面,如果我们真的要逐个逐个处理 Unicode 字符,则必须使用其他编码机制; 考虑到我们第一个例子的字符串 "世界" ,该字符串包含两个中文字符; 图 3-5 说明了该字符串的内存布局; 它含有 13 个字节,而按作 UTF-8 解读,本质是 9 个码点或文字符号的编码: | |
import "unicode/utf8" s := "Hello,世界" fmt.Println(len(s)) // "13" fmt.Println(utf8.RuneCountInString(s)) // "9" 我们需要 UTF-8 解码器来处理这些字符,unicode/utf8 包就具备一个: for i := 0;i < len(s);{ r,size := utf8.DecodeEuneInString(s[i:]) fmt.Printf("%d\t%c\n",i,r) i += size } 说明: 每次 DecodeRuneInString 的调用都返回 r(文字符号本身) 和一个值 offset(表示 r 按 UTF-8 编码所占用的字节数) offset 这个值用来更新下标 i ,定位字符串内下一个文字符号; 可是按此方法,我们总是需要使用上例中的循环方式; 所幸,Go 的 range 循环也适用于字符串,按 UTF-8 隐式解码; 图 3-5 也展示了以下循环的输出;注意,对于非 ASCII 文字符号,下标增量大于 1 | |
for i,r := range "Hello,世界" { fmt.Printf("%d\t%q\t%d\n",i,r,r) } // 我们可以用简单的 range 循环统计字符串中的文字符号数目,如下所示 n := 0 for _,_ = range s { n++ } // 与其他形式的 range 循环一样,可以忽略没用的变量 n := 0 for range s { n++ } // 或者,直截了当地调用 utf8.RuneCountInString(s) | |
之前提到过,文本字符串作为按 UTF-8 编码的 Unicode 码点序列解读,很大程度上是出于习惯,但为了确保使用 range 循环能正确处理字符串,则必须要求而不仅仅是按照习惯;如果字符串含有任意二进制数,也就是说,UTF-8 数据出错,而我们对它做 range 循环,会发生什么? | |
每次 UTF-8 解码器读入一个不合理的字节,无论是显式调用 utf8.DecodeRuneInString ,还是在 range 循环内隐式读取,都会产生一个专门的 Unicode 字符 '\uFFFD' 替换它,其输出通常是个黑色六角形或类似钻石的形状,里面有个白色问号; 如果程序碰到这个文字符号值,通常意味着,生成字符串数据的系统上游部分在处理文本编码方面存在瑕疵 | |
UTF-8 是一种分外便捷的交互格式,而在程序内部使用文字字符类型可能更加方便,因为它们大小一致,便于在数组和 slice 中用下标访问 | |
当 []rune 转换作用于 UTF-8 编码的字符串时,返回该字符串的 Unicode 码点序列: | |
// 日语片假名 "程序" s := "プログラム" fmt.Printf("% x\n",s) // "e3 83 97 e3 83 ad e3 82 b0 e3 83 a9 e3 83 a0" r := []rune(s) fmt.Printf("%x\n",r) // "[30d7 30ed 30b0 30e9 30e0]" | |
(第一个 Printf 里的谓词 %x (注意,% 和 x 之间有空格)以十六进制数形式输出,并在每两个数位间插入空格) | |
如果把文字符号类型的 slice 转换成一个字符串,它会输出各个文字符号的 UTF-8 编码拼接结果: fmt.Println(string(r)) "プログラム" | |
若将一个整数值转换成字符串,其值按文字符号类型解读,并且产生代表该文字符号值的 UTF-8 码: fmt.Println(string(65)) // "A",而不是 "65" fmt.Println(string(0x4eac)) // "京" | |
如果文字符号值非法,将被专门的替换字符取代(见前面的 '\uFFFD') fmt.Println(string(1234567)) // "�" | |
3.5.4 字符串和字节 slice | |
4 个标准 字符串包 | 4 个标准包对字符串操作特别重要:strings 、bytes 、strconv 、unicode |
strings 包 | strings 包提供了许多函数,用于搜索 、替换 、比较 、修整 、切分 、连接字符串 |
bytes 包 | bytes 包也有 strings 包中类似的函数,用于操作字节 slice( []byte 类型,其某些属性和字符串相同 );由于字符串不可变,因此按增量方式构建字符串会导致多次内存分配和复制;这种情况下,使用 bytes.Buffer 类型会更高效 |
strconv 包 | strconv 包具备的函数,主要用于转换布尔值 、整数 、浮点数 为与之对应的字符串形式,或者把字符串转换为 布尔值 、整数 、浮点数,另外还有为字符串添加 / 去除引号的函数 |
unicode 包 | unicode 包备有判别文字符号值特性的函数,如 IsDigit、IsLetter、IsUpper 、IsLower 每个函数以单个文字符号值作为参数,并返回布尔值; 若文字符号值是英文字母,转换函数(如 ToUpper 和 ToLower)将其转换成指定的大小写; 上面所有函数都遵循 Unicode 标准,对字母数字等的分类原则; strings 包也有类似的函数,函数名也是 ToUpper 和 ToLower ,它们对原字符串的每个字符做指定变换,生成并返回一个新字符串 |
下例中,basename 函数模仿 UNIX shell 中的同名实用程序;只要 s 的前缀看起来像是文件系统路径(各部分由斜杠分隔),该版本的 basename(s) 就将其移除,貌似文件类型的后缀也被移除: | |
fmt.Println(basename("a/b/c.go")) // "c" fmt.Println(basename("c.d.go")) // "c.d" fmt.Println(basename("abc")) // "abc" 初版的 basename 独自完成全部工作,并不依赖任何库: // gop.io/ch3/basename1 // basename 移除路径部分和 . 后缀 // e.g. ,a => a ,a.go => a , a/b/c.go => c ,a/b.c.go => b.c func basename( s string ) string { // } | |
6. 常量
常量是一种表达式,其可以保证在编译阶段就计算出表达式的值 | |
所有常量本质上都属于基本类型,如 :布尔型 、字符串 、数字 | |
常量的声明定义了具名的值(命名对象,其值一旦确定,就不可修改),语法上与变量类似,防止了程序运行过程中的意外(或恶意)修改 例如,姚表示数学常量,像圆周率,在 Go 程序中用常量表示比变量更适合,因为值是恒定不变的 | |
常量声明 | 声明单个常量 : const pi = 3.1415926 // 近似数;math.Pi 是更精准的近似 |
声明一组常量 : 与变量类似,同一个声明可以定义一系列常量,适用于一组相关的值; const ( e = 2.718281828 pi = 3.1415926 ) | |
为什么使用常量? | 许多针对常量的计算完全可以在编译阶段完成,从而减少运行时的工作量,并让其他编译器优化得以实现; 某些错误通常要在运行时才能检测到,但如果操作数是常量,编译时就会报错,例如整数除以 0 ,字符串下标越界,以及任何产生无限大值的浮点数运算 |
1. 对于常量操作数,所有数学运算 、逻辑运算 、比较运算的结果依旧是常量; 2. 常量的类型转换结果还是常量; 3. 某些内置函数的返回值是常量,如 len 、cap 、real 、imag 、complex 、unsafe.Sizeof 等 | |
因为编译器知道常量(表达式)的值,所以常量表达式可以出现在涉及类型的声明中,主要就是数组类型的长度: const IPv4Len = 4 // parseIPv4 函数解析一个 IPv4 地址(d.d.d.d) func parseIPv4(s string) IP { var p [IPv4Len]byte // ... } | |
常量声明可以同时指定类型和值,如果没有显式指定类型,则类型根据右边的表达式推断(有点类似使用 var 关键字声明变量) var a int = 5 var b int var c = 10 fmt.Println(b) // " 0 " fmt.Printf("%T\n", c) // " int " 下面的例子中,time.Duration 是一种具名类型,其基本类型是 int64 ,time.Minute 也是基于 int64 的常量;下面声明的两个常量都属于 time.Duration 类型,通过 %T 展示 const noDelay time.Duration = 0 const timeout = 5 * time.Minute fmt.Printf("%T %[1]v\n",noDelay) // "time.Duration 0" fmt.Printf("%T %[1]v\n",timeout) // "time.Duration 5m0s" fmt.Printf("%T %[1]v\n",time.Minute) // "time.Duration 10m0s" | |
若同时声明一组常量,除了第一项之外,其他项在等号右侧的表达式都可以省略; 这意味着,会复用前一项的表达式及其类型 例如: const ( a = 1 b c = 3.14 d ) fmt.Println(a,b,c,d) // "1 1 3.14 3.14" 如果复用右侧表达式导致计算结果总是相同,这就不太实用;假若该结果可变,该怎么办呢?来看看 iota | |
常量生成器 iota | |
iota | 常量的声明可以使用常量生成器 iota ,它创建一系列相关值,而不是逐个值显式写出; 常量声明中,iota 从 0 开始取值,逐项加 1 |
iota 的理解 | 1. iota 遇到 const ,就会被重置为 0 2. 每出现一个新的常量项,iota 的值都自动加 1 |
示例 | 下面的例子来自 time 包,它定义了 Weekday 的具名类型,并声明每周的 7 天为该类型的常量,从 Sunday 开始,其值为 0 这种类型通常称为 "枚举型(enumeration ,或缩写为 enum)" type Weekday int const ( Sunday Weekday = iota // Sunday为常量项,Weekday为类型,iota 是初始值,为 0 Monday // iota = 1 Tuesday // iota = 2 Wednesday // iota = 3 Thurday // iota = 4 Friday // iota = 5 Saturday // iota = 6 ) 更复杂的表达式也可使用 iota ,借用 net 包的代码举例如下,无符号整数最低 5 位数中的每一个都逐一命名,并解释为布尔值 type Flags uint const ( FlagUp Flags = 1 << iota // 向上 FlagBroadcast // 支持广播访问 FlagLoopback // 环回接口 FlagPointToPoint // 点对点链路 FlagMulticast // 支持多路广播访问 ) 随着 iota 递增,每个常量项都按 1<< iota 赋值,这等价于 2 的连续次幂,它们分别与单个 "位" 对应;若某些函数要针对相应的位执行判定 、设置 或 清除操作,就会用到这些常量 func IsUp(v Flags) bool { return v&FlagUp == FlagUp } func TurnDown(v *Flags) { *v &^= FlagUp } func SetBroadcast(v *Flags) { *v |= FlagBroadcast } func IsCast(v Flags) bool { return v&( FlagBroadcast | FlagMulticast) != 0 } func main() { var v Flags = FlagMulticast | FlagUp fmt.Printf("%b %t\n",v,IsUp(v)) // "10001 true" turnDown(&v) fmt.Printf("%b %t\n",v,IsUp(v)) // "10000 false" SetBroadcast(&v) fmt.Printf("%b %t\n",v,IsUp(v)) // "10010 false" fmt.Printf("%b %t\n",v,IsCast(v)) // "10010 true" } 下面的例子更复杂,声明的常量表示 1024 的幂 const ( _ = 1 << ( 10 * iota ) KiB // 1024 MiB // 1048576 GiB // 1073741824 TiB // 1099511627776 PiB // 1125899906842624 EiB // 1152921504606846976 ZiB // 1180591620717411303424 YiB // 12089258196146291774706176 ) 然而,iota 机制也存在局限; 比如,因为不存在指数运算符,所以无法生成更为人熟知的 1000 的幂(KB 、MB 等) |
总结 (特别重要) | 1. 同时声明一组常量,除了第一个常量项之外,后续常量项在等号右侧的表达式都可以省略;这意味着,会复用前一项的表达式及其类型 2. iota 遇到 const ,就会被重置为 0 3. 每出现一个新的常量项,iota 的值都自动加 1 |
无类型常量 | |