Go语言 运算操作符 - 顺便介绍了更多的类型推断规则

2023-02-16 17:36 更新

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

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

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

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

  • 如果这两个操作数都是类型确定值,则它们的类型必须相同,或者其中一个操作数可以被隐式转换到另一个操作数的类型。
  • 如果其中只有一个操作数是类型确定的,则要么另外一个类型不确定操作数可以表示为此类型确定操作数的类型的值,要么此类型不确定操作数的默认类型的任何值可以被隐式转换到此类型确定操作数的类型。
  • 如果这两个操作数都是类型不确定的,则它们必须同时都为两个布尔值,同时都为两个字符串值,或者同时都为两个基本数字值。

类似的,当我们说一个运算符(一元或者二元)运算要求其涉及的某个操作数的类型必须为某个特定类型时,这指:

  • 如果这个操作数是类型确定的,则它的类型必须为所要求的特定类型,或者此操作数可以被隐式转换为所要求的特定类型。
  • 如果一个操作数是类型不确定的,则要么此操作数可以表示为所要求的特定类型值,要么此操作数的默认类型的任何值可以被隐式转换为所要求的特定类型。

常量表达式

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

算术运算符

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

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

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

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

&

位与

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

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

|

位或

^

(位)异或

&^

清位


<<


左移位

左操作数必须为一个整数,右操作数也必须为一个整数(如果它是一个常数,则它必须非负),但它们的类型可以不同。 (注意:在Go 1.13之前,右操作数必须为一个无符号整数类型的类型确定值或者一个可以表示成uint值的类型不确定常数值。)

一个负右操作数(非常数)将在运行时刻造成一个恐慌。

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

注意,在右移运算中,左边空出来的位(即高位)全部用左操作数的最高位(即正负号位)填充。 比如如果左操作数-128的类型为int8(二进制补码表示为100000002), 则100000002 >> 2的二进制补码结果为111000002(即-32)。



>>


右移位

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

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

注意:

  • 在很多其它流行语言中,位反运算符是用​~​表示的。
  • 和一些其它流行语言一样,加号运算符​+​也可用做字符串衔接运算符(见下)。
  • 和C及C++语言一样,​*​除了可以当作乘号运算符,它也可以用做指针解引用运算符; ​&​除了可以当作位与运算符,它也可以用做取地址运算符。 后面的指针一文将详解内存地址和指针类型。
  • 和Java不一样,Go支持无符号数,所以Go不需要无符号右移运算符​>>>​。
  • Go不支持幂运算符, 我们必须使用​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的类型不确定值。

对于移位运算,结果规则有点小复杂。首先移位运算的结果肯定都是整数。

  • 如果左操作数是一个类型确定值(则它的类型必定为整数),则此移位运算的结果也是一个和左操作数类型相同的类型确定值。
  • 如果左操作数是一个类型不确定值并且右操作数是一个常量,则左操作数将总是被视为一个整数。 如果它的默认类型不是一个整数(runeint),则它的默认类型将被视为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

关于除法和余数运算

假设两个操作数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中还有一些其它操作符。它们将在后续其它适当的文章中介绍。


以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号