## 3.1. 整型
Go語言的數值類型包括幾種不同大小的整數、浮點數和復數。每種數值類型都決定了對應的大小范圍和是否支持正負符號。讓我們先從整數類型開始介紹。
Go語言同時提供了有符號和無符號類型的整數運算。這里有int8、int16、int32和int64四種截然不同大小的有符號整數類型,分別對應8、16、32、64bit大小的有符號整數,與此對應的是uint8、uint16、uint32和uint64四種無符號整數類型。
這里還有兩種一般對應特定CPU平臺機器字大小的有符號和無符號整數int和uint;其中int是應用最廣泛的數值類型。這兩種類型都有同樣的大小,32或64bit,但是我們不能對此做任何的假設;因為不同的編譯器即使在相同的硬件平臺上可能產生不同的大小。
Unicode字符rune類型是和int32等價的類型,通常用于表示一個Unicode碼點。這兩個名稱可以互換使用。同樣byte也是uint8類型的等價類型,byte類型一般用于強調數值是一個原始的數據而不是一個小的整數。
最后,還有一種無符號的整數類型uintptr,沒有指定具體的bit大小但是足以容納指針。uintptr類型只有在底層編程時才需要,特別是Go語言和C語言函數庫或操作系統接口相交互的地方。我們將在第十三章的unsafe包相關部分看到類似的例子。
不管它們的具體大小,int、uint和uintptr是不同類型的兄弟類型。其中int和int32也是不同的類型,即使int的大小也是32bit,在需要將int當作int32類型的地方需要一個顯式的類型轉換操作,反之亦然。
其中有符號整數采用2的補碼形式表示,也就是最高bit位用來表示符號位,一個n-bit的有符號數的值域是從$-2^{n-1}$到$2^{n-1}-1$。無符號整數的所有bit位都用于表示非負數,值域是0到$2^n-1$。例如,int8類型整數的值域是從-128到127,而uint8類型整數的值域是從0到255。
下面是Go語言中關于算術運算、邏輯運算和比較運算的二元運算符,它們按照優先級遞減的順序排列:
```
* / % << >> & &^
+ - | ^
== != < <= > >=
&&
||
```
二元運算符有五種優先級。在同一個優先級,使用左優先結合規則,但是使用括號可以明確優先順序,使用括號也可以用于提升優先級,例如`mask & (1 << 28)`。
對于上表中前兩行的運算符,例如+運算符還有一個與賦值相結合的對應運算符+=,可以用于簡化賦值語句。
算術運算符`+`、`-`、`*`和`/`可以適用于整數、浮點數和復數,但是取模運算符%僅用于整數間的運算。對于不同編程語言,%取模運算的行為可能并不相同。在Go語言中,%取模運算符的符號和被取模數的符號總是一致的,因此`-5%3`和`-5%-3`結果都是-2。除法運算符`/`的行為則依賴于操作數是否全為整數,比如`5.0/4.0`的結果是1.25,但是5/4的結果是1,因為整數除法會向著0方向截斷余數。
一個算術運算的結果,不管是有符號或者是無符號的,如果需要更多的bit位才能正確表示的話,就說明計算結果是溢出了。超出的高位的bit位部分將被丟棄。如果原始的數值是有符號類型,而且最左邊的bit位是1的話,那么最終結果可能是負的,例如int8的例子:
```Go
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),當用作一元運算符時表示按位取反;也就是說,它返回一個每個bit位都取反的數。位操作運算符`&^`用于按位置零(AND NOT):如果對應y中bit位為1的話, 表達式`z = x &^ y`結果z的對應的bit位為0,否則z對應的bit位等于x相應的bit位的值。
下面的代碼演示了如何使用位操作解釋uint8類型值的8個獨立的bit位。它使用了Printf函數的%b參數打印二進制格式的數字;其中%08b中08表示打印至少8個字符寬度,不足的前綴部分用0填充。
```Go
var x uint8 = 1<<1 | 1<<5
var y uint8 = 1<<1 | 1<<2
fmt.Printf("%08b\n", x) // "00100010", the set {1, 5}
fmt.Printf("%08b\n", y) // "00000110", the set {1, 2}
fmt.Printf("%08b\n", x&y) // "00000010", the intersection {1}
fmt.Printf("%08b\n", x|y) // "00100110", the union {1, 2, 5}
fmt.Printf("%08b\n", x^y) // "00100100", the symmetric difference {2, 5}
fmt.Printf("%08b\n", x&^y) // "00100000", the difference {5}
for i := uint(0); i < 8; i++ {
if x&(1<<i) != 0 { // membership test
fmt.Println(i) // "1", "5"
}
}
fmt.Printf("%08b\n", x<<1) // "01000100", the set {2, 6}
fmt.Printf("%08b\n", x>>1) // "00010001", the set {0, 4}
```
(6.5節給出了一個可以遠大于一個字節的整數集的實現。)
在`x<<n`和`x>>n`移位運算中,決定了移位操作的bit數部分必須是無符號數;被操作的x可以是有符號數或無符號數。算術上,一個`x<<n`左移運算等價于乘以$2^n$,一個`x>>n`右移運算等價于除以$2^n$。
左移運算用零填充右邊空缺的bit位,無符號數的右移運算也是用0填充左邊空缺的bit位,但是有符號數的右移運算會用符號位的值填充左邊空缺的bit位。因為這個原因,最好用無符號運算,這樣你可以將整數完全當作一個bit位模式處理。
盡管Go語言提供了無符號數的運算,但即使數值本身不可能出現負數,我們還是傾向于使用有符號的int類型,就像數組的長度那樣,雖然使用uint無符號類型似乎是一個更合理的選擇。事實上,內置的len函數返回一個有符號的int,我們可以像下面例子那樣處理逆序循環。
```Go
medals := []string{"gold", "silver", "bronze"}
for i := len(medals) - 1; i >= 0; i-- {
fmt.Println(medals[i]) // "bronze", "silver", "gold"
}
```
另一個選擇對于上面的例子來說將是災難性的。如果len函數返回一個無符號數,那么i也將是無符號的uint類型,然后條件`i >= 0`則永遠為真。在三次迭代之后,也就是`i == 0`時,i--語句將不會產生-1,而是變成一個uint類型的最大值(可能是$2^64-1$),然后medals[i]表達式運行時將發生panic異常(§5.9),也就是試圖訪問一個slice范圍以外的元素。
出于這個原因,無符號數往往只有在位運算或其它特殊的運算場景才會使用,就像bit集合、分析二進制文件格式或者是哈希和加密操作等。它們通常并不用于僅僅是表達非負數量的場合。
一般來說,需要一個顯式的轉換將一個值從一種類型轉化為另一種類型,并且算術和邏輯運算的二元操作中必須是相同的類型。雖然這偶爾會導致需要很長的表達式,但是它消除了所有和類型相關的問題,而且也使得程序容易理解。
在很多場景,會遇到類似下面代碼的常見的錯誤:
```Go
var apples int32 = 1
var oranges int16 = 2
var compote int = apples + oranges // compile error
```
當嘗試編譯這三個語句時,將產生一個錯誤信息:
```
invalid operation: apples + oranges (mismatched types int32 and int16)
```
這種類型不匹配的問題可以有幾種不同的方法修復,最常見方法是將它們都顯式轉型為一個常見類型:
```Go
var compote = int(apples) + int(oranges)
```
如2.5節所述,對于每種類型T,如果轉換允許的話,類型轉換操作T(x)將x轉換為T類型。許多整數之間的相互轉換并不會改變數值;它們只是告訴編譯器如何解釋這個值。但是對于將一個大尺寸的整數類型轉為一個小尺寸的整數類型,或者是將一個浮點數轉為整數,可能會改變數值或丟失精度:
```Go
f := 3.141 // a float64
i := int(f)
fmt.Println(f, i) // "3.141 3"
f = 1.99
fmt.Println(int(f)) // "1"
```
浮點數到整數的轉換將丟失任何小數部分,然后向數軸零方向截斷。你應該避免對可能會超出目標類型表示范圍的數值做類型轉換,因為截斷的行為可能依賴于具體的實現:
```Go
f := 1e100 // a float64
i := int(f) // 結果依賴于具體實現
```
任何大小的整數字面值都可以用以0開始的八進制格式書寫,例如0666;或用以0x或0X開頭的十六進制格式書寫,例如0xdeadbeef。十六進制數字可以用大寫或小寫字母。如今八進制數據通常用于POSIX操作系統上的文件訪問權限標志,十六進制數字則更強調數字值的bit位模式。
當使用fmt包打印一個數值時,我們可以用%d、%o或%x參數控制輸出的進制格式,就像下面的例子:
```Go
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的兩個使用技巧。通常Printf格式化字符串包含多個%參數時將會包含對應相同數量的額外操作數,但是%之后的`[1]`副詞告訴Printf函數再次使用第一個操作數。第二,%后的`#`副詞告訴Printf在用%o、%x或%X輸出時生成0、0x或0X前綴。
字符面值通過一對單引號直接包含對應字符。最簡單的例子是ASCII中類似'a'寫法的字符面值,但是我們也可以通過轉義的數值來表示任意的Unicode碼點對應的字符,馬上將會看到這樣的例子。
字符使用`%c`參數打印,或者是用`%q`參數打印帶單引號的字符:
```Go
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'"
```
- 前言
- Go語言起源
- Go語言項目
- 本書的組織
- 更多的信息
- 致謝
- 入門
- Hello, World
- 命令行參數
- 查找重復的行
- GIF動畫
- 獲取URL
- 并發獲取多個URL
- Web服務
- 本章要點
- 程序結構
- 命名
- 聲明
- 變量
- 賦值
- 類型
- 包和文件
- 作用域
- 基礎數據類型
- 整型
- 浮點數
- 復數
- 布爾型
- 字符串
- 常量
- 復合數據類型
- 數組
- Slice
- Map
- 結構體
- JSON
- 文本和HTML模板
- 函數
- 函數聲明
- 遞歸
- 多返回值
- 錯誤
- 函數值
- 匿名函數
- 可變參數
- Deferred函數
- Panic異常
- Recover捕獲異常
- 方法
- 方法聲明
- 基于指針對象的方法
- 通過嵌入結構體來擴展類型
- 方法值和方法表達式
- 示例: Bit數組
- 封裝
- 接口
- 接口是合約
- 接口類型
- 實現接口的條件
- flag.Value接口
- 接口值
- sort.Interface接口
- http.Handler接口
- error接口
- 示例: 表達式求值
- 類型斷言
- 基于類型斷言識別錯誤類型
- 通過類型斷言查詢接口
- 類型分支
- 示例: 基于標記的XML解碼
- 補充幾點
- Goroutines和Channels
- Goroutines
- 示例: 并發的Clock服務
- 示例: 并發的Echo服務
- Channels
- 并發的循環
- 示例: 并發的Web爬蟲
- 基于select的多路復用
- 并發的退出
- 示例: 聊天服務
- 基于共享變量的并發
- 競爭條件
- sync.Mutex互斥鎖
- sync.RWMutex讀寫鎖
- 內存同步
- 競爭條件檢測
- 示例: 并發的非阻塞緩存
- Goroutines和線程
- 包和工具
- 包簡介
- 導入路徑
- 包聲明
- 導入聲明
- 包的匿名導入
- 包和命名
- 工具
- 測試
- go test
- 測試函數
- 測試覆蓋率
- 基準測試
- 剖析
- 示例函數
- 反射
- 為何需要反射?
- reflect.Type和reflect.Value
- Display遞歸打印
- 示例: 編碼S表達式
- 通過reflect.Value修改值
- 示例: 解碼S表達式
- 顯示一個類型的方法集
- 幾點忠告
- 底層編程
- unsafe.Sizeof, Alignof 和 Offsetof
- unsafe.Pointer
- 示例: 深度相等判斷
- 通過cgo調用C代碼
- 幾點忠告
- 附錄
- 附錄A:原文勘誤
- 附錄B:作者譯者
- 附錄C:譯文授權
- 附錄D:其它語言