go byte转uint_Go语言中一个关于移位操作的设计细节

先来看一段代码:

package main

func foo() float32 {
    const n = uint(2)
    var x float32 = 1 << n  // OK
    var y = float32(1 << n) // OK
    return x + y
}

func bar() float32 {
    var n = uint(2)
    var x float32 = 1 << n  // error: 不可对浮点数进行移位
    var y = float32(1 << n) // error: 不可对浮点数进行移位
    return x + y
}

func main() {}

编译之,得到如下出错信息:

./aa.go:12:20: invalid operation: 1 << n (shift of type float32)
./aa.go:13:17: invalid operation: 1 << n (shift of type float32)

为什么foo函数编译没问题,但是bar函数编译却通不过呢?两者代码中唯一的差别就是n在函数foo中为常量,而在函数bar中为变量。

在搞懂这个问题之前,我们需要了解Go语言设计中的一些细节。

首先,我们需要知道,Go中的值可以分为两类:类型确定值和类型不确定值。一般来说,

  • 每个变量都是类型确定值。(除了预声明的伪变量nil
  • 有些常量是类型确定值,有些是类型不确定值;但所有的字面型常量都属于类型不确定值,比如1, true, "Go"等,它们可以用作很类型的值。
  • 也有一些类型不确定值为非常量,比如预声明的nil

第二,我们需要知道,每个类型不确定值(除了预声明的nil)都有一个默认类型。Go支持类型推断。一般说来,当一个孤单的类型不确定数值用在一个需要确定类型的场景,此类型不确定数值将被视为一个类型为它的默认类型的类型确定值。对于本文,我们只需知道,码点数值字面值(使用单引号括起来的数值字面值)的默认类型为内置类型runeint32类型的别名),其它任何数值字面值的内置类型为int。比如,在下例中,字面值5在此场景中的类型将被推断为int,而字面值'A'的类型将被推断为rune

package main

import "fmt"

var n = 5 << 1
var c = 'A' >> 1

func main() {
    fmt.Printf("%T %Tn", n, 5)   // int int
    fmt.Printf("%T %Tn", c, 'A') // int32 int32
}

另一条常用的类型推断规则是对于加减乘除等二元操作,如果其中一个操作数是类型不确定的,而另一个是类型确定的,则此类型不确定操作数将被视为一个类型确定值并且的类型将被推断为另一个类型确定操作数的类型。比如,在下面这条语句中,字面值2的类型将被推断为byte(因此Y也将被视为一个类型为byte的类型确定值)。

const Y = byte(1) + 2

第三,我们需要知道,一个类型不确定数值可以超出它的默认类型的取值范围,但是类型确定数值却不可以。

比如下例中的Zz行是编译不过的,但是Xx行却可以。

package main

// '@' == 64: 2的6次方
// '@' << 25: 2的31次方
const Y = rune('@') << 24 // OK
const Z = rune('@') << 25 // error: overflows rune
const X = '@' << 25       // OK

const z = int(1) << 100 // error: overflows int
const x = 1 << 100      // OK

func main() {} 

上例可以被认为是类型不确定值的一个优点。其实类型不确定值还有另外一个优点:一个类型不确定值可以不用显式转换而被当做某些合适的类型的值用。比如:

package main

const X = '@' << 25 // X为一个类型不确定值
const Y = int64(X)  // Y为一个类型确定值

var m int = X          // OK
var n uint16 = X >> 16 // OK
var o float32 = X      // OK

var p int = Y          // error: 类型不匹配
var q uint16 = Y >> 16 // error: 类型不匹配
var r float32 = Y      // error: 类型不匹配

func main() {}

第四,我们需要知道,一个所有操作数均为常量的运算称为一个常量表达式。每个常量表达式都将在编译时刻被估值,其估值结果仍然是一个常量。特别地,对于一个移位运算常量表达式来说,如果它的左操作数为类型不确定值,则估值结果也是一个(可以超出它的默认类型的取值范围)类型不确定值。如果一个移位运算中的任意一个操作数不为常量,则此表达式的估值肯定发生在运行阶段,因此其类型必须在编译时刻确定下来。

OK,到这里,我们可以回到正题了。

对于本文一开始展示的例子中的函数foo,按照上述规则,其中的字面值1将被认为是一个默认类型为int的类型不确定值。常量表达式1 << n的结果为一个在编译时刻估值的类型不确定常量。编译器将检查此估值结果是否溢出了float32

func foo() float32 {
    const n = uint(2)
    var x float32 = 1 << n  // OK
    var y = float32(1 << n) // OK
    return x + y
}

如果n的值过大,编译器将报错:

func foo() float32 {
    const n = uint(200)
    var x float32 = 1 << n  // error: 溢出float32
    var y = float32(1 << n) // error: 溢出float32
    return x + y
}

编译器可以在编译时刻得出常量表达式的值,但是却不能在编译时刻得出非常量表达式的值。如果编译器将本文一开始展示的例子中的函数bar中的字面值1的类型推断为它的默认类型int而不是报错,则此函数在运行时刻会在不同架构的机器上对于某些变量n值返回不同的值。比如当n等于32时,函数bar在32位架构的机器上将返回0,而在64位架构的机器上将返回大约+8.589935e+009

func bar() float32 {
    var n = uint(32)
    var x float32 = 1 << n // 在32系统上运行时刻溢出
    var y = float32(1 << n)
    return x + y
}

这种在编译时刻不报错但在运行时刻返回不同结果的行为不是一个好的设计,特别对于跨平台编译来说更是如此。为了防止此情况的发生,Go语言的设计者对上面提到的类型推断规则“当一个孤单的类型不确定数值用在一个需要确定类型的场景,此类型不确定数值将被视为一个类型为它的默认类型的类型确定值”添加了一个例外:当一个移位运算的左操作数为一个类型不确定值并且右操作数(移动位数)不是一个常量时,左操作数将被视为一个类型确定值,但是它的类型将并不被推断为它的默认类型,而是被推断为它的设想类型。

什么是设想类型呢?下面这条赋值语句中的字面值1的设想类型为float32(如果n是一个非常量)。

var x float32 = 1 << n

所以它等价于下面这条语句:

var x = float32(1) << n

同样地,下面这条语句也等价于上面这条语句(如果n是一个非常量):

var x = float32(1 << n)

浮点数是不能参与移位运算的,所以编辑器将报错。

当然,如果n是一个常量,则上述等价关系并不存在。

到此为止,文章开头的问题已回答完毕。如果你已经理解了上面的解释,则也会理解下例中为什么xX的值会不同。

package main

var n uint = 10
var x byte = (1 << n) / 100
var y int16 = (1 << n) / 100

const N uint = 10
const X byte = (1 << N) / 100
const Y int16 = (1 << N) / 100

func main() {
    println(x, y) // 0 10
    println(X, Y) // 10 10
}

更多关于Go语言的细节、技巧和常识,请访问《Go语言101》项目或者《Go语言101》官网,或者关注本专栏公众号(Go 101):

ff0abe29b3dd421bdedff325a988fc70.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值