開源技術教學文件網 處理數字 (Number)

最後修改日期為 MAY 26, 2019

前言

數字 (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")
	}
}
分享本文
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Yahoo
追蹤本站
Facebook Facebook Twitter