位元詩人 [Golang] 程式設計教學:處理數字 (Number)

Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

前言

數字 (number) 是電腦程式中相當基礎的型別,許多電腦程式會將領域問題轉化為數字運算和處理。科學起源於我們對世界的觀察,科學家會將我們觀察到的現象量化,也就是轉為可運算的數字,之後就可以用電腦來處理。許多的型別 (data type),像是字串,內部也是以數字來儲存。本文討論如何以 Go 來處理數字。

和數字相關的資料型別

Go 包含數種和數字相關的型別

  • 整數 (integer)
    • 帶號整數 (signed): int8int16int32int64int
    • 無號整數 (unsigned): uint8uint16uint32uint64uint
  • 浮點數 (floating-point number)
    • 單倍精確度 (single precision):float32
    • 雙倍精確度 (double precision):float64
  • 複數 (complex number)
    • 單倍精確度 (single precision):complex64
    • 雙倍精確度 (double precision):complex128

註:intuint 實際的位數會依底層的系統而變動。

我們使用不同的型別是著眼於效率的考量,使用最小的位數來儲存數字可以節約系統記憶體和儲存空間;此外,整數和浮點數內倍儲存數字的方式不同,要依情境選擇合適的型別。在日常使用上,我們不需過度關注這些技術細節,需要整數時使用 int,浮點數使用 float64,複數使用 complex128 即可。

不同的進位系統 (Positional Notation)

一般日常使用的數字所採用的進位系統是十進位 (decimal),但實際上還存在其他的進位系統。在 Go 語言中,我們可以直接使用八進位 (octal) 和十六進位 (hexadecimal) 的數字:

package main

import (
    "log"
)

func main() {
    if !(010 == 8) {
        log.Fatal("Wrong number")
    }

    if !(0xFF == 255) {
        log.Fatal("Wrong number")
    }
}

但我們無法直接在程式中寫出二進位 (binary) 數的實字 (literal),僅能用字串來顯示當下的數字:

package main

import (
    "fmt"
    "log"
)

func main() {
    if !(fmt.Sprintf("%b", 8) == "1000") {
        log.Fatal("Wrong number string")
    }
}

注意浮點數所帶來的誤差

由於浮點數在電腦內的儲存方式,在每次浮點數運算時都會累積微小的誤差;如果各位讀者對其學理有興趣,可以看一些計算機概論的書籍,這裡就不談細節。以下程式碼看似正常:

package main

import (
    "log"
)

func main() {
    if !(0.1+0.2-0.3 == 0.0) {
        log.Fatal("Uneqal")
    }
}

但以下的程式會因累積過多的誤差而變成無限迴圈:

package main

import (
    "fmt"
)

func main() {
    i := 1.0
    for {
        if i == 0.0 {
            break
        }

        fmt.Println(i)
        i -= 0.1
    }
}

為了要消除這個不可避免的誤差,我們在進行浮點數運算時會比較其誤差的絕對值 (absolute value)。可參考以下公式:

|實際值 - 理論值| < 誤差

由上式可知,我們的目標不是去除誤差,而是將誤差降低到可接受的程度。在下列實例中,當誤差值夠小時就滿足運算條件、跳出迴圈:

package main

import (
    "fmt"
    "math"
)

func main() {
    i := 1.0
    for {
        if math.Abs(i-0.0) < 1.0/1000000 {
            break
        }

        fmt.Println(i)
        i -= 0.1
    }
}

將浮點數用在迴圈時,要注意其值應逐漸逼近誤差值,否則會因無法滿足迴圈終止條件變無限迴圈。

小心溢位 (Overflow) 或下溢 (Underflow)

溢位 (overflow) 或下溢 (underflow) 也是因電腦內部儲存數字的方式所引起的議題。當某個數字超過其最大 (或最小) 值,電腦程式會自動忽略超出極值的部分,造成溢位或下溢。

注意 Go 不會自動提醒程式設計者這個議題,如下例:

package main

import (
    "fmt"
    "math"
)

func main() {
    // num is the maximal int32 value.
    num := math.MaxInt32

    // Get -2147483648.
    fmt.Println(num + 1)
}

很少電腦程式會故意引發溢位或下溢,因此,程式設計者要預先避免這個問題,像是使用較大的資料型別或用大數函式庫進行運算。

產生質數 (Prime Number)

如果了解質數 (prime number) 的數學定義,要找出一個小的質數不會耗費過多運算時間。參考以下實例:

package main

import (
    "log"
    "math"
)

type IPrime interface {
    Next() int
}

type Prime struct {
    num int
}

func NewPrime() IPrime {
    p := new(Prime)
    p.num = 1
    return p
}

