第二章 序
在计算机底层,一切都是比特位。然而计算机一般操作的都是固定大小的值,称之为字(word)。字会被解释为整数、浮点数、比特位数组、内存地址等,这些字又可以进一步聚合成数据包(packet)、像素点、作品集、诗歌或者其它任何对象。Go语言提供了多样化的数据组织方式,这些数据类型既有硬件层面的兼容性,又能让程序员方便的组合成更复杂的数据类型。
Go语言的数据类型分为四大类:基本类型,复合类型,引用类型及接口类型。本章将介绍基本类型:数字,字符串,布尔值。
一、整数
Go语言的数值数据类型包括以下几种:整数,浮点数,复数,每一种都包含了大小(size)不同的数值类型,例如有符号整数包含int8,int16,int32,int64,int。每一种数值类型都会决定值的大小和符号(正负),我们首先从整数类型开始讲起。
Go提供了有符号和无符号整数运算。有符号整数类型分为8bit,16bit,32bit,64bit四种:int8,int16,int32,int64,还有对应的无符号整数:uint8,uint16,uint32,uint64。 还有两种整数类型,它们的大小是取决于机器平台的CPU字长的:int和uint,其中int是使用最广的。两种类型在不同平台上可能有不同的大小,32或64bit,但是我们在使用过程中不能有任何的假设,因为即使是在同一个机器平台,不同的编译器也可能采用不同的字长。因此如果确切需要64位长度,那就使用int64,如果32位长度足够,就使用int,毕竟int是性能最高的整数类型。
Unicode的字符都是rune类型的,rune是int32的同义词,事实上rune的底层类型就是int32,每个rune代表一个Unicode码点,这两个类型是可以互换使用的。同样的,byte也是uint8的同义词,byte用来强调数值是字符流的一个点而不是一个小整数。
最后,还有一种无符号整数类型uintptr,它的长度等同于机器的字长,因此是不固定的,但是足够容纳一个指针值。uintptr只在底层编程时才需要,例如,需要指针计算,需要和C语言交互等等。
总之,这些类型都是完全不同的类型,例如int和int32,虽然int在某个平台上也可能是32bits,但是把int值当作int32使用时,必须要显式的类型转换,反之亦然。
有符号整数采用2的补码形式表示,最高bit位表示符号位。一个n-bit的有符号数使用n-1个bit来表示范围,它的值的范围是
-1。无符号数使用所有的bit位来表示非负值,因此值的范围是0到 。例如int8的范围是-128到127,uint8是0到255。
下面列举了Go的算数运算、逻辑运算、比较运算中的二元操作符,按照优先级递减顺序排列:
* / % << >> & &^ + - | ^ == != < <= > >= && ||
上图中,二元操作符有五层优先级,在同一个层级使用左优先的计算原则,因此可能要使用括号来提升可读性、保证运算顺序的正确性 :mask & (1 << 28) 。
前两行的每个操作符都有对应的简略的赋值语句,例如 + 对应的赋值语句 +=, ^对应的赋值语句 ^=。
其中,整数运算符 + , - , * , / 还可以用在浮点数、复数类型上,但是求余运算符只能用在整数上。对于不同的语言来说,%可能有不同的行为,在Go中,余数的符号和被除数的符号是相同的。所以 -5%3和-5%-3的结果都是-2。 / (除法)的行为依赖于操作数的类型,如果两个操作数都是整数,那么结果也是整数: 5 / 4 的结果是1;如果至少有一个操作数是浮点数,那么结果就是浮点数, 5.0 / 4.0 的结果是1.25。
如果算数运算的结果,无论是有符号还是无符号,如果需要更多的bit才能正确表示,那么我们就说计算溢出了,这时,超过高位的bit将被丢弃。如果原始数值是有符号的且最左边的bit是1,那么最终结果可能是负的,例如下面的int8示例:
var u uint8 = 255 fmt.Println(u, u+1, u*u) // "255 0 1" var i int8 = 127 fmt.Println(i, i+1, i*i) // "127 -128 1"两个同类型整数可以通过比较运算符进行比较,比较表达式的类型是一个布尔值:
== 等于 != 不等于 < 小于 <= 小于等于 > 大于 >= 大于等于事实上,所有的基本类型:布尔,数值,字符串等都是可以比较的,这些类型至少支持== 和 != 运算符。其中,整数、浮点数、字符串可以用所有的比较运算符。有很多类型是不可以比较的,因此是不可以排序的。当后续章节遇到这些类型时,我们会详细讲解这些规则。
下面这些是一元运算符:
+ 正号 - 负号对于整数,+x 是 0 + x表达式的缩写,-x是0 - x表达式的缩写;对于浮点数和复数,+x就是x, -x就是x的负数。
Go也提供了下面这些bit位操作符,其中前4个不区分操作数的符号:
& 位运算 AND | 位运算 OR ^ 位运算 XOR &^ 位清空 (AND NOT) << 左移 >> 右移
位操作符^作为二元运算符时是按位异或(XOR),但是作为一元操作符时是按位取反或补位:返回一个每个位都取反的数,10100111 -> 01011000。$^操作符是位清空(AND NOT):在表达式z = x &^ y 中,如果y的某个位是1,那么z的对应位就是0,z剩余的位取决于x的对应位。
下面的代码利用按位操作把uint8的值作为8个独立的位来使用。使用了Printf的 %b参数打印了数值的位表示;08修饰%b,可以让结果准确的显示为8bit,不足位补0:
var x uint8 = 1<<1 | 1<<5 var y uint8 = 1<<1 | 1<<2 fmt.Printf("%08b\n", x) // "00100010" fmt.Printf("%08b\n", y) // "00000110" fmt.Printf("%08b\n", x&y) // "00000010" fmt.Printf("%08b\n", x|y) // "00100110" fmt.Printf("%08b\n", x^y) // "00100100" fmt.Printf("%08b\n", x&^y) // "00100000" for i := uint(0); i < 8; i++ { if x&(1<<i) != 0 { fmt.Println(i) // "1", "5" } } fmt.Printf("%08b\n", x<<1) // "01000100" fmt.Printf("%08b\n", x>>1) // "00010001"(章节5.5会给出一个远大于byte整数集的实现)
在移位操作x<<n和x>>n中,操作数n决定了位移动的个数,n必须是无符号的。x操作数可以是无符号或有符号的。从算数上来说,x<<n等价于x * 2n
,x>>n等价于x / 2n
。
左移操作会在右边新增的空缺bit位填充0;无符号数右移是在左边新增的空缺bit位填充0,但是有符号数的右移会在左边新增的空缺bit位填充它的符号位。因此当你要将一个整数按位操作时,最好使用无符号运算,例如给待运算的数指定类型uint8。
虽然Go提供无符号数和运算,但是我们更倾向于使用int类型,即使在需要非负数的场景,例如数组的长度,虽然这里看上去使用uint更合适。事实上len函数返回的就是有符号的int类型:
medals := []string{"gold", "silver", "bronze"} for i := len(medals) - 1; i >= 0; i-- { fmt.Println(medals[i]) // "bronze", "silver", "gold" }这里如果len返回的是uint类型,那将是致命的,因为 i 将是一个uint类型,i >= 0 将永远为真。这里 i 永远不会变为-1,当i == 0是,i--语句会将i变成uint的MAX值,这个时候程序就出现致命的问题了:medals[i]会去访问slice边界外的元素。
因此,无符号数一般只在按位操作或者特定的运算场景时使用,例如:实现bit集合,解析二进制文件,哈希算法或者其它加密算法中。我们不能仅仅因为需要非负数,就去使用无符号数。
一般来说把一个值转换成另外一个类型需要显式类型转换,且二元操作符需要两个操作数的类型都相同,这种时候偶尔会导致较长的表达式。但是这种妥协是值得的,因为这样就可以消除类型带来的问题,提供更好的可读性。
下面这个例子在很多场景中都会出现:
var apples int32 = 1 var oranges int16 = 2 var compote int = apples + oranges // compile error编译时会报错:
invalid operation: apples + oranges (mismatched types int32 and int16)有几种办法可以解决这个问题,其中一个是把所有类型统一成int:
var compote = int(apples) + int(oranges)许多整形之间的转换都不会改变具体的值,它们只是告诉编译器该怎么解释这个值。但是从范围大的值转换为范围小的值,例如从整数转为浮点数(反之亦然),可能会改变值的大小或者丢失精度:
f := 3.141 // a float64 i := int(f) fmt.Println(f, i) // "3.141 3" f = 1.99 fmt.Println(int(f)) // "1"浮点数转为整数会丢弃掉小数部分,因此我们应该避免这种操作数范围不一致的转换:
f := 1e100 // a float64 i := int(f) // result is implementation-dependent整型可以表示为普通的十进制数,也可以在头部加一个0表示为8进制:0666,加一个0x或0X表示为16进制:0xdeadbeef。16进制的数字可以用大小写字母。这几年,8进制貌似只有一个用途了:POSIX系统下的文件权限控制,但是16进制的使用是非常广泛的,因为16进制强调的是一个数值的位模式,10进制更强调数值的大小。
当使用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 %#[1]X\n", x) // Output: // 3735928559 deadbeef 0xdeadbeef 0XDEADBEEF
注意这里有两个fmt的技巧。首先,一般来说,打印参数%x 的数目和要打印的操作数的数目是相同的,但是这里使用了%[1]告诉Printf重复使用第一个打印的操作数。其次%o,%x,%X和#结合,告诉Printf打印的时间分别添加0,0x,0X。#的用途是非常广的,因为它可以打印数据的详细格式,例如打印struct的时候使用%#v,建议读者亲自尝试!
Rune的表现形式是字符两边加上单引号,最简单的例子就是ASCII字符 'a'。我们既可以直接写Unicode码点,也可以通过数值逃逸的方式。
fmt使用%c或者%q(加上引号)来打印runes:
ascii := 'a' unicode := 'Image' newline := '\n' fmt.Printf("%d %[1]c %[1]q\n", ascii) // "97 a 'a'" fmt.Printf("%d %[1]c %[1]q\n", unicode) // "22269 Image 'Image'" fmt.Printf("%d %[1]q\n", newline) // "10 '\n'"
文章所有权:Golang隐修会 联系人:孙飞,CTO@188.com!
有疑问加站长微信联系(非本文作者)