其實構思這系列的時候思考了很久,因為 Golang 有很多常用觀念要結合起來才能發揮出它的功能和特色,這導致 Golang 的教學文不太容易能像 C 語言系列文那樣明確地個別獨立成一類主題來講。我曾經提到建議在具備一些程式語言基礎之後才來學 Golang,也正是如此。
這篇文可能會預設你已經具有這方面知識而省略一些東西,除此之外也會有些我認為可以晚點再說的觀念被我排到之後的章節。如果有什麼地方讓你覺得卡住,就再請你到文章底下留言吧!
? 基本的資料型態
在使用變數以前,我們要先瞭解 Golang 有哪些常見的資料型態(型別)。
Golang 的基本型別非常簡單,我們可以粗分成以下幾類:
。int(整數)
。float(浮點數)
。bool(布林值)
。string(字串)
。complex(複數)
。float(浮點數)
。bool(布林值)
。string(字串)
。complex(複數)
光是使用上面幾種,就已經足以應付大部分日常情況。對於一個已經有程式語言基礎的人而言,相信至少 int、float、bool、string 這四樣都不是什麼很陌生的東西吧?
? 變數的宣告方式
宣告變數主要有兩種方式,一種是使用 var 關鍵字,一種是使用「:=」符號來宣告並賦予初始值。
以下幾種方法都是 OK 的,使用 := 符號的話就不需要再用 var 關鍵字了,因為這個冒號本身就有宣告新變數的意義:
package main
import "fmt"
func main() {
var a = 8
var b int
var c int = 561
d := 32
e := int(77)
fmt.Println(a, b, c, d, e) // -> 8 0 561 32 77
}
但如果你想在函式外面宣告能夠給不同函式共用的全域變數(global variable),就只能用 var 關鍵字來宣告:
package main
import "fmt"
var money = 300
func main() {
fmt.Printf("I have $%d.", money) // -> I have $300.
}
和 C 語言不同的是,Golang 在宣告變數的時候不需要擔心殘值問題,每一種型態都有預設的 zero-value——假如你在宣告 int 的時候沒有給予初始值,就會預設是 0。
如果你想瞭解 C 語言的殘值現象,可以參考跟著豬腳 C 起來:用電腦來操控資料吧。
除此之外,你也能利用逗號在一行之內同時宣告多個變數:
ar m, n int = 32, 99
p, q := 64, 188
或者是用來單純賦值:
var x, y int
x, y = 9, 17
甚至是使用括號來宣告很多個變數(或賦值):
var (
class int
money = 114514
age = 24
job = "student"
)
一般來說如果在沒有非常講究變數型態的地方(例如 for 迴圈的迭代變數 i),我們直接使用 := 符號來宣告變數就可以了,畢竟我們本來就不怎麼在乎它的型態,這麼做也相對使得 code 更簡潔:
for i := 0; i < 10; i++ {
fmt.Print(i)
} // -> 0123456789
? 定義常數
就像其他語言的常數一樣,常數用來定義在整個程式流程裡不需要被修改的內容。在 Golang 定義常數(contant)的方法也很簡單,只要用 const 關鍵字就行了:
const pi = 3.14159
const e float64 = 2.71828
const sqrt2, sqrt3 = 1.41421, 1.73205
它也一樣可以使用括號把相關的擺在一起,更加美觀:
const (
log2 = 0.30103
log3 = 0.47712
log5 = 0.69897
)
如果這些常數是遞增的整數(通常用在編號),你可以用 iota 關鍵字,Golang 會自動把它從 0 開始往下數,然後定義好:
const (
Cat = iota
Dog
Hamster
Bird
Pig
)
func main() {
fmt.Println(Cat, Dog, Hamster, Bird, Pig) // -> 0 1 2 3 4
}
如果你希望這些編號從 n 開始數,就用 iota+n:
const (
Cat = iota + 5
Dog
Hamster
Bird
Pig
)
func main() {
fmt.Println(Cat, Dog, Hamster, Bird, Pig) // -> 5 6 7 8 9
}
iota 並不是沒有用處,這就有點像是 C 語言的 enum 功能。我們有些場合並不在乎這些常數的實際數值內容,而是直接把這些常數當成是一種符號來使用,目的是在讓程式碼變得更能一眼就看懂。像是我們瀏覽網站的時候偶爾遇上問題,會看到例如 403(禁止訪問)、404(找不到檔案)這種 HTTP 協定裡面的標準錯誤編號。
假如在程式裡面老是直接用 404 來表達「找不到檔案」的錯誤代碼,程式碼一旦多了就會變得很難閱讀(也不易統一修改,要是不慎寫錯的話也難以察覺),不如直接令 const ErrorNotFound = 404,然後用 ErrorNotFound 來表示這個錯誤編號,寫出來的程式豈不漂亮多了嗎?
? 各種型態的家族
在 Golang 裡面,整數型態 int 可以細分成 int、int8、int16、int32、int64、uint、uint8、uint16、uint32、uint64。這些不同的 int 類型看起來好像很複雜,但其實只是差在佔用的位元數量(8, 16, 32, 64)、是否無號(unsigned)而已。當你需要儲存更大範圍的數,就使用更多位元數的 int;當你能百分之百肯定你不會存入負數,就使用 unsigned 的型態。
關於 unsigned integer,可以參考跟著豬腳 C 起來:各式各樣的資料型態。
通常情況下,我們會直接使用 int 來儲存整數——int 型態佔用的位元組數量(和可以表示的範圍)會依照平臺而定,如果是在 32-bit 的系統的話,這個 int 的範圍就會和 int32 一樣,在 64-bit 的系統裡面則會和 int64 一樣。只是,Golang 不允許我們把這些東西通通混在一起,所以 int、int32、int64 都是不能直接互通的。我們如果想把 int32 型態的值取出來存到 int 的變數裡,需要先透過顯式轉換來明確地表示它被轉換成什麼樣的型別,才能繼續操作:
var a int = 555
var b int32 = 777
// you should do a = int(b) instead of a = b
a = int(b)
浮點數則分為 float32 和 float64。注意,浮點數不像 int 一樣,你要嘛就用 float32、要嘛就用 float64,只能從這兩種選一個。一般我們比較多會使用 float64,如果你在宣告的時候使用諸如 var a = 0.0 或是 a := 0.0 之類的句式,編譯器會預設為 float64。
至於上面提到的複數,分為 complex64 和 complex128 兩種(複數預設會是 complex128)。其中 complex64 是由兩個 float32 組成,而 complex128 則是由兩個 float64 組成,分成兩個是因為一個要用來存放實部、一個用來存放虛部:
var j = 7 - 4i // complex128 as default
var k complex128 = 3 + 5i
var h = complex(9, -5) // same as (9-5i)
由於使用 complex64/complex128 型態的機會實在太少,這方面我就不再多著墨,大家可以稍微看看就好。只是作為一個通用的語言來說,Golang 竟然提供複數的內建型態,我覺得很神奇就是了。
? 型態別名與型態定義
當你需要用某些套件處理資料流(例如 JSON parser、檔案讀寫)的時候,你可能會見到有人使用 byte 型態。其實,在 Golang 裡面的 byte 型態就跟 uint8 並沒有任何不同,它們甚至是完全相容的:
var a byte = 66
var b uint8
b = a
這就是所謂的別名(alias),你高興的話也可以自己替任一個型態來定義別名:
package main
import "fmt"
type bit = bool
func main() {
var a bool = true
var b bit
b = a
fmt.Println(b)
}
雖然看似沒有什麼意義,但這麼做的話就會有更方便閱讀、理解程式碼內容的優點。像是比起 uint8,如果我們使用 byte 型態來處理檔案資料流,就會更能體現「以 byte 為單位」的意義,畢竟只要一眼就可以看得出這是代表一個位元組了。除此之外,Golang 也有一個資料型態 rune 是 int32 的別名,可以用來處理 unicode 字元。
不單單只有型態別名,有個和型態別名很相似的做法:
type bit bool
少了一個等號,這樣就可以讓自訂的 bit 無法直接和原先的 bool 互通了:
var a bool = true
var b bit
b = a // -> cannot use a (variable of type bool) as bit value in assignment
這是為了在某些情況下我們可能會需要定義一些無法直接互通的型態(例如 ErrorCode、Color 之類的東西,搭配 iota 來定義),同樣也是為了讓程式碼變得更好閱讀或使用,就像這樣:
type color int
const (
ColorBlack color = iota
ColorWhite
ColorRed
ColorBlue
ColorYellow
ColorGreen
ColorOrange
ColorPink
)
func GetColorRGB(c color) string {
switch c {
case ColorBlack:
return "#000000"
case ColorWhite:
return "#FFFFFF"
case ColorRed:
return "#FF0000"
case ColorBlue:
return "#0000FF"
case ColorYellow:
return "#FFFF00"
case ColorGreen:
return "#00FF00"
case ColorOrange:
return "#FF7F00"
case ColorPink:
return "#FFA5A5"
default:
return ""
}
}
當然也不是指完全就不能互通啦。以上面的兩個例子來說,如果你用 bit(a) 把 bool 轉型成 bit,或是用 color() 把一個 int 的變數轉成 color 型態,那也是沒問題的,端看身為設計者的你如何面對你自己(或是你手上的案件)有什麼樣的需求。
關於資料型態就說到這裡吧,下次再來講講函式的各種功能,開始進入 Golang 的核心。
HackMD 好讀版:https://hackmd.io/@upk1997/go-datatype
縮圖素材原作者:Renée French(CC BY-SA 3.0)