golang冷知识001:位移运算类型推导

511 阅读2分钟

下面语句输出是?

var s string = "123456789"    // len(s)=9
const ss string = "123456789" // len(ss)=9

var a byte = (1 << len(s)) >> len(s)
var b byte = (1 << len(ss)) >> len(ss)
fmt.Println(a, b)

答案

0 1

原因

官方解释:在位移表达式的右侧的操作数必须为整数类型,或者可以被 uint 类型的值所表示的无类型的常量。 如果一个非常量位移表达式的左侧的操作数是一个无类型常量,那么它会先被隐式地转换为假如位移表达式被其左侧操作数单独替换后的类型。

官方解释很晦涩,下面按语句解释:

len(s) s为动态字符串返回int非常数;(1 << len(s)) 为非常数表达式;取左侧类型为byte;byte(1)左移9位溢出,再右移9位,所以为0;

len(ss) 因为ss为常量 len(ss) 返回常数;(1 << len(ss)) 为常数表达式,还是uint类型,所以左移动9位;后右移动9位还是uint类型为uint(1);在byte范围类,所以结果为1;

没完呢,追加一个疑问

下面编译报错,为什么呢?

var s string = "123456789"    // len(s)=9
const ss string = "123456789" // len(ss)=9
var c byte = (1 << len(ss)) >> len(s)
fmt.Println(c)

compile fail: constant 512 overflows byte

(1 << len(ss)) 结果为常数,右侧len(s)非常数;

最后

var c byte = 常数 >> 非常数 

为非常量表达式;

常数类型为byte,右移9位就报错了。

ssa分析

分别用常量字符串和非常量字符串编译出ssa文件比较差异; 以下指令执行后生成一个ssa.html文件。

GOSSAFUNC=f1 go build

源码1

package main

const ss string = "123456789"

func f1() byte {
    var b byte = 1 << len(ss) >> len(ss)
    return b
}

源码2 只是将const ss 改为 var ss

package main

var ss string = "123456789"

func f1() byte {
    var b byte = 1 << len(ss) >> len(ss)
    return b
}

通过比对ssa文件我们发现,如果是常量表达式在编译阶段已经求得最终值,非常量表达式游戏规则就是用左类型不用右。

所以,真正本质的原因是:编译阶段计算规则和运行时规则不一致

最后的最后

以上均是在golang1.13上验证的结果,在更早的golang版本(已验证)和未来版本可能会有不一样的结果。

最佳实践是在涉及常数的位移运算给常数设定一个类型。例如:uint64(1) << len(ss)。

这冷知识算做语言设计缺陷还是特性大家自行保留,未来会不会又偷偷改了,谁又知道呢。