位元詩人 [Golang] 程式設計教學:使用運算子 (Operator)

Golang運算子
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

在程式設計中,運算子多使用符號 (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 語言的運算子使用方式。由於運算子是程式語言中的基本特性,讀者應試著練習一下運算子的用法。

關於作者

身為資訊領域碩士,位元詩人 (ByteBard) 認為開發應用程式的目的是為社會帶來價值。如果在這個過程中該軟體能成為永續經營的項目,那就是開發者和使用者雙贏的局面。

位元詩人喜歡用開源技術來解決各式各樣的問題,但必要時對專有技術也不排斥。閒暇之餘,位元詩人將所學寫成文章,放在這個網站上和大家分享。