一 运算符

除比较运算符外,对于其他二元运算符,操作数类型必须相同,除非操作涉及移位或无类型常量。
除移位操作之外,如果一个操作数是无类型常量而另一个操作数不是,则该常量将隐式转换为另一个操作数的类型。

移位表达式中的右操作数必须具有无符号整数类型,或者是由类型为uint的值表示的无类型常量。如果非常量移位表达式的左操作数是无类型常量,则首先将其隐式转换为假设该移位表达式仅保留其左操作数时应该转换成的类型。

var s uint = 33
var i = 1<<s                  // 1 的类型为 int
var j int32 = 1<<s            // 首先将该语句转换为:var j int32 = 1,
                              // 则由此推算出1 的类型为 int32; j == 0
                              
var k = uint64(1<<s)          // 1 has type uint64; k == 1<<33
var m int = 1.0<<s            // 首先将该语句转换为:var m int = 1.0
                              // 1.0 的类型为 int; 如果int在32位系统上则 m == 0 (溢出)
                              
var n = 1.0<<s == j           // 首先将该语句转换为:var n = 1.0 == j
                              // 1.0 要转换为与j相同的类型:int32; n == true
                              
var o = 1<<s == 2<<s          // 1 和 2 的类型为 int; 如果int在32位系统上则 o == true 
var p = 1<<s == 1<<33         // 如果int在32位系统上则是非法的: 1 的类型为 int, 但 1<<33 会int溢出
var u = 1.0<<s                // 非法的: 1.0 的类型为 float64, 不能移位
var u1 = 1.0<<s != 0          // 非法的: 1.0 的类型为 float64, cannot shift
var u2 = 1<<s != 1.0          // 非法的: 1 的类型为 float64, cannot shift
var v float32 = 1<<s          // 非法的: 1 的类型为 float32, cannot shift
var w int64 = 1.0<<33         // 1.0<<33 是一个常量移位表达式
var x = a[1.0<<s]             // 1.0 的类型为 int; 如果int在32位系统上则 x == a[0]
var a = make([]byte, 1.0<<s)  // 1.0 的类型为 int; 如果int在32位系统上则 len(a) == 0

二 运算符的优先级

一元运算符具有最高优先级。由于++--操作表示语句而不是表达式,因此他们不属于运算符体系,同样,语句*p++等同于(*p)++

二元运算符有五个优先级。乘法类运算符优先级最高,然后是加法类运算符,比较类运算符,&&(逻辑AND),最后是|| (逻辑OR):

 优先级        运算符
    5             *  /  %  <<  >>  &  &^
    4             +  -  |  ^
    3             ==  !=  <  <=  >  >=
    2             &&
    1             ||

具有相同优先级的二元运算符从左到右依次计算。例如x / y * z(x / y) * z相同。

三 算数运算符

算术运算符应用于数字,并产生与第一个操作数相同类型的结果。四个标准算术运算符(+-*/)适用于整数、浮点数和复数类型;+也适用于字符串。按位逻辑和移位运算符仅适用于整数。

+    和(sum)                        整型, 浮点数, 复数, 字符串
-    差(difference)                 整型, 浮点数, 复数
*    积(product)                    整型, 浮点数, 复数
/    商(quotient)                   整型, 浮点数, 复数
%    余数(remainder)                整型

&    按位与(bitwise AND)             整型
|    按位或(bitwise OR)              整型
^    按位异或(bitwise XOR)           整型
&^   按位清除(bit clear (AND NOT))   整型

<<   左移位             整型 << 无符号整型
>>   右移位             整型 >> 无符号整型

四 整数运算符

对于两个整数值xy,整数商q = x / y和余数r = x % y满足以下关系:

