运算操作符
本文将介绍适用于基本类型值的各种运算操作符。
关于本文中的内容和一些解释
本文只介绍算术运算符、位运算符、比较运算符、布尔运算符和字符串衔接运算符。 这些运算符要么是二元的(需要两个操作数),要么是一元的(需要一个操作数)。 所有这些运算符运算都只返回一个结果。操作数常常也称为操作值。
本文中的解释不追求描述的完全准确性。 比如,当我们说一个二元运算符运算需要其涉及的两个操作数类型必须一样的时,这指:
- 如果这两个操作数都是类型确定值,则它们的类型必须相同,或者其中一个操作数可以被隐式转换到另一个操作数的类型。
- 如果其中只有一个操作数是类型确定的,则要么另外一个类型不确定操作数可以表示为此类型确定操作数的类型的值,要么此类型不确定操作数的默认类型的任何值可以被隐式转换到此
类型确定操作数的类型。 - 如果这两个操作数都是类型不确定的,则它们必须同时都为两个布尔值,同时都为两个字符串值,或者同时都为两个基本数字值。
类似的,当我们说一个运算符(一元或者二元)运算要求其涉及的某个操作数的类型必须为某个特定类型时,这指:
- 如果这个操作数是类型确定的,则它的类型必须为所要求的特定类型,或者此操作数可以被隐式转换为所要求的特定类型。
- 如果一个操作数是类型不确定的,则要么此操作数可以表示为所要求的特定类型值,要么此操作数的默认类型的任何值可以被隐式转换为所要求的特定类型。
常量表达式
在继续下面的章节之前,我们需要知道什么叫常量表达式和关于常量表达式估值的一个常识。 表达式的概念将在表达式和语句一文中得到解释。 目前我们只需知道本文中所提及的大多数运算都属于表达式。 当一个表达式中涉及到的所有操作数都是常量时,此表达式称为一个常量表达式。 一个常量表达式的估值是在编译阶段进行的。一个常量表达式的估值结果依然是一个常量。 如果一个表达式中涉及到的操作数中至少有一个不为常量,则此表达式称为非常量表达式。
算术运算符
Go支持五个基本二元算术运算符:
字面形式 | 名称 | 对两个运算数的要求 |
---|---|---|
+ | 加法 | 两个运算数的类型必须相同并且为基本数值类型 |
- | 减法 | |
* | 乘法 | |
/ | 除法 | |
% | 余数 | 两个运算数的类型必须相同并且为基本整数数值类型 |
Go支持六种位运算符(也属于算术运算):
字面形式 | 名称 | 对两个操作数的要求以及机制解释 |
---|---|---|
& | 位与 | 两个操作数的类型必须相同并且为基本整数数值类型。
机制解释(下标2表明一个字面量为二进制):
|
| | 位或 | |
^ | (位)异或 | |
&^ | 清位 | |
<< | 左移位 | 左操作数必须为一个整数,右操作数也必须为一个整数(如果它是一个常数,则它必须非负),但它们的类型可以不同。 (注意:在Go 1.13 之前,右操作数必须为一个无符号整数类型的类型确定值或者一个可以表示成uint值的类型不确定常数值。)
一个负右操作数(非常数)将在运行时刻造成一个恐慌。
机制解释:
|
>> | 右移位 |
Go也支持三个一元算术运算符:
字面形式 | 名称 | 解释 |
---|---|---|
+ | 取正数 | +n等价于0 + n. |
- | 取负数 | -n等价于0 - n. |
^ | 位反(或位补) | ^n等价于m ^ n,其中m和n同类型并且它的二进制表示中所有比特位均为1。 比如如果n的类型为int8,则m的值为-1;如果n的类型为uint8,则m的值为255。 |
注意:
- 在很多其它流行语言中,位反运算符是用
~
表示的。 - 和一些其它流行语言一样,加号运算符
+
也可用做字符串衔接运算符(见下)。 - 和C及C++语言一样,
*
除了可以当作乘号运算符,它也可以用做指针解引用运算符;&
除了可以当作位与运算符,它也可以用做取地址运算符。 后面的指针一文将详解内存地址和指针类型。 - 和Java不一样,Go支持无符号数,所以Go不需要无符号右移运算符
>>>
。 - Go不支持幂运算符, 我们必须使用
math
标准库包中的Pow
函数来进行幂运算。 下一篇文章将详解包和包引入。 - 清位运算符
&^
是Go中特有的一个运算符。m &^ n
等价于m & (^n)
。
一些运算符的使用示例:
1 | func main() { |
关于溢出
上一篇文章提到了
- 一个类型确定数字型常量所表示的值是不能溢出它的类型的表示范围的。
- 一个类型不确定数字型常量所表示的值是可以溢出它的默认类型的表示范围的。 当一个类型不确定数字常量值溢出它的默认类型的表示范围时,此数值不会被截断(亦即回绕)。
- 将一个非常量数字值转换为其它数字类型时,此非常量数字值可以溢出转化结果的类型。 在此转换中,当溢出发生时,转化结果为此非常量数字值的截断(亦即回绕)表示。
对于一个算数运算的结果,上述规则同样适用。
示例:
1 | // 结果为非常量 |
关于算术运算的结果
除了移位运算,对于一个二元算术运算,
- 如果它的两个操作数都为类型确定值,则此运算的结果也是一个和这两个操作数类型相同的类型确定值。
- 如果只有一个操作数是类型确定的,则此运算的结果也是一个和此类型确定操作数类型相同的类型确定值。 另一个类型不确定操作数的类型将被推断为(或隐式转换为)此类型确定操作数的类型。
- 如果它的两个操作数均为类型不确定值,则此运算的结果也是一个类型不确定值。 在运算中,两个操作数的类型将被设想为它们的默认类型中一个(按照此优先级来选择:
complex128
高于float64
高于rune
高于int
)。 结果的默认类型同样为此设想类型。 比如,如果一个类型不确定操作数的默认类型为int
,另一个类型不确定操作数的默认类型为rune
, 则前者的类型在运算中也被视为rune
,运算结果为一个默认类型为rune
的类型不确定值。
对于移位运算,结果规则有点小复杂。首先移位运算的结果肯定都是整数。
- 如果左操作数是一个类型确定值(则它的类型必定为整数),则此移位运算的结果也是一个和左操作数类型相同的类型确定值。
- 如果左操作数是一个类型不确定值并且右操作数是一个常量,则左操作数将总是被视为一个整数。 如果它的默认类型不是一个整数(
rune
或int
),则它的默认类型将被视为int
。 此移位运算的结果也是一个类型不确定值并且它的默认类型和左操作数的默认类型一致。 - 如果左操作数是一个类型不确定值并且右操作数是一个非常量,则左操作数将被首先转化为运算结果的期待设想类型。 如果期待设想类型并没有被指定,则左操作数的默认类型将被视为它的期待设想类型。 如果此期待设想类型不是一个基本整数类型,则编译报错。 当然最终运算结果是一个类型为此期待设想类型的类型确定值。
一些非移位算术运算的例子:
1 | func main() { |
一个移位算术运算的例子:
1 | const N = 2 |
上面提到的移位运算结果的最后一点类型推断规则有点反常。 这条规则的主要目的是为了防止一些移位运算在32位架构和64位架构的机器上的运算结果出现不一致但不一致却没有被及时发现的情况。 比如如果上面一段代码中第10行(或第9行)的1
的类型被推断为它的默认类型int
, 则在32位架构的机器上,x
的取值在运行时刻将被截断为0,而在64位架构的机器上,x
的取值在运行时刻将为232。 因为m
是一个变量,在32位架构的机器上,第9行和第10行并不会在编译时刻报错。 这将导致Go程序员在不经意间写出没有料到的和难以觉察的bug。 因此,第9行和第10行中的1
的类型被推断为int64
(最终的设想结果类型),而不是它们的默认类型int
。
下面这段代码展示了对于左操作数为类型不确定值的移位运算,编译结果因右操作数是否为常量而带来的不同结果:
1 | const n = uint(2) |
上面这段代码最后两行编译失败是因为它们都等价于下面这两行:
1 | var _ = float64(1) << m |
另一个例子:
1 | package main |
上面这个程序打印出2 0
,因为最后两行等价于:
1 | var a = byte(int(1) << n / 128) |
关于除法和余数运算
假设两个操作数x
和y
的类型为同一个整数类型, 则它们通过除法和余数运算得到的商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时)。
示例:
1 | println( 5/3, 5%3) // 1 2 |
op=
运算符
对于一个二元算数运算符op
,语句x = x op y
可以被简写为x op= y
。 在这个简写的语句中,x
只会被估值一次。
示例:
1 | var a, b int8 = 3, 5 |
自增和自减操作符
和很多其它流行语言一样,Go也支持自增(++
)和自减(--
)操作符。 不过和其它语言不一样的是,自增(aNumber++
)和自减(aNumber--
)操作没有返回值, 所以它们不能当做表达式来使用。 另一个显著区别是,在Go中,自增(++
)和自减(--
)操作符只能后置,不能前置。
一个例子:
1 | package main |
字符串衔接运算符
上面已经提到了,加法运算符也可用做字符串衔接运算符。
字面形式 | 名称 | 对两个操作数的要求 |
---|---|---|
+ | 字符串衔接 | 两个操作数必须为同一类型的字符串值。 |
+=
运算符也适用于字符串衔接。
示例:
1 | println("Go" + "lang") // Golang |
如果一个字符串衔接运算中的一个操作值为类型确定的,则结果字符串是一个类型和此操作数类型相同的类型确定值。 否则,结果字符串是一个类型不确定值(肯定是一个常量)。
布尔(又称逻辑)运算符
Go支持两种布尔二元运算符和一种布尔一元运算符。
字面形式 | 名称 | 对操作值的要求 |
---|---|---|
&& | 布尔与(二元) | 两个操作值的类型必须为同一布尔类型。 |
|| | 布尔或(二元) | |
! | 布尔否(一元) | 唯一的一个操作值的类型必须为一个布尔类型。 |
我们可以用下一小节介绍的不等于操作符!=来做为布尔异或操作符。
机理解释:
1 | // x y x && y x || y !x !y |
如果一个布尔运算中的一个操作值为类型确定的,则结果为一个和此操作值类型相同的类型确定值。 否则,结果为一个类型不确定布尔值。
比较运算符
Go支持6种比较运算符:
字面形式 | 名称 | 名对两个操作值的要求称 |
---|---|---|
== | 等于 | 如果两个操作数都为类型确定的,则它们的类型必须一样,或者其中一个操作数可以隐式转换为另一个操作数的类型。 两者的类型必须都为可比较类型(将在以后的文章中介绍)。 如果只有一个操作数是类型确定的,则另一个类型不确定操作数必须可以隐式转换到类型确定操作数的类型。 如果两个操作数都是类型不确定的,则它们必须同时为两个类型不确定布尔值、两个类型不确定字符串值或者另个类型不确定数字值。 |
!= | 不等于 | |
< | 小于 | 两个操作值的类型必须相同并且它们的类型必须为整数类型、浮点数类型或者字符串类型。 |
<= | 小于或等于 | |
> | 大于 | |
>= | 大于或等于 |
比较运算的结果总是一个类型不确定布尔值。 如果一个比较运算中的两个操作数都为常量,则结果布尔值也为一个常量。
以后,如果我们说两个值可以比较,我们的意思是说这两个值可以用==
或者!=
运算符来比较。 我们将在以后的文章中,我们将了解到某些类型的值是不能比较的。
注意,并非所有的实数在内存中都可以被精确地表示,所以比较两个浮点数或者复数的结果并不是很可靠。 在编程中,我们常常比较两个浮点数的差值是否小于一个阙值来检查两个浮点数是否相等。
操作符运算的优先级
Go中的操作符运算的优先级和其它流行语言有一些差别。 下面列出了本文介绍的操作符的优先级。 同一行中的操作符的优先级是一样的。优先级逐行递减。
1 | * / % << >> & &^ |
一个和其它流行语言明显的差别是,移位运算<<
和>>
的优先级比加减法+
和-
的优先级要高。
一个表达式(做为一个子表达式)可以出现在另一个表达式中。 这个子表达式的估值结果将成为另一个表达式的一个操作数。 在这样的复杂表达式中,对于相同优先级的运算,它们将从左到右进行估值。 和很多其它语言一样,我们也可用一对小括号()
来提升一个子运算的优先级。
更多关于常量表达式
常量子表达式的顺序有可能影响到最终的估值结果。
下面这个声明的变量将被初始化为2.2
,而不是2.7
。 优先级更高的子表达式3/2
是一个常量表达式,所以它将在编译阶段被估值。 根据上面介绍的规则,在运算中,3
和2
都被视为int
,所以3/2
的估值结果为1
。 在常量表达式1.2 + 1
的运算中,两个操作数的类型被视为float64
,所以最终的估值结果为2.2
。
1 | var x = 1.2 + 3/2 |
再比如下例,在一个常量声明中,3/2
先被估值,其结果为1
,所以最终的估值结果为0.1
。 在第二个常量声明中,0.1*3
先被估值,其结果为0.3
,所以最终的估值结果为0.15
。
1 | package main |
更多其它操作符
Go中还有一些其它操作符。它们将在后续其它适当的文章中介绍。