前言
數字 (number) 是電腦程式中相當基礎的型別,許多電腦程式會將領域問題轉化為數字運算和處理。科學起源於我們對世界的觀察,科學家會將我們觀察到的現象量化,也就是轉為可運算的數字,之後就可以用電腦來處理。許多的型別 (data type),像是字串,內部也是以數字來儲存。本文討論如何以 Go 來處理數字。
和數字相關的資料型別
Go 包含數種和數字相關的型別
- 整數 (integer)
- 帶號整數 (signed):
int8
、int16
、int32
、int64
、int
- 無號整數 (unsigned):
uint8
、uint16
、uint32
、uint64
、uint
- 帶號整數 (signed):
- 浮點數 (floating-point number)
- 單倍精確度 (single precision):
float32
- 雙倍精確度 (double precision):
float64
- 單倍精確度 (single precision):
- 複數 (complex number)
- 單倍精確度 (single precision):
complex64
- 雙倍精確度 (double precision):
complex128
- 單倍精確度 (single precision):
註:int
和 uint
實際的位數會依底層的系統而變動。
我們使用不同的型別是著眼於效率的考量,使用最小的位數來儲存數字可以節約系統記憶體和儲存空間;此外,整數和浮點數內倍儲存數字的方式不同,要依情境選擇合適的型別。在日常使用上,我們不需過度關注這些技術細節,需要整數時使用 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")
}
}