本文将介绍适用于基本类型值的各种运算操作符。
本文只介绍算术运算符、位运算符、比较运算符、布尔运算符和字符串衔接运算符。 这些运算符要么是二元的(需要两个操作数),要么是一元的(需要一个操作数)。 所有这些运算符运算都只返回一个结果。操作数常常也称为操作值。
在继续下面的章节之前,我们需要知道什么叫常量表达式和关于常量表达式估值的一个常识。 表达式的概念将在表达式和语句一文中得到解释。 目前我们只需知道本文中所提及的大多数运算都属于表达式。 当一个表达式中涉及到的所有操作数都是常量时,此表达式称为一个常量表达式。 一个常量表达式的估值是在编译阶段进行的。一个常量表达式的估值结果依然是一个常量。 如果一个表达式中涉及到的操作数中至少有一个不为常量,则此表达式称为非常量表达式。
Go支持五个基本二元算术运算符:
字面形式 | 名称 | 对两个运算数的要求 |
---|---|---|
+ | 加法 | 两个运算数的类型必须相同并且为基本数值类型。 |
- | 减法 | |
* | 乘法 | |
/ | 除法 | |
% | 余数 | 两个运算数的类型必须相同并且为基本整数数值类型。 |
Go支持六种位运算符(也属于算术运算):
字面形式 | 名称 | 对两个操作数的要求以及机制解释 |
---|---|---|
位与 |
两个操作数的类型必须相同并且为基本整数数值类型。 机制解释(下标2 表明一个字面量为二进制):
|
|
位或 | ||
(位)异或 | ||
清位 | ||
<< |
左移位 |
左操作数必须为一个整数,右操作数也必须为一个整数(如果它是一个常数,则它必须非负),但它们的类型可以不同。
(注意:在Go 1.13之前,右操作数必须为一个无符号整数类型的类型确定值或者一个可以表示成 一个负右操作数(非常数)将在运行时刻造成一个恐慌。 机制解释:
注意,在右移运算中,左边空出来的位(即高位)全部用左操作数的最高位(即正负号位)填充。
比如如果左操作数 |
>> |
右移位 |
Go也支持三个一元算术运算符:
字面形式 | 名称 | 解释 |
---|---|---|
+ | 取正数 |
+n 等价于0 + n .
|
- | 取负数 |
-n 等价于0 - n .
|
^ | 位反(或位补) |
^n 等价于m ^ n ,其中m 和n 同类型并且它的二进制表示中所有比特位均为1。
比如如果n 的类型为int8 ,则m 的值为-1 ;如果n 的类型为uint8 ,则m 的值为255 。
|
~
表示的。
+
也可用做字符串衔接运算符(见下)。
*
除了可以当作乘号运算符,它也可以用做指针解引用运算符;
&
除了可以当作位与运算符,它也可以用做取地址运算符。
后面的指针一文将详解内存地址和指针类型。
>>>
。
math
标准库包中的Pow
函数来进行幂运算。
下一篇文章将详解包和包引入。
&^
是Go中特有的一个运算符。
m &^ n
等价于m & (^n)
。
一些运算符的使用示例:
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 << uint(c) // 编译没问题
_ = e << c // 从Go 1.13开始,此行才能编译过
_ = e << -c // 从Go 1.13开始,此行才能编译过。
// 将在运行时刻造成恐慌。
_ = e << -1 // error: 右操作数不能为负(常数)
}
对于一个算数运算的结果,上述规则同样适用。
示例:// 结果为非常量
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。
complex128
高于float64
高于rune
高于int
)。
结果的默认类型同样为此设想类型。
比如,如果一个类型不确定操作数的默认类型为int
,另一个类型不确定操作数的默认类型为rune
,
则前者的类型在运算中也被视为rune
,运算结果为一个默认类型为rune
的类型不确定值。
rune
或int
),则它的默认类型将被视为int
。
此移位运算的结果也是一个类型不确定值并且它的默认类型和左操作数的默认类型一致。
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 == 12,它是一个默认类型为int的类型不确定值。
const A = 3.0 << N
// B == 12,它是一个类型为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: 浮点数不能被移位
*/
上面提到的移位运算结果的最后一点类型推断规则有点反常。
这条规则的主要目的是为了防止一些移位运算在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
另一个例子:
package main
const n = uint(8)
var m = uint(8)
func main() {
println(a, b) // 2 0
}
var a byte = 1 << n / 128
var b byte = 1 << m / 128
上面这个程序打印出2 0
,因为最后两行等价于:
var a = byte(int(1) << n / 128)
var b = byte(1) << m / 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时)。
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
是一个常量表达式,所以它将在编译阶段被估值。
根据上面介绍的规则,在运算中,3
和2
都被视为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中还有一些其它操作符。它们将在后续其它适当的文章中介绍。
Go101.org网站内容包括Go编程各种相关知识(比如Go基础、Go优化、Go细节、Go实战、Go测验、Go工具等)。后续将不断有新的内容加入。敬请收藏关注期待。
本丛书微信公众号(联系方式一)名称为"Go 101"。二维码在网站首页。此公众号将时不时地发表一些Go语言相关的原创短文。各位如果感兴趣,可以搜索关注一下。
《Go语言101》系列丛书项目目前托管在Github上(联系方式二)。欢迎各位在此项目中通过提交bug和PR的方式来改进完善《Go语言101》丛书中的各篇文章。我们可以在项目目录下运行go run .
来浏览和确认各种改动。
本书的twitter帐号为@Golang_101(联系方式三)。玩推的Go友可以适当关注。
你或许对本书作者老貘开发的一些App感兴趣。
sync
标准库包sync/atomic
标准库包