运算操作符

本文将介绍适用于基本类型值的各种运算操作符。

关于本文中的内容和一些解释

本文只介绍算术运算符、位运算符、比较运算符、布尔运算符和字符串衔接运算符。 这些运算符要么是二元的(需要两个操作数),要么是一元的(需要一个操作数)。 所有这些运算符运算都只返回一个结果。操作数常常也称为操作值。

本文中的解释不追求描述的完全准确性。 比如,当我们说一个二元运算符运算需要其涉及的两个操作数类型必须一样的时,这指: 类似的,当我们说一个运算符(一元或者二元)运算要求其涉及的某个操作数的类型必须为某个特定类型时,这指:

常量表达式

在继续下面的章节之前,我们需要知道什么叫常量表达式和关于常量表达式估值的一个常识。 表达式的概念将在表达式和语句一文中得到解释。 目前我们只需知道本文中所提及的大多数运算都属于表达式。 当一个表达式中涉及到的所有操作数都是常量时,此表达式称为一个常量表达式。 一个常量表达式的估值是在编译阶段进行的。一个常量表达式的估值结果依然是一个常量。 如果一个表达式中涉及到的操作数中至少有一个不为常量,则此表达式称为非常量表达式。

算术运算符

Go支持五个基本二元算术运算符:

字面形式 名称 对两个运算数的要求
+ 加法 两个运算数的类型必须相同并且为基本数值类型。
- 减法
* 乘法
/ 除法
% 余数 两个运算数的类型必须相同并且为基本整数数值类型。

Go支持六种位运算符(也属于算术运算):

字面形式 名称 对两个操作数的要求以及机制解释

&

位与

两个操作数的类型必须相同并且为基本整数数值类型。

机制解释(下标2表明一个字面形式为二进制):
  • 11002 & 10102 得到 10002
  • 11002 | 10102 得到 11102
  • 11002 ^ 10102 得到 01102
  • 11002 &^ 10102 得到 01002

|

位或

^

(位)异或

&^

清位


<<


左移位

左操作数必须为一个整数,右操作数必须为一个无符号整数类型的类型确定值或者一个可以表示成uint值的类型不确定(常数)值(1)

机制解释:
  • 11002 << 3 得到 11000002(低位补零)
  • 11002 >> 3 得到 12(低位被舍弃)

如果左操作数的类型为一个有符号整数,则在右移位运算中,左操作数的符号位(即最高位)将总是保留在结果中。 比如如果左操作数-128的类型为int8(二进制表示为100000002), 则100000002 >> 1的结果为110000002(即-64)。



>>


右移位

(1) 从Go 1.13版本开始,移位运算中的右操作数的要求有可能将被放宽为任何整数,既可以为无符号整数,也可以为有符号整数。

Go也支持三个一元算术运算符:

字面形式 名称 解释
+ 取正数 +n等价于0 + n.
- 取负数 -n等价于0 - n.
^ 位反(或位补) ^n等价于m ^ n,其中mn同类型并且它的二进制表示中所有比特位均为1。 比如如果n的类型为int8,则m的值为-1;如果n的类型为uint8,则m的值为255
注意:

一些运算符的使用示例:
func main() {
	var (
		a, b float32 = 12.0, 3.14
		c, d int16   = 15, -6
		e	uint8   = 7
	)

	// 这些行编译没问题。
	_ = 12 + 'A' // 两个类型不确定操作数(都为数值类型)
	_ = 12 - a   // 12将被当做a的类型(float32)使用。
	_ = a * b    // 两个同类型的类型确定操作数。
	_ = c % d
	_, _ = c + int16(e), uint8(c) + e
	_, _, _, _ = a / b, c / d, -100 / -9, 1.23 / 1.2
	_, _, _, _ = c | d, c & d, c ^ d, c &^ d
	_, _, _, _ = d << e, 123 >> e, e >> 3, 0xF << 0
	_, _, _, _ = -b, +c, ^e, ^-1

	// 这些行编译将失败。
	_ = a % b   // error: a和b都不是整数
	_ = a | b   // error: a和b都不是整数
	_ = c + e   // error: c和e的类型不匹配
	_ = b >> 5  // error: b不是一个整数
	_ = c >> -5 // error: -5不是一个无符号整数
	_ = e << c  // error: c不是一个无符号整数
}

关于算术运算的结果

除了移位运算,对于一个二元算术运算, 对于移位运算,结果规则有点小复杂。首先移位运算的结果肯定都是整数。 一些非移位算术运算的例子:
func main() {
	// 三个类型不确定常量。它们的默认类型
	// 分别为:int、rune和complex64.
	const X, Y, Z = 2, 'A', 3i


	var a, b int = X, Y // 两个类型确定值

	// 变量d的类型被推断为Y的默认类型:rune(亦即int32)。
	d := X + Y
	// 变量e的类型被推断为a的类型:int。
	e := Y - a
	// 变量f的类型和a及b的类型一样:int。
	f := a * b
	// 变量g的类型被推断为Z的默认类型:complex64。
	g := Z * Y

	// 2 65 (+0.000000e+000+3.000000e+000i)
	println(X, Y, Z)
	// 67 63 130 (+0.000000e+000+1.950000e+002i)
	println(d, e, f, g)
}

