在很多强类型编程语言中都会有一种特殊的类型——无符号整数类型,该数据类型在使用过程中往往稍不留意就会引发出乎意料的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溢出之后会怎么样?会有问题吗?