操作無符號整數的注意事項
在很多強類型編程語言中都會有一種特殊的類型——無符號整數類型,該數據類型在使用過程中往往稍不留意就會引發出乎意料的bug。
至于,有什么注意事項以及需要了解的知識點,一起來看看吧。
1. go源碼中的數據類型
- // go源碼位置: src/math/const.go
- //
- // Integer limit values.
- const (
- MaxInt8 = 1<<7 - 1
- MinInt8 = -1 << 7
- MaxInt16 = 1<<15 - 1
- MinInt16 = -1 << 15
- MaxInt32 = 1<<31 - 1
- MinInt32 = -1 << 31
- MaxInt64 = 1<<63 - 1
- MinInt64 = -1 << 63
- MaxUint8 = 1<<8 - 1
- MaxUint16 = 1<<16 - 1
- MaxUint32 = 1<<32 - 1
- MaxUint64 = 1<<64 - 1
- )
- // go源碼位置: src/builtin/builtin.go
- //
- // uint8 is the set of all unsigned 8-bit integers.
- // Range: 0 through 255.
- type uint8 uint8
- // uint16 is the set of all unsigned 16-bit integers.
- // Range: 0 through 65535.
- type uint16 uint16
- // uint32 is the set of all unsigned 32-bit integers.
- // Range: 0 through 4294967295.
- type uint32 uint32
- // uint64 is the set of all unsigned 64-bit integers.
- // Range: 0 through 18446744073709551615.
- type uint64 uint64
- // int8 is the set of all signed 8-bit integers.
- // Range: -128 through 127.
- type int8 int8
- // int16 is the set of all signed 16-bit integers.
- // Range: -32768 through 32767.
- type int16 int16
- // int32 is the set of all signed 32-bit integers.
- // Range: -2147483648 through 2147483647.
- type int32 int32
- // int64 is the set of all signed 64-bit integers.
- // Range: -9223372036854775808 through 9223372036854775807.
- type int64 int64
- // float32 is the set of all IEEE-754 32-bit floating-point numbers.
- type float32 float32
- // float64 is the set of all IEEE-754 64-bit floating-point numbers.
- type float64 float64
從源碼中可以看出:
- 無符號類型只有正數值域(最小值為0),沒有負數值域
- 有符號類型有正、負數值域
- 無符號類型正數值域數值個數是有符號類型正數值域數值個數的2倍
以 uint8 與 int8 為例,無符號類型 uint8,正數值域 0 ~ 255 共 256個數值。有符號類型 int8,正數值域 0 ~ 127 共 128個數值。
2. 無符號類型與有符號類型的值域
可能你會問:相同長度的無符號、有符號類型,值域為什么是這樣的分布?
看過計算機微機原理的同學大概都會略知一二,明白其緣由:
- 計算機只認識0、1,每個0、1被稱為1個bit (bit是計算機中最小的單位)
- 計算機系統中所有的數值以及數據都使用0、1串組合存儲
- 不同的0、1組合,由于使用不同編碼、解碼方式才被賦予了不同的含義,進而擁有了各種不同數據類型
就拿長度都是8 bit的無符號類型uint8與有符號類型int8來說:
uint8無符號類型的8個bit位都用來表示數值
int8有符號類型的8個bit中,只有后7個bit位用來表示數值。剩余的一個bit用來表示符號位,為0表示正數值,為1表示負數值。
在二進制中,1個bit長度之差造成的表達值域就是2倍
3. 無符號類型與有符號類型的加減法
先看一段代碼:
- func demo() {
- var a uint8 = 1
- var b uint8 = 2
- v1 := a - b
- fmt.Println("uint8 1-2=", v1)
- var c int8 = 1
- var d int8 = 2
- v2 := c - d
- fmt.Println("int8 1-2=", v2)
- fmt.Println("---------------")
- var e uint8 = math.MaxUint8
- var f uint8 = 1
- v3 := e + f
- fmt.Printf("uint8 255+1=%d %T\n", v3, v3)
- var g int8 = math.MaxInt8
- var h int8 = 1
- v4 := g + h
- fmt.Printf("int8 127+1=%d %T\n", v4, v4)
- }
聰明的你,猜下執行結果會是什么?
- uint8 - v1 1-2= 255
- int8 - v2 1-2= -1
- ---------------
- uint8 - v3 255+1=0 uint8
- int8 - v4 127+1=-128 int8
結果分析:
- v3、v4在進行相加操作時,由于運算結果超出了對應的數值位長度而發生溢出,導致溢出的數據位無效
- v2結果正確
- v1不僅是本文重點之一,也會在很多場合中稍有不慎就導致嚴重bug
在網上看到的一個關于無符號整形減法產生的bug,如下圖所示:
4. 關于無符號整形加減法的一些結論
先說一些關于無符號整形加減法的結論:
1.無符號整形進行加法操作時會像其他類型一樣,在運算結果超出數值位時發生溢出
2.無符號整形進行減法操作時,運算結果有兩種情況
2.1 減數>=被減數,則最終結果大于等于0
2.2 減數<被減數,最終結果也大于等于0
關于無符號整形進行減法比較特殊,減數小于被減數時結果也大于等于0,是不是很意外。
總結一句話概括:無符號類型數值無論加減操作,其結果從不會小于0。
5. 無符號類型數值相減問題
- [root@localhost workspace]# cat -n t.go
- 1 package main
- 2
- 3 import"fmt"
- 4
- 5 func main (){
- 6 var a uint8 = 1
- 7 var b uint8 = 2
- 8 v1 := a - b
- 9
- 10 fmt.Println("uint8 - v1 1-2=", v1)
- 11 }
第8行代碼執行了兩個uint8無符號類型減法操作,得到結果v1。
- [root@localhost workspace]# go build -gcflags="-N -l -S" t.go
- # command-line-arguments
- "".main STEXT size=222 args=0x0 locals=0x80 funcid=0x0
- 0x000000000 (/root/workspace/t.go:5) TEXT "".main(SB), ABIInternal, $128-0
- ......
- 0x002b00043 (/root/workspace/t.go:8) MOVBLZX "".a+54(SP), AX
- 0x003000048 (/root/workspace/t.go:8) ADDL $-2, AX // !!! 加 -2
- 0x003300051 (/root/workspace/t.go:8) MOVB AL, "".v1+52(SP)
- 0x003700055 (/root/workspace/t.go:10) MOVB AL, ""..autotmp_3+55(SP)
首先,要說明一點:在計算機中沒有減法,只有加法操作(出乎你的意料)。
通過匯編代碼可以看出 a-b 被轉換成了a + (-b),即 1-2 = 1+(-2)。
按理說1-2應該等于-1才對,這其中又發生了什么呢?
6. 負數的表達形式——補碼
前面說過,計算機只認識二進制的0、1,2屬于10進制的數值,10進制則可以看做是一種計算機上的編解碼規則。那么,其對應的二進制又是什么呢?
- func demo2() {
- var a, b, c uint8
- a = 1
- b = 2
- c = a + (-b)
- fmt.Printf(
- "a的二進制為:%08b \n"+
- "b的二進制為:%08b \n"+
- "-b的二進制為:%08b \n"+
- "c的二進制為:%08b \n"+
- "c的十進制為:%d", a, b, -b, c, c,
- )
- }
執行結果為:
- a的二進制為:00000001
- b的二進制為:00000010
- -b的二進制為:11111110
- c的二進制為:11111111
- c的十進制為:255
在計算機系統里面,數值有三種編碼:原碼、反碼、補碼。
- 反碼、補碼一般用于負數,反碼=負數對應正數的原碼取反,補碼=反碼+1
- 正數的原碼、反碼、補碼一樣
- 負數分兩種情況:3.1 對于有符號類型:負數的數值位使用補碼表示,同時設置符號位為13.2 對于無符號類型:負數的數值位使用補碼表示,由于無符號位,故無需設置符號位
由上文若干規則可知:
- uint8 類型的 -b 對應的二進制為 11111110,uint8 類型 a 對應二進制為00000001
- c=a+(-b),則對應bit位相加為11111111
- 同類型相加結果還為同一類型,所以c仍然為uint8
- 由于無符號類型的值域不存在負數域,所以11111111轉換為十進制為255
7. 一些疑惑
你是否跟我一樣,存在一些疑惑?
無符號類型可以賦值為負數嗎?
你可能會問:無符號類型既然永遠不為負數,那么可以賦值為負數嗎?
- func demo3() {
- var a uint8
- a = -2
- fmt.Println(a)
- }
執行結果:
- # command-line-arguments
- ./main.go:59:4: constant -2 overflows uint8
通過報錯信息可知,是無法給無符號類型賦值負數的。
無符號類型不可以賦值負數,為什么可以進行取負操作?
既然無符號類型不可以賦值為負數,為什么無符號類型可以取負操作?
- func demo3() {
- var a uint8
- a = 2
- fmt.Println(-a)
- }
可能你又會問:-a需要跟a類型一致才對,-a不能表示為無符號類型,為什么沒報錯呢?
- [root@localhost workspace]# cat -n t.go
- 1 package main
- 2
- 3 import"fmt"
- 4
- 5 func main (){
- 6 var a1 uint8
- 7 a1 = 2
- 8
- 9 fmt.Printf("%b\n", -a1)
- 10 }
- [root@localhost workspace]# go build -gcflags="-N -l -S" t.go
- # command-line-arguments
- "".main STEXT size=197 args=0x0 locals=0x80 funcid=0x0
- 0x000000000 (/root/workspace/t.go:5) TEXT "".main(SB), ABIInternal, $128-0
- ......
- 0x002b00043 (/root/workspace/t.go:9) MOVB $-2, ""..autotmp_1+71(SP)
- 0x003000048 (/root/workspace/t.go:9) XORPS X0, X0
- 0x003300051 (/root/workspace/t.go:9) MOVUPS X0, ""..autotmp_2+80(SP)
- 0x003800056 (/root/workspace/t.go:9) LEAQ ""..autotmp_2+80(SP), AX
- 0x003d00061 (/root/workspace/t.go:9) MOVQ AX, ""..autotmp_4+72(SP)
- 0x004200066 (/root/workspace/t.go:9) TESTB AL, (AX)
- 0x004400068 (/root/workspace/t.go:9) MOVBLZX ""..autotmp_1+71(SP), CX
- 0x004900073 (/root/workspace/t.go:9) LEAQ type.uint8(SB), DX //!!! type.uint8對-2進行類型轉換
- 0x005000080 (/root/workspace/t.go:9) MOVQ DX, ""..autotmp_2+80(SP)
- 0x005500085 (/root/workspace/t.go:9) LEAQ runtime.staticuint64s(SB), DX
- 0x005c00092 (/root/workspace/t.go:9) LEAQ (DX)(CX*8), CX
- 0x006000096 (/root/workspace/t.go:9) MOVQ CX, ""..autotmp_2+88(SP)
- 0x006500101 (/root/workspace/t.go:9) TESTB AL, (AX)
- 0x006700103 (/root/workspace/t.go:9) JMP 105
- 0x006900105 (/root/workspace/t.go:9) MOVQ AX, ""..autotmp_3+96(SP)
- 0x006e00110 (/root/workspace/t.go:9) MOVQ $1, ""..autotmp_3+104(SP)
- 0x007700119 (/root/workspace/t.go:9) MOVQ $1, ""..autotmp_3+112(SP)
- 0x008000128 (/root/workspace/t.go:9) LEAQ go.string."%b\n"(SB), CX
- 0x008700135 (/root/workspace/t.go:9) MOVQ CX, (SP)
- 0x008b00139 (/root/workspace/t.go:9) MOVQ $3, 8(SP)
- 0x009400148 (/root/workspace/t.go:9) MOVQ AX, 16(SP)
- 0x009900153 (/root/workspace/t.go:9) MOVQ $1, 24(SP)
- 0x00a200162 (/root/workspace/t.go:9) MOVQ $1, 32(SP)
- 0x00ab00171 (/root/workspace/t.go:9) PCDATA $1, $0
- 0x00ab00171 (/root/workspace/t.go:9) CALL fmt.Printf(SB)
- ......
通過匯編可以看到,通過type.uint8(SB), DX對運算結果進行了類型轉換。
因此,我們可以得出結論:-a是a與負號(-)的一種運算,運算結果的最終類型會被轉換為與a一致。
總結
本文通過若干示例,展示了無符號類型與有符號類型的差別和注意事項。
那么,什么時候用無符號類型,什么時候用有符號類型呢?
- 運算結果期待包含負數,則不能用無符號類型,此時最好使用有符號類型
- 運算結果不需要包含負數,并且希望類型的正數值域足夠大,此時最好使用無符號類型
- 能不用無符號類型就少用無符號類型,減少bug產生!!!
其他特殊場景,如:在go語言runtime中GPM的邏輯處理器P結構上,P存儲goroutine的本地隊列頭尾位置使用了無符號類型。
- type p struct {
- ......
- // Queue of runnable goroutines. Accessed without lock.
- runqhead uint32// 本地運行隊列 頭位置
- runqtail uint32// 本地運行隊列 尾位置
- runq [256]guintptr // 每個P可以有256個G
- ......
- }
- // runqput tries to put g on the local runnable queue.
- // If next if false, runqput adds g to the tail of the runnable queue.
- // If next is true, runqput puts g in the _p_.runnext slot.
- // If the run queue is full, runnext puts g on the global queue.
- // Executed only by the owner P.
- // runqput把G放到p里。如果next為true,就放到下一個。否則就追加到隊尾。如果隊列滿了,就放到全局隊列。
- func runqput(_p_ *p, gp *g, next bool) {
- ......
- h := atomic.Load(&_p_.runqhead) // load-acquire, synchronize with consumers
- t := _p_.runqtail
- // 放本地隊列
- ift-h < uint32(len(_p_.runq)) {
- _p_.runq[t%uint32(len(_p_.runq))].set(gp)
- atomic.Store(&_p_.runqtail, t+1) // store-release, makes the item available for consumption
- return
- }
- ......
- }
由于t、h的數值是一直在進行+1操作,會超過uint32的最大表示范圍。
思考當 t、h溢出之后會怎么樣?會有問題嗎?