x = q*y + r  and  |r| < |y|
x / y 为向零取整(参考模除
 x     y     x / y     x % y
 5     3       1         2
-5     3      -1        -2
 5    -3      -1         2
-5    -3       1        -2

该规则的一个例外是,如果被除数x是int类型的最小负数,则由于二的补码整型溢出,商q = x / -1 等于x(且r = 0):

                         x, q
int8                     -128
int16                  -32768
int32             -2147483648
int64    -9223372036854775808
一的补码(one's complement) 指的是正数=原码,负数=反码
二的补码(two's complement) 指的就是通常所指的补码

如果被除数x是非负数,并且除数是2的常数幂n,则除法可以用右移(>>)代替,并且计算余数可以用按位与(&)运算代替:

 x     x / 4     x % 4     x >> 2     x & 3
 11      2         3         2          3
-11     -2        -3        -3          1
取余:x & (2<<n - 1)

移位运算符将左操作数移位右操作数指定的移位计数。如果左操作数是有符号整数,则它们执行算术移位;如果是无符号整数,则它们执行逻辑移位。移位数没有上限。对于移位计数n,移位的行为就好像左操作数用1移位n次。结果,x << 1x * 2相同,x >> 1x / 2相同,但向负无穷大取整。

对于整数操作数,一元运算符+-^定义如下:

+x                         表示 0 + x
-x    相反数(negation)      表示 0 - x
^x    按位补码              表示 m ^ x  对于无符号x, m="所有位设置为1";对于有符号x, m=-1

4.1 整数溢出

对于无符号整数值,运算符+,-,*<<以2n为模计算,其中n是无符号整数类型的位宽。简言之,这些无符号整数运算在溢出时丢弃高位,且程序可能依赖于“循环查找”("wrap around")。

对于有符号整数,运算符+,-,*<<可以合法的溢出,并且结果值存在,且由有符号整数、运算符及操作数明确地定义。此时溢出不会导致运行时panic。在假设不发生溢出的情况下,编译器可能不会优化代码。例如,它可能不会假设x < x + 1总是成立。

4.2 浮点运算符

对于浮点数和复数,+xx相同,而-x则是x的否定。IEEE-754标准中没有规定浮点或复数除零的结果,是否发生运行时恐慌是特定于实现的。

in Java: 2.3 / 0 // Infinity
in Go: x := 2.3; x / 0 // +Inf

具体实现中可以将多个浮点运算组合成单个融合运算,可能跨语句,并且产生与通过单独执行和舍入指令而获得的值不同的结果。显式浮点类型转换将舍入到目标类型的精度,从而防止会丢弃该舍入的融合。

例如,某些架构提供了“积和熔加运算”(fused multiply and add, FMA)指令,该指令计算x*y + z时不舍入中间结果x*y。以下示例展示了Go何时可以使用这种指令:

// FMA 允许计算 r, 因为 x*y 未显示舍入:
r  = x*y + z
r  = z;   r += x*y
t  = x*y; r = t + z
*p = x*y; r = *p + z
r  = x*y + float64(z)

// FMA 不允许计算 r, 因为它会舍入 x*y:
r  = float64(x*y) + z
r  = z; r += float64(x*y)
t  = float64(x*y); r = t + z

积和熔加运算,又称融合乘加运算(fused multiply-add, FMA)
融合乘加运算的操作和乘积累加的基本一样,对于浮点数的操作也是一条指令完成。但不同的是,非融合乘加的乘积累加运算,处理浮点数时,会先完成b×c的乘积,将其结果数值修约到N个比特,然后才将修约后的结果与寄存器a的数值相加,再把结果修约到N个比特;融合乘加则是先完成a+b×c的操作,获得最终的完整结果后方才修约到N个比特。由于减少了数值修约次数,这种操作可以提高运算结果的精度,以及提高运算效率和速率。

积和融加运算可以显著提升像是这些运算的性能和精度:

  • 点积
  • 矩阵乘法
  • 多项式方程求解(像是秦九韶算法等)
  • 牛顿法求解函數的零点

积和融加运算通常被依靠用来获取更精确的运算结果。然而,Kahan指出,如果不加思索地使用这种运算操作,在某些情况下可能会带来问题。[1]像是平方差公式x2 − y2,它等价于 ((x×x) − y×y),若果x与y已知数值,使用积和融加运算来求结果,哪怕x = y时,因为在进行首次乘法操作时无视低位的有效比特,可能会使运算结果出错,如果是多步运算,第一步就出错则会连累后续的运算结果接连出错,比如前述的平方差求值后,再取结果的平方根,那么这个结果也会出错。

4.3 字符串连接

字符串可以使用+运算符或+=赋值运算符:

s := "hi" + string(c)
s += " and good bye"

字符串连接会生成新字符串。

字符串是不可变量

五 比较运算符

比较运算符比较两个操作数并产生无类型的布尔值。

==    等于
!=    不等于
<     小于
<=    小于等于
>     大于
>=    大于等于

在任何比较中,第一个操作数必须可以分配给第二个操作数的类型,反之亦然。

相等性运算符==!=适用于可比较操作数;排序运算符<<=>,和>=适用于可排序操作数。规则如下:

  • 布尔值是可比较的。如果两个布尔值都为true或都为false,则它们相等。
  • 通常,整数值既是可比较的,也是可排序的。
  • 根据IEEE-754标准的定义,浮点数既是可比较的,也是可排序的。
  • 复数是可比较的。如果两个复数uv的实部和虚部都相等(real(u) == real(v),且imag(u) == imag(v)),则它们相等。
  • 字符串是可比较的,并且可按字节顺序排序。
  • 指针是可比较的。如果两个指针同时指向同一个变量或同时为nil,则它们相等。指向不同零值内存大小变量的指针可能不相等(例如int8int16)。
  • 信道是可比较的。如果两个信道是由相同的make调用创建或都为nil,则它们相等。
  • 接口值是可比较的。如果两个接口值具有相同的动态类型和动态值或都为nil,则它们相等。
  • 当类型X的值是可比较的,且X实现了类型T时,非接口类型X的值x与接口类型T的值t是可比较的。如果t的动态类型与X相同且t的动态值与x相等,则它们相等。
  • 如果结构类型(的值)的所有字段都是可比较的,则这些值是可比较的。如果两个结构值相应的非空字段值相等,则它们相等。
  • 如果数组元素的类型值是可比较的,则数组值就是可比较的。如果两个数组的所有元素相等,则这两个数组相等。

动态类型相同的两个接口值进行比较时,如果它们不是可比较的,则会引发运行时恐慌。该行为不仅适用于直接接口值之间的比较,同样也适用于接口值的数组或有接口值字段的结构类型之间的比较。

切片,映射,或函数值虽不是可比较的,但可以与预声明标识符nil进行比较。指针,信道以及接口值同样也可以同nil进行比较。

const c = 3 < 4            // c is the untyped boolean constant true

type MyBool bool
var x, y int
var (
    // The result of a comparison is an untyped boolean.
    // The usual assignment rules apply.
    b3        = x == y // b3 has type bool
    b4 bool   = x == y // b4 has type bool
    b5 MyBool = x == y // b5 has type MyBool
)

六 逻辑运算符

逻辑运算又称为布尔运算。逻辑运算符应用于布尔值,并生成与操作数相同类型的结果值。右操作数的求值是有条件的。

&&    逻辑与     p && q, 当 p 为 true 时才计算q,否则表达式为 false
||    逻辑或     p || q, 当 p 为 true 时表达式为 true,否则计算q
!     逻辑非     !p    , 即 非p
逻辑与,又称短路与;逻辑或又称短路或。上述说右操作数的求值是有条件的,就是指右操作数是否参与求值,取决于左操作数的求值结果。

七 取址运算符

取址运算符用符号&表示。

对于一个类型为T的操作数x,取址操作&x会生成一个类型为*T并指向x的指针。操作数必须是可寻址的,即:变量,指针重定向或切片索引运算;或可寻址结构操作数的字段选择器;或可寻址数组的数组索引运算。一个例外是,x也可以是一个复合字面量(可能带括号)如果对x的计算会引发运行时恐慌,那么对&x的计算也是一样。

对于指针类型*T的操作数x,指针重定向*x表示由x指向类型T的变量。如果xnil,则尝试对*x进行求值会引发运行时恐慌。

&x
&a[f(2)]
&Point{2, 3}
*p
*pf(x)

var x *int = nil
*x   // 会引发运行时panic
&*x  // 会引发运行时panic

八 接收运算符

接收运算符用有向箭头<-表示,箭头方向表示数据流转的方向。

对于一个信道类型的操作数ch,接收运算<-ch的值是指从信道ch接收到的值。信道方向必须允许接收操作,并且接收运算的类型是信道元素的类型。在已关闭信道上的接收运算总是可以立即处理(无阻塞):在接收完之前发送的任何值之后将产生该元素类型的零值。

v1 := <-ch
v2 = <-ch
f(<-ch)
<-strobe  // wait until clock pulse and discard received value

用于赋值或初始化的特殊形式的接收表达式:

x, ok = <-ch
x, ok := <-ch
var x, ok = <-ch
var x, ok T = <-ch

这会产生一个额外的无类型布尔结果(ok),表示通信是否成功。如果接收到的值是通过到信道的成功发送操作传递的,则ok的值为true;如果是由于信道关闭并且为空而产生零值,则为false

参考:
https://golang.org/ref/spec#Operators
https://zh.wikipedia.org/wiki/%E4%B9%98%E7%A9%8D%E7%B4%AF%E5%8A%A0%E9%81%8B%E7%AE%97
https://zh.wikipedia.org/wiki/FMA%E6%8C%87%E4%BB%A4%E9%9B%86

文章目录