一个移位算术运算的例子:
const N = 2
// A == 6,它是一个默认类型为int的类型不确定值。
const A = 3.0 << N
// B == 6,它是一个类型为int8的类型确定值。
const B = int8(3.0) << N

var m = uint(32)
// 下面的三行是相互等价的。
var x int64 = 1 << m  // 1的类型将被设想为int64,而非int
var y = int64(1 << m) // 同上
var z = int64(1) << m // 同上

// 下面这两行编译不通过。
/*
var _ = 1.23 << m // error: 浮点数不能被移位
const _ = 1 << B  // error: 右操作符不是无符号数
*/

注意:如上面已经提到的,上例中的最后一行很可能从Go 1.13开始将能够编译通过。

上面提到的移位运算结果的最后一点类型推断规则有点反常。 这条规则的主要目的是为了防止一些移位运算在32位架构和64位架构的机器上的运算结果出现不一致但不一致却没有被及时发现的情况。 比如如果上面一段代码中第10行(或第9行)的1的类型被推断为它的默认类型int, 则在32位架构的机器上,x的取值将被截断为0,而在64位架构的机器上,x的取值将为232。 因为m是一个变量,在32位架构的机器上,第9行和第10行并不会编译报错。 这将导致Go程序员在不经意间写出没有料到的和难以觉察的bug。 因此,第9行和第10行中的1的类型被推断为int64(最终的设想结果类型),而不是它们的默认类型int

下面这段代码展示了对于左操作数为类型不确定值的移位运算,编译结果因右操作数是否为常量而带来的不同结果:
const n = uint(2)
var m = uint(2)

// 这两行编译没问题。
var _ float64 = 1 << n
var _ = float64(1 << n)

// 这两行编译失败。
var _ float64 = 1 << m  // error
var _ = float64(1 << m) // error
上面这段代码最后两行编译失败是因为它们都等价于下面这两行:
var _ = float64(1) << m
var _ = 1.0 << m // error: shift of type float64

关于溢出

上一篇文章提到了

对于一个算数运算的结果,上述规则同样适用。

示例:
// 结果为非常量
var a, b uint8 = 255, 1
var c = a + b  // c==0。a+b是一个非常量表达式,
               // 结果中溢出的高位比特将被截断舍弃。
var d = a << b // d == 254。同样,结果中溢出的
               // 高位比特将被截断舍弃。

// 结果为类型不确定常量,允许溢出其默认类型。
const X = 0x1FFFFFFFF * 0x1FFFFFFFF // 没问题,尽管X溢出
const R = 'a' + 0x7FFFFFFF          // 没问题,尽管R溢出

// 运算结果或者转换结果为类型确定常量
var e = X                // error: X溢出int。
var h = R                // error: R溢出rune。
const Y = 128 - int8(1)  // error: 128溢出int8。
const Z = uint8(255) + 1 // error: 256溢出uint8。

关于除法和余数运算

假设两个操作数xy的类型为同一个整数类型, 则它们通过除法和余数运算得到的商q= x / y)和余数r= x % y)满足x == q*y + r|r| < |y|)。如果余数r不为零,则它的符号和被除数x相同。商q的结果为x / y向零靠拢截断。

如果除数y是一个常量,则它必须不为0,否则编译不通过。 如果它是一个整数型非常量,则在运行时刻将抛出一个恐慌(panic)。 恐慌类似与某些其它语言中的异常(exception)。 我们将在以后的文章中了解到Go中的恐慌和恐慌恢复机制。 如果除数y非整数型的非常量,则运算结果为一个无穷大(Inf,当被除数不为0时)或者NaN(not a number,当被除数为0时)。

示例:
println( 5/3,   5%3)  // 1 2
println( 5/-3,  5%-3) // -1 2
println(-5/3,  -5%3)  // -1 -2
println(-5/-3, -5%-3) // 1 -2

println(5.0 / 3.0)     // 1.666667
println((1-1i)/(1+1i)) // -1i

var a, b = 1.0, 0.0
println(a/b, b/b) // +Inf NaN

_ = int(a)/int(b) // 编译没问题,但在运行时刻将造成恐慌。

// 这两行编译不通过。
println(1.0/0.0) // error: 除数为0
println(0.0/0.0) // error: 除数为0

op=运算符

对于一个二元算数运算符op,语句x = x op y可以被简写为x op= y。 在这个简写的语句中,x只会被估值一次。

示例:
var a, b int8 = 3, 5
a += b
println(a) // 8
a *= a
println(a) // 64
a /= b
println(a) // 12
a %= b
println(a) // 2
b <<= uint(a)
println(b) // 20

自增和自减操作符

和很多其它流行语言一样,Go也支持自增(++)和自减(--)操作符。 不过和其它语言不一样的是,自增(aNumber++)和自减(aNumber--)操作操作没有返回值, 所以它们不能当做表达式来使用。 另一个显著区别是,在Go中,自增(++)和自减(--)操作符只能后置,不能前置。