func (p *Prime) Next() int {
    for {
        p.num++
        isPrime := true

        for i := 2; float64(i) <= math.Sqrt(float64(p.num)); i++ {
            if p.num%i == 0 {
                isPrime = false
                break
            }
        }

        if isPrime {
            break
        }
    }

    return p.num
}

func main() {
    p := NewPrime()

    if !(p.Next() == 2) {
        log.Fatal("Wrong number")
    }

    if !(p.Next() == 3) {
        log.Fatal("Wrong number")
    }

    if !(p.Next() == 5) {
        log.Fatal("Wrong number")
    }

    if !(p.Next() == 7) {
        log.Fatal("Wrong number")
    }
}

在這個實例中,我們把物件寫成迭代器,每次呼叫時會傳回下一個質數。

常見的數學公式 (Formula)

Go 標準函式庫中的 math 套件提供一些常見的數學公式,像是平方根 (square root)、指數 (exponential function)、對數 (logarithm)、三角函數 (trigonometric function) 等,程式設計者不需再重造輪子。有需要的讀者請自行查閱該套件的 API 說明文件

進行大數 (Big Number) 運算

我們先前提過,電腦儲存數字的位數是有限制的,如果我們需要更大位數的數字,就要用軟體去模擬,math/big 套件提供大數運算的功能。然而,由於 Go 不支援運算子重載 (operator overloading),在 Go 語言中進行大數運算略顯笨拙。我們展示一個 2 的 100 次方的例子:

package main

import (
    "fmt"
    "math/big"
)

func main() {
    base := big.NewInt(1)
    two := big.NewInt(2)

    for i := 0; i < 100; i++ {
        base.Mul(base, two)
    }

    fmt.Println(base)
}

math/big 提供三種數字物件,分別是 Int (整數)、Float (浮點數)、Rat (有理數)。但是,math/big 套件未提供常見的數學公式,可以使用 ALTree/bigfloat 內提供的一些公式來補足內建套件的不足。

產生亂數 (Random Number)

亂數 (random number) 是隨機産生的數字。許多電腦程式會使用亂數,像是電腦遊戲就大量使用亂數使得每次的遊戲狀態略有不同,以增加不確定性和樂趣。

在電腦內部,其實沒有什麼黑魔法在操作隨機事件,亂數就是用某些亂數演算法所産生的數字。大概過程如下:

  • 給予程式一個初始值,做為種子 (seed)
  • 藉由某種隨機數演算法從種子産生某個數字
  • 將該數字做為新的種子,套入同樣的演算法,繼續産生下一個數字

亂數演算法産生的數字看起來的確沒有什麼規律,對於一般用途是足夠了。至於亂數演算法本身如何實作,已經超出本文的範圍,請讀者自行查閱相關書籍。我們這裡展示如何用 math/rand 套件産生亂數:

package main

import (
    "fmt"
    "math/rand"
    "time"
)

func main() {
    // Set a random object
    // It is a common practice to use system time as the seed.
    r := rand.New(rand.NewSource(time.Now().UnixNano()))

    // The initial state of our runner.
    miles := 0

GAME_OVER:
    for {
        // Get a new game state for each round.
        state := r.Intn(10)

        // Update the state of our runner.
        switch state {
        case 7:
            fmt.Println("Jump!")
            miles += 3
        case 0:
            fmt.Println("You FAIL")
            break GAME_OVER
        case 6:
            fmt.Println("Missed!")
            miles -= 2
        default:
            fmt.Println("Walk")
            miles += 1
        }

        // End the game if you win.
        if miles >= 10 {
            fmt.Println("You WIN")
            break GAME_OVER
        }
    }
}

在字串和數字間轉換

有時候我們需要在字串和數字間進行轉換,像是從文字檔案中讀取字串後,將其轉為相對應的數字。Go 提供 strconv 套件來完成這些任務:

package main

import (
    "log"
    "strconv"
)

func main() {
    num, err := strconv.Atoi("100")
    if err != nil {
        log.Fatal(err)
    }

    if !(num == 100) {
        log.Fatal("It should be 100.")
    }
}

我們不能一廂情願地認為轉換的過程總是成功,因此,我們需要在程式中處理可能的錯誤。

我們也可以將數字轉為字串:

package main

import (
    "log"
    "strconv"
)

func main() {
    str := strconv.Itoa(100)

    if !(str == "100") {
        log.Fatal("Wrong string")
    }
}

數字轉字串的過程不會失敗,所以我們不需處理錯誤。

先前的例子是以十進位來轉換資料,也可以用其他位數來轉換。這裡我們展示一個將字串轉為二進位數的過程:

package main

import (
    "log"
    "strconv"
)

func main() {
    num, err := strconv.ParseInt("0111", 2, 0)
    if err != nil {
        log.Fatal(err)
    }

    if !(num == 7) {
        log.Fatal("Wrong number")
    }
}
關於作者

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

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