Go語言 運算操作符 - 順便介紹了更多的類型推斷規(guī)則

2023-02-16 17:36 更新

本文將介紹適用于基本類型值的各種運算操作符。

關(guān)于本文中的內(nèi)容和一些解釋

本文只介紹算術(shù)運算符、位運算符、比較運算符、布爾運算符和字符串銜接運算符。 這些運算符要么是二元的(需要兩個操作數(shù)),要么是一元的(需要一個操作數(shù))。 所有這些運算符運算都只返回一個結(jié)果。操作數(shù)常常也稱為操作值。

本文中的解釋不追求描述的完全準(zhǔn)確性。 比如,當(dāng)我們說一個二元運算符運算需要其涉及的兩個操作數(shù)類型必須一樣的時,這指:

  • 如果這兩個操作數(shù)都是類型確定值,則它們的類型必須相同,或者其中一個操作數(shù)可以被隱式轉(zhuǎn)換到另一個操作數(shù)的類型。
  • 如果其中只有一個操作數(shù)是類型確定的,則要么另外一個類型不確定操作數(shù)可以表示為此類型確定操作數(shù)的類型的值,要么此類型不確定操作數(shù)的默認(rèn)類型的任何值可以被隱式轉(zhuǎn)換到此類型確定操作數(shù)的類型。
  • 如果這兩個操作數(shù)都是類型不確定的,則它們必須同時都為兩個布爾值,同時都為兩個字符串值,或者同時都為兩個基本數(shù)字值。

類似的,當(dāng)我們說一個運算符(一元或者二元)運算要求其涉及的某個操作數(shù)的類型必須為某個特定類型時,這指:

  • 如果這個操作數(shù)是類型確定的,則它的類型必須為所要求的特定類型,或者此操作數(shù)可以被隱式轉(zhuǎn)換為所要求的特定類型。
  • 如果一個操作數(shù)是類型不確定的,則要么此操作數(shù)可以表示為所要求的特定類型值,要么此操作數(shù)的默認(rèn)類型的任何值可以被隱式轉(zhuǎn)換為所要求的特定類型。

常量表達式

在繼續(xù)下面的章節(jié)之前,我們需要知道什么叫常量表達式和關(guān)于常量表達式估值的一個常識。 表達式的概念將在表達式和語句一文中得到解釋。 目前我們只需知道本文中所提及的大多數(shù)運算都屬于表達式。 當(dāng)一個表達式中涉及到的所有操作數(shù)都是常量時,此表達式稱為一個常量表達式。 一個常量表達式的估值是在編譯階段進行的。一個常量表達式的估值結(jié)果依然是一個常量。 如果一個表達式中涉及到的操作數(shù)中至少有一個不為常量,則此表達式稱為非常量表達式。

算術(shù)運算符

Go支持五個基本二元算術(shù)運算符:

字面形式 名稱 對兩個運算數(shù)的要求
+ 加法 兩個運算數(shù)的類型必須相同并且為基本數(shù)值類型。
- 減法
* 乘法
/ 除法
% 余數(shù) 兩個運算數(shù)的類型必須相同并且為基本整數(shù)數(shù)值類型。

Go支持六種位運算符(也屬于算術(shù)運算):

字面形式 名稱 對兩個操作數(shù)的要求以及機制解釋

&

位與

兩個操作數(shù)的類型必須相同并且為基本整數(shù)數(shù)值類型。

機制解釋(下標(biāo)2表明一個字面量為二進制):
  • 11002 & 10102 得到 10002
  • 11002 | 10102 得到 11102
  • 11002 ^ 10102 得到 01102
  • 11002 &^ 10102 得到 01002

|

位或

^

(位)異或

&^

清位


<<


左移位

左操作數(shù)必須為一個整數(shù),右操作數(shù)也必須為一個整數(shù)(如果它是一個常數(shù),則它必須非負(fù)),但它們的類型可以不同。 (注意:在Go 1.13之前,右操作數(shù)必須為一個無符號整數(shù)類型的類型確定值或者一個可以表示成uint值的類型不確定常數(shù)值。)

一個負(fù)右操作數(shù)(非常數(shù))將在運行時刻造成一個恐慌。

機制解釋:
  • 11002 << 3 得到 11000002(低位補零)
  • 11002 >> 3 得到 12(低位被舍棄)

注意,在右移運算中,左邊空出來的位(即高位)全部用左操作數(shù)的最高位(即正負(fù)號位)填充。 比如如果左操作數(shù)-128的類型為int8(二進制補碼表示為100000002), 則100000002 >> 2的二進制補碼結(jié)果為111000002(即-32)。



>>


右移位

Go也支持三個一元算術(shù)運算符:

字面形式 名稱 解釋
+ 取正數(shù) +n等價于0 + n.
- 取負(fù)數(shù) -n等價于0 - n.
^ 位反(或位補) ^n等價于m ^ n,其中mn同類型并且它的二進制表示中所有比特位均為1。 比如如果n的類型為int8,則m的值為-1;如果n的類型為uint8,則m的值為255。

注意:

  • 在很多其它流行語言中,位反運算符是用?~?表示的。
  • 和一些其它流行語言一樣,加號運算符?+?也可用做字符串銜接運算符(見下)。
  • 和C及C++語言一樣,?*?除了可以當(dāng)作乘號運算符,它也可以用做指針解引用運算符; ?&?除了可以當(dāng)作位與運算符,它也可以用做取地址運算符。 后面的指針一文將詳解內(nèi)存地址和指針類型。
  • 和Java不一樣,Go支持無符號數(shù),所以Go不需要無符號右移運算符?>>>?。
  • Go不支持冪運算符, 我們必須使用?math?標(biāo)準(zhǔn)庫包中的?Pow?函數(shù)來進行冪運算。 下一篇文章將詳解包和包引入。
  • 清位運算符?&^?是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' // 兩個類型不確定操作數(shù)(都為數(shù)值類型)
	_ = 12 - a   // 12將被當(dāng)做a的類型(float32)使用。
	_ = a * b    // 兩個同類型的類型確定操作數(shù)。
	_ = 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都不是整數(shù)
	_ = a | b   // error: a和b都不是整數(shù)
	_ = c + e   // error: c和e的類型不匹配
	_ = b >> 5  // error: b不是一個整數(shù)
	_ = c >> -5 // error: -5不是一個無符號整數(shù)

	_ = e << uint(c) // 編譯沒問題
	_ = e << c       // 從Go 1.13開始,此行才能編譯過
	_ = e << -c      // 從Go 1.13開始,此行才能編譯過。
	                 // 將在運行時刻造成恐慌。
	_ = e << -1      // error: 右操作數(shù)不能為負(fù)(常數(shù))
}

關(guān)于溢出

上一篇文章提到了

  • 一個類型確定數(shù)字型常量所表示的值是不能溢出它的類型的表示范圍的。
  • 一個類型不確定數(shù)字型常量所表示的值是可以溢出它的默認(rèn)類型的表示范圍的。 當(dāng)一個類型不確定數(shù)字常量值溢出它的默認(rèn)類型的表示范圍時,此數(shù)值不會被截斷(亦即回繞)。
  • 將一個非常量數(shù)字值轉(zhuǎn)換為其它數(shù)字類型時,此非常量數(shù)字值可以溢出轉(zhuǎn)化結(jié)果的類型。 在此轉(zhuǎn)換中,當(dāng)溢出發(fā)生時,轉(zhuǎn)化結(jié)果為此非常量數(shù)字值的截斷(亦即回繞)表示。

對于一個算數(shù)運算的結(jié)果,上述規(guī)則同樣適用。

示例:

// 結(jié)果為非常量
var a, b uint8 = 255, 1
var c = a + b  // c==0。a+b是一個非常量表達式,
               // 結(jié)果中溢出的高位比特將被截斷舍棄。
var d = a << b // d == 254。同樣,結(jié)果中溢出的
               // 高位比特將被截斷舍棄。

// 結(jié)果為類型不確定常量,允許溢出其默認(rèn)類型。
const X = 0x1FFFFFFFF * 0x1FFFFFFFF // 沒問題,盡管X溢出
const R = 'a' + 0x7FFFFFFF          // 沒問題,盡管R溢出

// 運算結(jié)果或者轉(zhuǎn)換結(jié)果為類型確定常量
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。

關(guān)于算術(shù)運算的結(jié)果

除了移位運算,對于一個二元算術(shù)運算,

  • 如果它的兩個操作數(shù)都為類型確定值,則此運算的結(jié)果也是一個和這兩個操作數(shù)類型相同的類型確定值。
  • 如果只有一個操作數(shù)是類型確定的,則此運算的結(jié)果也是一個和此類型確定操作數(shù)類型相同的類型確定值。 另一個類型不確定操作數(shù)的類型將被推斷為(或隱式轉(zhuǎn)換為)此類型確定操作數(shù)的類型。
  • 如果它的兩個操作數(shù)均為類型不確定值,則此運算的結(jié)果也是一個類型不確定值。 在運算中,兩個操作數(shù)的類型將被設(shè)想為它們的默認(rèn)類型中一個(按照此優(yōu)先級來選擇:complex128高于float64高于rune高于int)。 結(jié)果的默認(rèn)類型同樣為此設(shè)想類型。 比如,如果一個類型不確定操作數(shù)的默認(rèn)類型為int,另一個類型不確定操作數(shù)的默認(rèn)類型為rune, 則前者的類型在運算中也被視為rune,運算結(jié)果為一個默認(rèn)類型為rune的類型不確定值。