一个例子:
package main

func main() {
	a, b, c := 12, 1.2, 1+2i
	a++ // ok. <=> a += 1 <=> a = a + 1
	b-- // ok. <=> b -= 1 <=> b = b - 1
	c++ // ok.

	// 下面这些行编译不通过。
	/*
	_ = a++
	_ = b--
	_ = c++
	++a
	--b
	++c
	*/
}

字符串衔接运算符

上面已经提到了,加法运算符也可用做字符串衔接运算符。
字面形式 名称 对两个操作数的要求
+ 字符串衔接 两个操作数必须为同一类型的字符串值。

+=运算符也适用于字符串衔接。

示例:
println("Go" + "lang") // Golang
var a = "Go"
a += "lang"
println(a) // Golang

如果一个字符串衔接运算中的一个操作值为类型确定的,则结果字符串是一个类型和此操作数类型相同的类型确定值。 否则,结果字符串是一个类型不确定值(肯定是一个常量)。

布尔运算符

Go支持两种布尔二元运算符和一种布尔一元运算符。

字面形式 名称 对操作值的要求
&& 布尔与(二元) 两个操作值的类型必须为同一布尔类型。
|| 布尔或(二元)
! 布尔否(一元) 唯一的一个操作值的类型必须为一个布尔类型。

我们可以用下一小节介绍的不等于操作符!=来做为布尔异或操作符。

机理解释:
// x    y       x && y   x || y   !x      !y
true    true    true     true     false   false
true    false   false    true     false   true
false   true    false    true     true    false
false   false   false    false    true    true

如果一个布尔运算中的一个操作值为类型确定的,则结果为一个和此操作值类型相同的类型确定值。 否则,结果为一个类型不确定布尔值。

比较运算符

Go支持6种比较运算符:

字面形式 名称 对两个操作值的要求


==


等于

如果两个操作数都为类型确定的,则它们的类型必须一样,或者其中一个操作数可以隐式转换为另一个操作数的类型。 两者的类型必须都为可比较类型(将在以后的文章中介绍)。

如果只有一个操作数是类型确定的,则另一个类型不确定操作数必须可以隐式转换到类型确定操作数的类型。

如果两个操作数都是类型不确定的,则它们必须同时为两个类型不确定布尔值、两个类型不确定字符串值或者另个类型不确定数字值。



!=


不等于
< 小于 两个操作值的类型必须相同并且它们的类型必须为整数类型、浮点数类型或者字符串类型。
<= 小于或等于
> 大于
>= 大于或等于

比较运算的结果总是一个类型不确定布尔值。 如果一个比较运算中的两个操作数都为常量,则结果布尔值也为一个常量。

以后,如果我们说两个值可以比较,我们的意思是说这两个值可以用==或者!=运算符来比较。 我们将在以后的文章中,我们将了解到某些类型的值是不能比较的。

注意,并非所有的实数在内存中都可以被精确地表示,所以比较两个浮点数或者复数的结果并不是很可靠。 在编程中,我们常常比较两个浮点数的差值是否小于一个阙值来检查两个浮点数是否相等。

操作符运算的优先级

Go中的操作符运算的优先级和其它流行语言有一些差别。 下面列出了本文介绍的操作符的优先级。 每行中的操作符的优先级是一样的。优先级逐行递减。

*   /   %   <<  >>  &   &^
+   -   |   ^
==  !=  <   <=  >   >=
&&
||

一个和其它流行语言明显的差别是,移位运算<<>>的优先级比加减法+-的优先级要高。

一个表达式(做为一个子表达式)可以出现在另一个表达式中。 这个子表达式的估值结果将成为另一个表达式的一个操作数。 在这样的复杂表达式中,对于相同优先级的运算,它们将从左到右进行估值。 和很多其它语言一样,我们也可用一对小括号()来提升一个子运算的优先级。

更多关于常量表达式

常量子表达式的顺序有可能影响到最终的估值结果。

下面这个声明的变量将被初始化为2.2,而不是2.7。 优先级更高的子表达式3/2是一个常量表达式,所以它将在编译阶段被估值。 根据上面介绍的规则,在运算中,32都被视为int,所以3/2的估值结果为1。 在常量表达式1.2 + 1的运算中,两个操作数的类型被视为float64,所以最终的估值结果为2.2
var x = 1.2 + 3/2

再比如下例,在一个常量声明中,3/2先被估值,其结果为1,所以最终的估值结果为0.1。 在第二个常量声明中,0.1*3先被估值,其结果为0.3,所以最终的估值结果为0.15
package main

const x = 3/2*0.1
const y = 0.1*3/2

func main() {
	println(x) // +1.000000e-001
	println(y) // +1.500000e-001
}

更多其它操作符

Go中还有一些其它操作符。它们将在后续其它适当的文章中介绍。

Go语言101项目目前同时托管在GithubGitlab上。 欢迎各位在这两个项目中通过提交bug和PR的方式来改进完善Go语言101中的各篇文章。

本书微信公众号名称为"Go 101"。每个工作日此公众号将尽量发表一篇和Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。

赞赏