Go语言核心之美 2.1-整数

第二章 序

在计算机底层,一切都是比特位。然而计算机一般操作的都是固定大小的值,称之为字(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来表示范围,它的值的范围是

2n12n12n12n1
       
-1。无符号数使用所有的bit位来表示非负值,因此值的范围是0到2n12n1 。例如int8的范围是-128到127,uint8是0到255。

下面列举了Go的算数运算、逻辑运算、比较运算中的二元操作符,按照优先级递减顺序排列:2n1

*    /    %    <<    >>    &    &^
+    -    |    ^
==   !=   <    <=    >     >=
&&
||

上图中,二元操作符有五层优先级,在同一个层级使用左优先的计算原则,因此可能要使用括号来提升可读性、保证运算顺序的正确性 :mask & (1 << 28) 。

前两行的每个操作符都有对应的简略的赋值语句,例如 + 对应的赋值语句 +=, ^对应的赋值语句 ^=。

其中,整数运算符 + , - , * , / 还可以用在浮点数、复数类型上,但是求余运算符只能用在整数上。对于不同的语言来说,%可能有不同的行为,在Go中,余数的符号和被除数的符号是相同的。所以 -5%3和-5%-3的结果都是-2。 / (除法)的行为依赖于操作数的类型,如果两个操作数都是整数,那么结果也是整数: 5 / 4 的结果是1;如果至少有一个操作数是浮点数,那么结果就是浮点数, 5.0 / 4.0 的结果是1.25。

2n1水豆

如果算数运算的结果,无论是有符号还是无符号,如果需要更多的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<>1) // "00010001"
(章节5.5会给出一个远大于byte整数集的实现)

在移位操作x<>n中,操作数n决定了位移动的个数,n必须是无符号的。x操作数可以是无符号或有符号的。从算数上来说,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隐修会 联系人:孙飞,[email protected]






2n

你可能感兴趣的:(编程语言,Go语言高级进阶篇)