對于移位運算,結(jié)果規(guī)則有點小復(fù)雜。首先移位運算的結(jié)果肯定都是整數(shù)。

  • 如果左操作數(shù)是一個類型確定值(則它的類型必定為整數(shù)),則此移位運算的結(jié)果也是一個和左操作數(shù)類型相同的類型確定值。
  • 如果左操作數(shù)是一個類型不確定值并且右操作數(shù)是一個常量,則左操作數(shù)將總是被視為一個整數(shù)。 如果它的默認(rèn)類型不是一個整數(shù)(runeint),則它的默認(rèn)類型將被視為int。 此移位運算的結(jié)果也是一個類型不確定值并且它的默認(rèn)類型和左操作數(shù)的默認(rèn)類型一致。
  • 如果左操作數(shù)是一個類型不確定值并且右操作數(shù)是一個非常量,則左操作數(shù)將被首先轉(zhuǎn)化為運算結(jié)果的期待設(shè)想類型。 如果期待設(shè)想類型并沒有被指定,則左操作數(shù)的默認(rèn)類型將被視為它的期待設(shè)想類型。 如果此期待設(shè)想類型不是一個基本整數(shù)類型,則編譯報錯。 當(dāng)然最終運算結(jié)果是一個類型為此期待設(shè)想類型的類型確定值。

一些非移位算術(shù)運算的例子:

func main() {
	// 三個類型不確定常量。它們的默認(rèn)類型
	// 分別為:int、rune和complex64.
	const X, Y, Z = 2, 'A', 3i


	var a, b int = X, Y // 兩個類型確定值

	// 變量d的類型被推斷為Y的默認(rèn)類型:rune(亦即int32)。
	d := X + Y
	// 變量e的類型被推斷為a的類型:int。
	e := Y - a
	// 變量f的類型和a及b的類型一樣:int。
	f := a * b
	// 變量g的類型被推斷為Z的默認(rèn)類型: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)
}

一個移位算術(shù)運算的例子:

const N = 2
// A == 12,它是一個默認(rèn)類型為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的類型將被設(shè)想為int64,而非int
var y = int64(1 << m) // 同上
var z = int64(1) << m // 同上

// 下面這行編譯不通過。
/*
var _ = 1.23 << m // error: 浮點數(shù)不能被移位
*/

上面提到的移位運算結(jié)果的最后一點類型推斷規(guī)則有點反常。 這條規(guī)則的主要目的是為了防止一些移位運算在32位架構(gòu)和64位架構(gòu)的機器上的運算結(jié)果出現(xiàn)不一致但不一致卻沒有被及時發(fā)現(xiàn)的情況。 比如如果上面一段代碼中第10行(或第9行)的1的類型被推斷為它的默認(rèn)類型int, 則在32位架構(gòu)的機器上,x的取值在運行時刻將被截斷為0,而在64位架構(gòu)的機器上,x的取值在運行時刻將為232。 因為m是一個變量,在32位架構(gòu)的機器上,第9行和第10行并不會在編譯時刻報錯。 這將導(dǎo)致Go程序員在不經(jīng)意間寫出沒有料到的和難以覺察的bug。 因此,第9行和第10行中的1的類型被推斷為int64(最終的設(shè)想結(jié)果類型),而不是它們的默認(rèn)類型int。

下面這段代碼展示了對于左操作數(shù)為類型不確定值的移位運算,編譯結(jié)果因右操作數(shù)是否為常量而帶來的不同結(jié)果:

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

關(guān)于除法和余數(shù)運算

假設(shè)兩個操作數(shù)xy的類型為同一個整數(shù)類型, 則它們通過除法和余數(shù)運算得到的商q= x / y)和余數(shù)r= x % y)滿足x == q*y + r|r| < |y|)。如果余數(shù)r不為零,則它的符號和被除數(shù)x相同。商q的結(jié)果為x / y向零靠攏截斷。

如果除數(shù)y是一個常量,則它必須不為0,否則編譯不通過。 如果它是一個整數(shù)型非常量,則在運行時刻將拋出一個恐慌(panic)。 恐慌類似與某些其它語言中的異常(exception)。 我們將在以后的文章中了解到Go中的恐慌和恐慌恢復(fù)機制。 如果除數(shù)y非整數(shù)型的非常量,則運算結(jié)果為一個無窮大(Inf,當(dāng)被除數(shù)不為0時)或者NaN(not a number,當(dāng)被除數(shù)為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: 除數(shù)為0
println(0.0/0.0) // error: 除數(shù)為0

op=運算符

對于一個二元算數(shù)運算符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--)操作沒有返回值, 所以它們不能當(dāng)做表達式來使用。 另一個顯著區(qū)別是,在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
	*/
}

