[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")
    	}
    }
    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk