Go 里面的 ^ 和 &^

这几天在研究 Go 的源码,突然发现了一个之前没有见过的位运算,见这里

new &^= mutexWoken

&^,分别表示 AND 和 XOR,这个不用多说。

值得一提的是 ^ 这个符号,在我的印象中,它一直是一个二元运算符,平时见的最多的是 a ^ b 这种用法。

但是实际上它还是一个一元运算符。单走一个 a 也是没问题的,例如 ^a

^ 作为一元运算符的作用

去知识的源头寻找答案!

在 Go 的规范文档Constant expressions 这一节中有提到 ^ 作为一元运算符的作用

The mask used by the unary bitwise complement operator ^ matches the rule for non-constants: the mask is all 1s for unsigned constants and -1 for signed and untyped constants.

^1         // untyped integer constant, equal to -2
uint8(^1)  // illegal: same as uint8(-2), -2 cannot be represented as a uint8
^uint8(1)  // typed uint8 constant, same as 0xFF ^ uint8(1) = uint8(0xFE)
int8(^1)   // same as int8(-2)
^int8(1)   // same as -1 ^ int8(1) = -2

^a: 当 a 是 unsigned 时,相当于用 11111… (… 表示很多很多 1) 与 a 做异或运算;当 a 是 signed 时,相当于用 -1与 a 做异或运算

PS: 无论 int 还是 uint ,其底层都是用 bit 表示的,而 -1 用补码表示就是 1111… ,如果从 bit 的角度出现,可以发现,无论 a 是正数还是负数最终都是与 1111… 做 XOR 运算!而最终的效果则是将 a 所有的 bit 位的值全部反转

让我们来推导一下,首先复习一下反码和补码的知识:

反码: 正数的反码等于本身,负数的反码保持符号位不变,其他位取反

补码: 正数的补码等于它本身,负数的补码等于它的反码加一

下面为了表示方便,正数和负数用到 8 位 bit 表示,即 int8 和 uint8 。

类型 原码 反码 补码
int8 1 0000 0001 0000 0001 0000 0001
int8 -1 1000 0001 1111 1110 1111
uint8 1 0000 0001 0000 0001 0000 0001
int8 -2 1000 0010 1101 1111 1110
uint8 254 (255 – 1) 1111 1110 1111 1110 1111 1110
^1         // untyped integer constant,使用 -1 与其做 XOR 运算 1111 1111 ^ 0000 0001 = 1111 1110, 恰好为 -2 的补码
uint8(^1)  // illegal: same as uint8(-2), -2 cannot be represented as a uint8
^uint8(1)  // typed uint8 constant, same as 0xFF ^ uint8(1) = uint8(0xFE)
int8(^1)   // same as int8(-2)
^int8(1)   // same as -1 ^ int8(1) = -2
&^ 的作用

回到 &^ 上面来,在 Arithmetic_operators 这一节中记录了 &^ 这个运算符。

&^   bit clear (AND NOT)    integers

实际上 a &^ b 的效果近似于 a & (^b),即将 ^b 的结果与 a 做 AND 运算。

&^ 名为 bit clear,他的作用自然也就是 bit clear,a &^ mask 会将 a 中一些位置的 bit 值 clear 为 0,通过将 mask 中 bit 值设置为 1 来指定位置。

例如

pos    12345
a    = 11001
mask = 01010

mask 的第 2 和第 4 位的 bit 为 1,则意味着将 a 的第 2 位和第 4 位的 bit clear 为 0,因此 a &^ mask 的结果为 10001

证明过程也很简单,之前说过 ^mask 的结果是将 mask 的 bit 位全部取反,所以 mask 内原本为 1 的 bit 就变成了 0,之后再与 a 做 AND 运算,任何 bit 值与 0 做 AND 运算的结果都为 0。

a &^ ba & ^b

上面提到过 a &^ b 的效果近似于 a & (^b),但是它两还是有一点微笑区别的。具体可见 stackoverflow 的这个问题: Why does Go have a “bit clear (AND NOT)” operator?

There’s a subtle difference that makes dealing with literals and untyped constants easier with the explicit bit clear operator.

Untyped integers have their default type as int so something like a := uint32(1) & ^1 is illegal as ^1 is evaluated first and it’s evaluated as ^int(1), which equals -2. a := uint32(1) &^ 1 is legal however as here 1 is evaluated as uint32, based on the context.

There could also be some performance gains in having an explicit bit clear, but I’m not too sure about that.

感触

哭,用了快两年的 Go,居然连 Go 的语法都还没学完!