前言
在程式設計中,運算子多使用符號 (symbol) 而非文字 (word) 來表示。運算子通常不能化約成更小的單位,在程式設計中將運算子視為該語言的基本指令。本文介紹 Go 語言中可用的運算子。
自製簡易的斷言 (Assertion)
許多程式設計教材會用格式化輸出 (formatted output) 來檢查程式,但筆者較推薦用斷言 (assertion) 來取代格式化輸出。
當我們以斷言檢查程式碼時,若條件發生錯誤,斷言會發出警告訊息,有的版本的斷言還會直接終止程式。因為斷言是由電腦程式自主檢查,不需要人為判讀,日後易於自動化。此外,在撰寫斷言時,我們會把想檢查的項目直接寫入程式碼,藉由閱讀程式碼即可清楚表達出程式設計者的意圖。
Go 語言沒有內建的斷言,有一些社群套件可用,像是 stretchr/testify 中的 assert。不過,只要了解斷言的原理,自己製作一個簡易版的斷言不會太困難。以下是一個實例,請讀者試著閱讀看看,我們會說明。
package main
import (
"fmt"
"os"
"runtime"
)
func main() {
assert(1+1 == 3, "1 + 1 should be 2")
}
// Home-made `assert`
func assert(cond bool, msg string) {
_, f, l, _ := runtime.Caller(1)
if !cond {
fmt.Fprintf(os.Stderr, "Failed on (%s:%d): %s", f, l, msg)
os.Exit(1)
}
}
在主函式中,我們用自製的 assert
函式檢查 1 + 1 == 3
這個條件是否為真。由於這個條件為偽,實際執行程式時,會中止程式執行並吐出錯誤訊息。
在自製的 assert
函式中,會接收 cond
(布林) 和 msg
(字串) 兩個參數。這個函式會執行簡單的斷言。
我們先藉由 runtime
套件的 Caller 函式取得呼叫者的資訊,從中取出檔案名稱 f
和呼叫者所在的行數 l
,並拋掉剩下的回傳值。在此處的呼叫者是主函式,被呼叫者是 assert
函式。由於我們預期呼叫的層次只有一層,故 Caller
函式的參數為 1
。
實際執行斷言時,以 if
敘述檢查 cond
是否為真。當 cond
不為真時,在標準錯誤 (standard error) 印出錯誤訊息,接著呼叫 os
套件的 Exit 函式來提早離開主程式。
如果讀者覺得這些細節過於複雜,也不用擔心。把 assert
函式當成立即可用的功能即可。我們在使用他人寫的函式時,並不會對每個函式都去追蹤其原始碼。只有在想了解函式的內部運作時,才會去觀看該函式的原始碼。
代數運算子 (Arithmetic Operators)
代數運算子用來進行基本的四則運算。以下是代數運算子:
+
:相加-
:相減*
:相乘/
:相除%
:取餘數
以下是簡短實例:
package main
import (
"fmt"
"math"
"os"
"runtime"
)
func main() {
assert(4+3 == 7, "4 + 3 should be 7")
assert(4-3 == 1, "4 - 3 should be 1")
assert(4*3 == 12, "4 * 3 should be 12")
assert(4/3 == 1, "4 / 3 should be 1")
assert(math.Abs(4.0/3.0-1.333333) < 0.00001, "4.0 / 3.0 should be 1.333333")
assert(4%3 == 1, "4 % 3 should be 1")
}
func assert(cond bool, msg string) {
_, f, l, _ := runtime.Caller(1)
if !cond {
fmt.Fprintf(os.Stderr, "Failed on (%s:%d): %s", f, l, msg)
os.Exit(1)
}
}
由於四則運算的原理相當簡單,讀者可試著自行閱讀程式碼。要注意在進行除法運算時,整數 (integer) 和浮點數 (floating point number) 會有不同的行為。
由於浮點數內部儲存數字的方式和整數相異,浮點數運算可能會產生誤差,故我們在比較浮點數的運算結果時,不會直接用相等 ==
來比較,而會確認運算結果的誤差在許可範圍內。我們使用 math
套件的 Abs 函式取得誤差的絕對值 (absolute value),以消除正負號所帶來的誤判。
二元運算子 (Bitwise Operators)
二元運算子也是代數運算子。但二元運算的概念和一般的代數運算有一些差異,故我們將其分開。以下是二元運算子:
&
:bitwise AND|
:bitwise OR^
:bitwise XOR&^
:bit clear<<
:左移 (left shift)>>
:右移 (right shift)
由於二元運算在日常生活中不會接觸到,我們把運算過程寫在註解中,供讀者參考。
package main
import (
"fmt"
"os"
"runtime"
)
func main() {
/* 3 is 0011
5 is 0101 */
/* 0011
&) 0101
---------
0001 */
assert((3 & 5) == 1, "3 & 5 should be 1")
/* 0011
|) 0101
---------
0111 */
assert((3 | 5) == 7, "3 | 5 should be 7")
/* 0011
^) 0101
---------
0110 */
assert((3 ^ 5) == 6, "3 ^ 5 should be 6")
/* <<) 0000 0101
---------------
0000 1010 */
assert((5 << 1) == 10, "5 << 1 should be 10")
/* >>) 0000 0101
---------------
0000 0010 */
assert((5 >> 1) == 2, "5 >> 1 should be 2")
}
func assert(cond bool, msg string) {
_, f, l, _ := runtime.Caller(1)
if !cond {
fmt.Fprintf(os.Stderr, "Failed on (%s:%d): %s", f, l, msg)
os.Exit(1)
}
}
二元運算會比一般的代數運算來得快,主要用於低階運算 (low-level computing) 中,日常生活不會用到,故我們不詳談。有興趣的讀者可自己查閱計算機概論等資料。
比較運算子 (Comparison Operators)
比較運算子用來比較兩項資料的大小,比較後會回傳布林值。以下是比較運算子:
==
:相等!=
:不相等<
:小於<=
:小於等於>
:大於>=
:大於等於
以下是簡短的實例:
package main
import (
"fmt"
"os"
"runtime"
)
func main() {
assert(4 == 4, "4 should be equal to 4")
assert(4 != 3, "4 should not be equal to 3")
assert(4 > 3, "4 should be greater than 3")
assert(4 >= 3, "4 should be greater than or equal to 3")
assert(4 < 5, "4 should be less than 5")
assert(4 <= 5, "4 should be less than or equal to 5")
}
func assert(cond bool, msg string) {
_, f, l, _ := runtime.Caller(1)
if !cond {
fmt.Fprintf(os.Stderr, "Failed on (%s:%d): %s", f, l, msg)
os.Exit(1)
}
}
邏輯運算子 (Logical Operators)
邏輯運算子用於布林運算,包括以下三種運算子:
&&
:且 (and)||
:或 (or)!
:非 (not)
以下是簡短的實例:
package main
import (
"fmt"
"os"
"runtime"
)
func main() {
assert((true && true) == true, "Wrong logic")
assert((true && false) == false, "Wrong logic")
assert((false && true) == false, "Wrong logic")
assert((false && false) == false, "Wrong logic")
assert((true || true) == true, "Wrong logic")
assert((true || false) == true, "Wrong logic")
assert((false || true) == true, "Wrong logic")
assert((false || false) == false, "Wrong logic")
assert((!true) == false, "Wrong logic")
assert((!false) == true, "Wrong logic")
}
func assert(cond bool, msg string) {
_, f, l, _ := runtime.Caller(1)
if !cond {
fmt.Fprintf(os.Stderr, "Failed on (%s:%d): %s", f, l, msg)
os.Exit(1)
}
}
由於運算子優先順序的議題,我們在進行邏輯運算時加上括號。
位址運算子 (Address Operators)
位址運算子有以下兩種:
*
&
在不同情境,位址運算子有不同的意義。基礎的財經運算用不到位址運算子,日後有機會時會在介紹指標時用到位址運算子。
接收運算子 (Receive Operator)
接收運算子有以下符號:
<-
接收運算子用在通道。基礎的財經運算用不到共時性程式,故不會用到接收運算子。
型別轉換
Go 語言為了避免不經意的錯誤,不能直接把不同型別的資料相結合。例如,在 Go 程式中不能把整數和浮點數直接相加。轉換型別的方式是用 T(x)
;像是 float(3)
會把整數 3
轉為浮點數 3.0
。
運算子優先順序
為了處理在單一敘述中出現多個運算子的情境,程式語言有內建的運算子優先順序。像是 Golang 官方提供了一份運算子優先順序的清單。
但程式設計者甚少背誦運算子優先順序。因為:
- 運算子的優先順序和數學的概念相同
- 可藉由簡化敘述來簡化運算子的使用
- 可使用括號來改變運算子優先順序
結語
藉由本文,讀者可了解 Go 語言的運算子使用方式。由於運算子是程式語言中的基本特性,讀者應試著練習一下運算子的用法。