上一篇我們裝好環境,也跑出了第一支 Hello World。接下來要寫真正的程式,第一步是變數與型別。在 Python 中,變數想放什麼都行,型別要等程式跑起來才知道;而 Go 與 Python 不同,它是靜態型別的語言,也就是每個變數的型別必須在編譯時就定下來,若型別對不上,編譯器會直接拒絕並報錯。
這篇會先從 Go 的型別以及宣告變數的兩種寫法,接著討論型別轉換以及型別相關的議題。
Go 的變數
Go 的每個變數都有固定型別,宣告時可以明寫,也可以讓編譯器從值推出來。在 Go 中,我們有兩種宣告變數的方式,分別是用 var,以及使用 := 的簡短宣告。
使用 var 宣告
var 是最完整的寫法,格式為
1 | var 名稱 型別 = 值 |
例如:
1 | var name string = "gopher" |
不過型別也可以省略,Go 會直接從右邊的值推出來。例如下面的 name 會是 string,age 會是 int:
1 | var name = "gopher" |
使用 := 宣告
在函式裡面,更常見的寫法是 :=,它一次完成宣告與賦值,型別同樣由值推出來:
1 | func main() { |
:= 和 var 最大的差別是位置:var 在函式內外都能用,:= 只能寫在函式內。如果寫在函式外,編譯時會出現這個錯誤:
1 | syntax error: non-declaration statement outside function body |
函式外只能放宣告,也就是以 var、const、func、type 開頭的敘述,而 := 不是宣告關鍵字。兩種寫法的使用位置整理如下:
| 位置 | var |
:= |
|---|---|---|
| 函式外 | 可以 | 不行 |
| 函式內 | 可以 | 可以(較常用) |
賦值與重複宣告
若預期宣告之後,變數會想要更換值,則要使用 =。:= 的意思是宣告一個新變數,所以對已存在的變數再用一次 := 會出現編譯錯誤:
1 | age := 3 |
另外,型別一旦決定就不能換。例如下面 age 已經定義型別為 int,因此將字串賦給它也會出現編譯錯誤:
1 | age := 3 |
而一次宣告多個變數時,:= 的左邊只要至少有一個新名字就可以通過編譯,已存在的名字只會被賦值,新名字才是真的宣告。
1 | age, name := 3, "gopher" |
Go 的型別
型別決定一個變數能存哪些值,以及這些值能做哪些運算 (The Go Project, 2026, Types)。Go 的型別分成兩類:Go 預先宣告好的型別,以及用現有型別組合出來的複合型別。下表整理了 Go 的所有型別,點開整數和複合型別則可以看每個型別各自的說明:
| 分類 | 型別 | 說明 |
|---|---|---|
| 布林 | bool | 真值 true 與 false |
| 展開查看更多 | 共 13 種 | |
int8 | 8 位元的有號整數 | |
int16 | 16 位元的有號整數 | |
int32 | 32 位元的有號整數 | |
int64 | 64 位元的有號整數 | |
uint8 | 8 位元的無號整數 | |
uint16 | 16 位元的無號整數 | |
uint32 | 32 位元的無號整數 | |
uint64 | 64 位元的無號整數 | |
int | 有號整數,大小由實作決定,為 32 或 64 位元 | |
uint | 無號整數,與 int 同大小 | |
uintptr | 大到足以存放指標位元的無號整數 | |
byte | uint8 的別名 | |
rune | int32 的別名 | |
| 浮點數 | float32 | IEEE 754 的 32 位元浮點數 |
float64 | IEEE 754 的 64 位元浮點數 | |
| 複數 | complex64 | 實部與虛部皆為 float32 的複數 |
complex128 | 實部與虛部皆為 float64 的複數 | |
| 字串 | string | 位元組的序列,內容建立後不能修改 |
| 展開查看更多 | 共 8 種 | |
[n]T | 陣列:由同一種型別的元素組成、長度固定的序列 | |
[]T | slice:底層陣列中一段連續區間的描述 | |
struct | 由具名欄位組成的序列,每個欄位有名稱與型別 | |
*T | 指標:指向某個型別的變數 | |
func | 函式:參數與回傳型別相同的所有函式 | |
interface | 定義一組型別,凡屬於這組型別的值都能存入 | |
map[K]V | 無序的元素群,以不重複的鍵索引 | |
chan T | channel:讓並行執行的函式互相傳送與接收值 |
本篇接下來先介紹整數、浮點數、字串與布林值,其他型別留待後續提到相關內容再詳細說明與討論。
整數
Go 的整數型別依兩件事分類:占幾個位元,以及能不能表示負數:
| 大小 | 有號整數(可為負) | 無號整數(不可為負) |
|---|---|---|
| 指定位元數 | int8、int16、int32、int64 |
uint8、uint16、uint32、uint64 |
| 由機器決定 | int |
uint |
- 名稱後面的數字就是位元數,例如
int8就占 8 位元 int和uint沒有指定位元數,大小由機器決定。在 64 位元的機器上是 64 位元
不同位元數能表示的數字不一樣。在 Python 裡整數沒有上限,不需要考慮這件事,但在 Go 中則需要特別留意。
整數的範圍
$n$ 位元的無號整數範圍是 $0$ 到 $2^n - 1$,有號整數範圍是 $-2^{n-1}$ 到 $2^{n-1} - 1$。
展開查看數學推導
位元數與範圍的關係可以推導出來:假設設定 $n$ 個位元,每個位元有 0 和 1 兩種可能,因此一共有 $2^n$ 種不同的組合,也就是最多能表示 $2^n$ 個不同的整數。
無號整數把這些組合全部拿來表示非負整數,從 0 開始連續往上數,所以範圍是
有號整數則要同時表示負數與非負數,因此把 $2^n$ 個組合對半分,各得 $2^{n-1}$ 個。負數這一半是 $-1, -2, \cdots, -2^{n-1}$,非負數這一半是 $0, 1, \cdots, 2^{n-1} - 1$。其中 0 占去了非負這一半的一個位置,所以正數會比負數少一個,範圍是
以 int8 為例,$n = 8$,代入式 $\eqref{eq:int-range}$ 可得
而 uint8 代入式 $\eqref{eq:uint-range}$ 則是 $0 \leq x \leq 2^{8} - 1 = 255$。
依照相同方式,可以得到各型別的範圍 (The Go Project, 2026, Numeric types):
| 有號整數 | 位元數 | 最小值 | 最大值 |
|---|---|---|---|
int8 |
8 | -128 | 127 |
int16 |
16 | -32,768 | 32,767 |
int32 |
32 | -2,147,483,648 | 2,147,483,647 |
int64 |
64 | -9,223,372,036,854,775,808 | 9,223,372,036,854,775,807 |
| 無號整數 | 位元數 | 最小值 | 最大值 |
|---|---|---|---|
uint8 |
8 | 0 | 255 |
uint16 |
16 | 0 | 65,535 |
uint32 |
32 | 0 | 4,294,967,295 |
uint64 |
64 | 0 | 18,446,744,073,709,551,615 |
整數溢位
如果運算的結果超出範圍,會發生什麼事?先看一個例子:
1 | var x int8 = 127 |
這段程式可以編譯,執行時也不會報錯,127 + 1 卻變成了 -128。這是因為超出範圍之後,Go 不會報錯或中止程式 (The Go Project, 2026, Integer overflow),而是繞回範圍的另一端1,就像里程表從 999999 跳回 000000。
繞回的規則是:$n$ 位元整數只能保留 $2^n$ 種結果,所以任意整數 $x$ 實際得到的值,是與 $x$ 除以 $2^n$ 餘數相同、且落在範圍內的那一個數。
展開查看數學推導
把 $x$ 先平移 $2^{n-1}$,使範圍變成從 0 開始,取餘數後再平移回去,即可得到
int8 的 $n = 8$,所以 $2^{n-1} = 128$,$2^n = 256$。
$x = 127 + 1 = 128$:
$x = 100 + 100 = 200$:
$x = -128 - 1 = -129$:
實際撰寫程式進行驗證:
1 | var x1 int8 = 127 |
無號整數的範圍本來就從 0 開始,不需要平移,繞回的規則簡化為
例如 uint8 的 255 + 1,代入式 $\eqref{eq:wrap-unsigned}$ 得 $256 \bmod 256 = 0$,實際執行也是 0。
要注意的是,編譯器只能檢查它在編譯時就知道值的情況。把常數寫死在程式裡,超出範圍會直接編譯錯誤:
1 | var x int8 = 128 |
但如果是變數運算的結果,編譯器在編譯時並不知道值,所以只能等到執行時繞回,不會有任何提示。這是整數溢位容易變成 bug 的原因,使用時要先估算數值會不會超出型別的範圍。
浮點數
帶小數的數字使用浮點數,Go 有 float32 和 float64 兩種,數字代表位元數,通常使用 float64。Go 沒有叫做 float 的型別,寫了會出現編譯錯誤:
1 | var a float = 3 |
兩者都是 IEEE 754 的浮點數2 (The Go Project, 2026, Numeric types),差別主要在精度,其次是範圍:
| 型別 | 位元數 | 有效位數 $p$ | 約等於十進位 | 最大值 | 最接近 0 的正數 |
|---|---|---|---|---|---|
float32 |
32 | 24 位元 | 約 7 位 | $3.4 \times 10^{38}$ | $1.4 \times 10^{-45}$ |
float64 |
64 | 53 位元 | 約 15 至 16 位 | $1.8 \times 10^{308}$ | $5 \times 10^{-324}$ |
浮點數的精度
浮點數用有限個二進位有效位數來儲存數字。IEEE 754 把一個數寫成符號、指數與有效位數三部分,有效位數為 $p$ 位元時,
其中 $s \in \{0, 1\}$ 是符號,$b_i \in \{0, 1\}$ 是有效位數的第 $i$ 位,$n$ 是指數。第一位 $b_0$ 固定是 1,所以不必儲存,float64 實際只儲存 52 位,加上這個隱含的 1 就是 $p = 53$。也就是說,能被剛好表示的,只有用有限項 2 的負冪次湊得出來的數,例如 $0.625 = 2^{-1} + 2^{-3}$。
以 $0.625$ 為例。先用 2 的負冪次湊出來,$0.625 = 2^{-1} + 2^{-3}$,也就是二進位的 $0.101_{2}$。把小數點移到第一個 1 的後面,得到 $1.01_{2} \times 2^{-1}$。
根據式 $\eqref{eq:float-value}$ 可知,各部分是
- 符號 $s = 0$(正數)
- 指數 $n = -1$
- 有效位數 $b_0 = 1$(隱含)、$b_1 = 0$、$b_2 = 1$,之後全是 0
代入式 $\eqref{eq:float-value}$ 可得
只需要有限項,所以 0.625 存得下。可以直接看 float64 的 64 個位元:
1 | fmt.Printf("%016x\n", math.Float64bits(0.625)) |
前三位十六進位 3fe 包含符號 0 與指數欄位 $1022 = 1023 - 1$,所以 $n = -1$。後面的 4(二進位 0100)是小數部分的開頭,也就是 $b_1 = 0$、$b_2 = 1$,其餘全是 0。
根據式 $\eqref{eq:float-value}$ 可知,從 1 往上,下一個可表示的數是把最後一位 $b_{p-1}$ 設為 1,與 1 的間距是
這個間距越小,表示精度越高。float32 大約能準確到 7 位十進位數字,float64 約 15 至 16 位。
展開查看數學推導
若要換算成十進位,可以比較能區分的數量:$p$ 位元的二進位有 $2^p$ 種組合,$d$ 位的十進位有 $10^d$ 種,令兩者相等,即 $10^d = 2^p$,兩邊取 $\log_{10}$ 可得
float32 的 $p = 24$,代入式 $\eqref{eq:float-eps}$ 與式 $\eqref{eq:float-digits}$ 可得
float64 的 $p = 53$,則是
這和用 math.Nextafter 實際量出的間距(1.1920929e-07 與 2.220446049250313e-16)一致。
兩者都存不下剛好的 0.1,只是 float64 近得多3:
1 | var a float32 = 0.1 |
這是因為 0.1 要用無限多項才湊得出來,而式 $\eqref{eq:float-value}$ 最多只加到 $i = p - 1$。
展開查看數學推導
先把 0.1 寫成 $1.\text{xxx} \times 2^{n}$ 的形式。$0.1 = 1.6 \times 2^{-4}$,所以只要把 $0.6$ 轉成二進位。每次乘 2 取整數部分,整數部分是 1,表示這一位的權重放得下,剩下的小數部分繼續乘 2:
| 步驟 | 乘 2 | 整數部分(這一位) | 小數部分 |
|---|---|---|---|
| 1 | $0.6 \times 2 = 1.2$ | 1 | 0.2 |
| 2 | $0.2 \times 2 = 0.4$ | 0 | 0.4 |
| 3 | $0.4 \times 2 = 0.8$ | 0 | 0.8 |
| 4 | $0.8 \times 2 = 1.6$ | 1 | 0.6 |
第 4 步的小數部分又回到 $0.6$,所以 1001 會無限循環,不會結束:
float64 只保留小數點後 52 位,剛好是 1001 重複 13 次。被捨去的下一位起是 1001…,第一位是 1 且後面還有非零位,超過一半,所以捨入時進位,最後 4 位 1001 變成 1010。存起來的值因此比 0.1 大一點點,與上面印出的 0.10000000000000000555 一致。
可以直接看 float64 的 64 個位元:
1 | fmt.Printf("%016x\n", math.Float64bits(0.1)) |
前三位十六進位 3fb 包含符號 0 與指數欄位 $1019 = 1023 - 4$(欄位存的是 $n + 1023$),所以 $n = -4$。後面 13 位 999999999999a 就是小數部分:12 個 9(1001)加上進位後的 a(1010)。
精度不足還會造成加 1 沒有效果:float32 從 $2^{24}$ 開始、float64 從 $2^{53}$ 開始,相鄰兩數的間距大於 1,加 1 會被捨入回原來的數。
展開查看數學推導
數值落在 $2^k \leq x < 2^{k+1}$ 時,相鄰兩個可表示的數的間距是
float32 的 $p = 24$,當 $k = 24$,也就是從 $2^{24} = 16{,}777{,}216$ 開始,間距是 $2^{24-24+1} = 2$,大於 1,所以加 1 會被捨入回原來的數:
1 | var c float32 = 16777216 // 2^24 |
float64 的 $p = 53$,要到 $2^{53}$ 才會遇到同樣的情形。
通常使用 float64,是因為它的精度與範圍都夠用,而且 math 套件的函式(例如 math.Sqrt)接受的就是 float64。float32 比較適合資料量很大、需要省記憶體的情況。
字串
字串需要用雙引號 " 包起來,型別是 string。與 Python 不同的是,Go 的字串內容是以 UTF-84 編碼的位元組序列,而不是字元序列,所以 len 回傳的是位元組數:
1 | s := "Go你好" |
Python 中 len("Go你好") 是 4,在 Go 卻是 8。這是因為在 UTF-8 中,不同字元占用的位元組數不一樣:
所以 Go你好 是 $1 + 1 + 3 + 3 = 8$ 個位元組。ASCII 一共只有 128 個字元,完整的對照可以參考圖 1。
用索引取值時,取出的同樣是位元組:
1 | s := "Go你好" |
s[0] 是 G 的位元組 71,s[2] 則是 你 三個位元組中的第一個 228,並不是一個完整的字。型別 uint8 就是前面整數那一節的 0 到 255。
Go 把一個字元對應的編號稱為 rune,型別是 int32 的別名。要以字元為單位處理字串,例如計算字數,就需要用到它,這部分留到之後專門介紹字串的文章。
布林值
布林值的型別是 bool,只有 true 和 false 兩個值,而且都是小寫,與 Python 的 True、False 不同:
1 | a := true |
Python 的 True 其實就是整數 15,所以 True + 1 會得到 2。Go 的 bool 則完全不是數字,不能用 1 或 0 代替,也不能參與數值運算,連型別轉換都不行:
1 | var b bool = 1 |
這三行都無法編譯。bool 與數值型別之間沒有任何轉換,這也呼應了前面 Go 偏好明確、不自動轉換的設計。
型別轉換與零值
前面介紹的幾種型別,實際使用時常常需要互相搭配。這一節說明型別之間如何轉換,以及變數宣告後沒有給值時,Go 會幫它填上什麼。
型別轉換
在 Python 中,3 * 1.5 會自動算出 4.5。但在 Go 裡,不同的數值型別不能直接混合運算 (The Go Project, 2026, Numeric types):
1 | var a int = 3 |
要先把其中一個轉成另一個的型別,語法是
1 | 型別(值) |
以 3 與 1.5 為例,直觀的做法有兩種:把 1.5 轉成整數,或把 3 轉成浮點數。先試第一種:
1 | var a int = 3 |
但是 1.5 轉成 int 後會被截成 1,小數部分就不見了,算出來的是 3 * 1 = 3,不是我們要的 4.5。
所以要反過來,把整數轉成浮點數:
1 | var a int = 3 |
3 變成 3.0,沒有損失任何資訊。一般的原則是轉成能完整表示兩個數的型別:整數能表示的值,浮點數大多也能表示6,但整數表示不了 1.5。
為什麼 Go 不自動轉換?
自動轉換容易讓人搞不清楚運算式是不是無號、數值有多大、會不會溢位,以及結果在不同機器上是否一致。Go 選擇讓程式碼多寫一些明確的轉換,換取清楚與可移植 (The Go Project, n.d., Why does Go not provide implicit numeric conversions?)。同時,讀程式的人也能一眼看出哪裡發生了型別轉換,也就是哪裡可能損失資訊。
反方向,也就是把浮點數轉成整數時,小數部分會被捨去:
1 | var f float64 = 3.9 |
這個規則叫做向零取整(round toward zero)(The Go Project, 2026, Conversions),也就是往零的方向截斷,不是四捨五入,也不是無條件捨去(取地板)。
展開查看數學推導
用數學式表示為
其中 $\operatorname{sgn}(x)$ 是 $x$ 的正負號。代入 $x = 3.9$ 得 $1 \times \lfloor 3.9 \rfloor = 3$;代入 $x = -3.9$ 得 $-1 \times \lfloor 3.9 \rfloor = -3$。對照取地板 $\lfloor -3.9 \rfloor = -4$,可以看出式 $\eqref{eq:trunc}$ 與取地板在負數時結果不同。
若需要四捨五入,要先用 math.Round 處理,再轉成整數。
最後,常數與變數的轉換規則不同。對常數而言,編譯器在編譯時就知道 3.9 不是整數,所以會直接拒絕:
1 | fmt.Println(int(3.9)) |
常數在編譯時檢查,變數在執行時處理,這個差別在前面的整數溢位已經出現過一次,在型別轉換又出現了一次。
零值
Python 的變數必須先賦值才能使用,但是 Go 的變數一宣告就有值,而如果沒有給值時,就是該型別的零值:
1 | var n int |
零值的規則由型別決定,所有型別的零值如下 (The Go Project, 2026, The zero value):
| 型別 | 零值 |
|---|---|
bool |
false |
| 數值型別(整數、浮點數、複數) | 0 |
string |
""(空字串) |
指標、函式、interface、slice、channel、map |
nil |
陣列、struct |
每個元素或欄位各自取其型別的零值 |
因此 var i int 與 var i int = 0 完全等價。nil 留到講 slice 和 map 時再說明。
練習
下面的題目對應前面各節,每題都附有解答,預設是收起來的,先自己想過再展開。
預測輸出
題目 1:下面的程式會輸出什麼?
1 | var a int8 = 127 |
展開查看解答
1 | // -128 255 false |
a:int8的127 + 1超出範圍,繞回-128。b:uint8的0 - 1繞回 $-1 \bmod 2^8 = 255$。x+y == 0.3:0.1 與 0.2 都存不下剛好的值,相加得到0.30000000000000004,不是最接近 0.3 的那個數,所以是false。
題目 2:下面的程式會輸出什麼?跟題目 1 的最後一項為什麼不同?
1 | x, y := 0.5, 0.25 |
展開查看解答
1 | // true |
0.5 = $2^{-1}$、0.25 = $2^{-2}$、0.75 = $2^{-1} + 2^{-2}$,都能用有限項 2 的負冪次湊出來,所以都存得下,相加也沒有誤差。0.1 和 0.2 需要無限多項,才會有題目 1 的結果。
題目 3:下面的程式會輸出什麼?
1 | var ( |
展開查看解答
1 | // 0 0 "" false |
沒有給值的變數會是零值:數字型別是 0,字串是空字串,布林值是 false。
題目 4:下面的程式會輸出什麼?為什麼?
1 | a := "hi" |
展開查看解答
1 | // 2 6 |
len 回傳的是位元組數,不是字元數。UTF-8 中英文字母佔 1 個位元組,一個中文字佔 3 個位元組,所以 你好 是 $2 \times 3 = 6$。
題目 5:下面的程式會輸出什麼?怎麼算出來的?
1 | var a uint8 = 200 |
展開查看解答
1 | // 44 |
a、b 是變數,編譯器在編譯時不知道它們相加的結果,所以可以編譯,執行時才繞回。uint8 有 $2^8 = 256$ 種值,根據式 $\eqref{eq:wrap-unsigned}$ 可得 $300 \bmod 256 = 44$。
修正程式
題目 6:下面的程式能編譯嗎?不能的話,有哪兩種改法?各自輸出什麼?
1 | var a int = 10 |
展開查看解答
不能編譯。int 與 float64 是不同型別,Go 不會自動轉換:
1 | // invalid operation: a * b (mismatched types int and float64) |
改法一,把 a 轉成 float64:
1 | fmt.Println(float64(a) * b) |
改法二,把 b 轉成 int,小數部分會被丟掉(2.5 變成 2):
1 | fmt.Println(a * int(b)) |
題目 7:下面的程式想交換 x 和 y,能編譯嗎?不能的話,怎麼改?
1 | x := 10 |
展開查看解答
不能編譯,:= 左邊至少要有一個新變數,這裡 x、y 都已存在:
1 | // no new variables on left side of := |
把 := 改成 =。等號右邊會先全部算好,再一起賦值給左邊,所以不需要暫存變數:
1 | x, y = y, x |
題目 8:下面這行能編譯嗎?為什麼結果跟題目 5 不同?
1 | var c uint8 = 200 + 100 |
展開查看解答
不能編譯:
1 | // cannot use 200 + 100 (untyped int constant 300) as uint8 value in variable declaration (overflows) |
200 + 100 是常數運算式,編譯器在編譯時就算得出是 300,確定 uint8 存不下,所以直接報錯。題目 5 的 a + b 是變數,要到執行時才知道結果,只能繞回 44。
實作
題目 9:身高 170(公分)、體重 65(公斤),兩個都先宣告成 int。算出 BMI 並印出,保留 1 位小數。預期輸出是 22.5。
【註】BMI = 體重(公斤)÷ 身高(公尺)的平方。
展開查看解答
1 | height := 170 |
要先轉型再除。如果寫成 float64(height / 100),height / 100 兩邊都是 int,整數除法會先得到 1,小數部分已經丟掉了,後面轉成 float64 也救不回來。
小結
這篇介紹了 Go 的兩種變數宣告方式 var 與 :=,以及四種基本型別:整數、浮點數、字串與布林值。Go 是靜態型別的語言,不同型別之間不會自動轉換,必須明確寫出轉換,而沒有給值的變數則會得到該型別的零值。下一篇我們將會介紹運算子~
1. 有號整數在電腦中以二補數表示,溢位時只會保留最低的 $n$ 個位元,所以結果會落在範圍的另一端。 ↩
2. IEEE 754 是浮點數運算的國際標準,規定了浮點數的儲存格式與捨入方式,大多數程式語言與處理器都遵循它。 ↩
3.fmt.Printf依照格式字串輸出。%.20f是印出小數點後 20 位的浮點數,\n是換行;後面還會用到%T(型別)、%d(整數)、%q(加上引號的字串)與%t(布林值)。完整的格式請見fmt套件文件。 ↩
4. Unicode 為每個字元訂定一個編號,UTF-8 則規定這個編號要怎麼存成 1 到 4 個位元組。 ↩
5. 更精確地說,Python 的bool是int的子類別,True == 1、False == 0。 ↩
6. 更精確地說,由式 $\eqref{eq:float-gap}$ 可知,float64的 $p = 53$,當 $k \leq 52$ 時,相鄰兩數的間距 $2^{k-52}$ 不大於 1,所以 $2^{53}$ 以內的整數都能被float64精確表示,轉換不會損失資訊。超過 $2^{53}$ 的整數就不一定能精確表示了。 ↩
參考文獻
The Go Project (2026, May 26). The Go Programming Language Specification (Language version go1.27). The Go Programming Language. https://go.dev/ref/spec
The Go Project (n.d.). Frequently Asked Questions (FAQ). The Go Programming Language. https://go.dev/doc/faq