字符串銜接運算符

上面已經(jīng)提到了,加法運算符也可用做字符串銜接運算符。

字面形式 名稱 對兩個操作數(shù)的要求
+ 字符串銜接 兩個操作數(shù)必須為同一類型的字符串值。

+=運算符也適用于字符串銜接。

示例:

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

如果一個字符串銜接運算中的一個操作值為類型確定的,則結(jié)果字符串是一個類型和此操作數(shù)類型相同的類型確定值。 否則,結(jié)果字符串是一個類型不確定值(肯定是一個常量)。

布爾(又稱邏輯)運算符

Go支持兩種布爾二元運算符和一種布爾一元運算符。

字面形式 名稱 對操作值的要求
&& 布爾與(二元) 兩個操作值的類型必須為同一布爾類型。
|| 布爾或(二元)
! 布爾否(一元) 唯一的一個操作值的類型必須為一個布爾類型。

我們可以用下一小節(jié)介紹的不等于操作符!=來做為布爾異或操作符。

機理解釋:

// 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

如果一個布爾運算中的一個操作值為類型確定的,則結(jié)果為一個和此操作值類型相同的類型確定值。 否則,結(jié)果為一個類型不確定布爾值。

比較運算符

Go支持6種比較運算符:

字面形式 名稱 對兩個操作值的要求


==


等于

如果兩個操作數(shù)都為類型確定的,則它們的類型必須一樣,或者其中一個操作數(shù)可以隱式轉(zhuǎn)換為另一個操作數(shù)的類型。 兩者的類型必須都為可比較類型(將在以后的文章中介紹)。

如果只有一個操作數(shù)是類型確定的,則另一個類型不確定操作數(shù)必須可以隱式轉(zhuǎn)換到類型確定操作數(shù)的類型。

如果兩個操作數(shù)都是類型不確定的,則它們必須同時為兩個類型不確定布爾值、兩個類型不確定字符串值或者兩個類型不確定數(shù)字值。



!=


不等于
< 小于 兩個操作值的類型必須相同并且它們的類型必須為整數(shù)類型、浮點數(shù)類型或者字符串類型。
<= 小于或等于
> 大于
>= 大于或等于

比較運算的結(jié)果總是一個類型不確定布爾值。 如果一個比較運算中的兩個操作數(shù)都為常量,則結(jié)果布爾值也為一個常量。

以后,如果我們說兩個值可以比較,我們的意思是說這兩個值可以用==或者!=運算符來比較。 我們將在以后的文章中,我們將了解到某些類型的值是不能比較的。

注意,并非所有的實數(shù)在內(nèi)存中都可以被精確地表示,所以比較兩個浮點數(shù)或者復(fù)數(shù)的結(jié)果并不是很可靠。 在編程中,我們常常比較兩個浮點數(shù)的差值是否小于一個闕值來檢查兩個浮點數(shù)是否相等。

操作符運算的優(yōu)先級

Go中的操作符運算的優(yōu)先級和其它流行語言有一些差別。 下面列出了本文介紹的操作符的優(yōu)先級。 同一行中的操作符的優(yōu)先級是一樣的。優(yōu)先級逐行遞減。

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

一個和其它流行語言明顯的差別是,移位運算<<>>的優(yōu)先級比加減法+-的優(yōu)先級要高。

一個表達式(做為一個子表達式)可以出現(xiàn)在另一個表達式中。 這個子表達式的估值結(jié)果將成為另一個表達式的一個操作數(shù)。 在這樣的復(fù)雜表達式中,對于相同優(yōu)先級的運算,它們將從左到右進行估值。 和很多其它語言一樣,我們也可用一對小括號()來提升一個子運算的優(yōu)先級。

更多關(guān)于常量表達式

常量子表達式的順序有可能影響到最終的估值結(jié)果。

下面這個聲明的變量將被初始化為2.2,而不是2.7。 優(yōu)先級更高的子表達式3/2是一個常量表達式,所以它將在編譯階段被估值。 根據(jù)上面介紹的規(guī)則,在運算中,32都被視為int,所以3/2的估值結(jié)果為1。 在常量表達式1.2 + 1的運算中,兩個操作數(shù)的類型被視為float64,所以最終的估值結(jié)果為2.2。

var x = 1.2 + 3/2

再比如下例,在一個常量聲明中,3/2先被估值,其結(jié)果為1,所以最終的估值結(jié)果為0.1。 在第二個常量聲明中,0.1*3先被估值,其結(jié)果為0.3,所以最終的估值結(jié)果為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中還有一些其它操作符。它們將在后續(xù)其它適當(dāng)?shù)奈恼轮薪榻B。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號