Golang
單元:基本語法與資料型態
主題:型態轉換與型態推斷
簡介
在 Go 語言中,**型別(type)**是編譯器檢查程式正確性的核心。正確的型別使用不僅能避免執行時錯誤,還能提升程式的可讀性與效能。
本單元聚焦於兩個常見且必備的技巧——型態轉換(type conversion)與型態推斷(type inference)。了解它們的運作原理,能讓你在處理數值、字串、切片或自訂結構時,寫出更安全、更簡潔的程式碼。
對於剛踏入 Go 的新手,往往會把「轉型」與「強制轉型」混為一談;而中階開發者則常在大型專案中因為型別不一致而產生維護困難。透過本篇教學,你將掌握:
- 何時需要顯式轉型、何時可以交給編譯器自動推斷。
- 常見的轉型寫法與語法細節。
- 在實務開發中避免型別相關 bug 的最佳實踐。
核心概念
1. 型態推斷(Type Inference)
Go 允許在宣告變數時省略型別,編譯器會根據右側的初始值自動推斷。
- 短宣告 (
:=):最常見的推斷方式。 var+ 初始化:若提供初始值,型別同樣會被推斷。
範例 1:短宣告的型別推斷
package main
import "fmt"
func main() {
// 編譯器依據右邊的字面值推斷型別
i := 42 // i 的型別是 int
f := 3.14 // f 的型別是 float64
s := "Hello Go" // s 的型別是 string
fmt.Printf("i: %T, f: %T, s: %T\n", i, f, s)
}
重點:
:=只能在函式內部使用;在全域範圍仍須使用var或明確指定型別。
範例 2:var 搭配初始化的推斷
package main
import "fmt"
func main() {
var (
flag = true // bool
cnt = uint(7) // uint
data = []byte{1, 2, 3} // []uint8 (alias 為 []byte)
)
fmt.Printf("%T %T %T\n", flag, cnt, data)
}
2. 顯式型態轉換(Explicit Type Conversion)
當兩個變數的型別不相容,或需要將常數轉成特定型別時,必須使用 顯式轉換。語法為 T(v),其中 T 為目標型別,v 為欲轉換的值。
2.1 基本數值型別之間的轉換
package main
import "fmt"
func main() {
var i int = 10
var f float64 = float64(i) // int → float64
var u uint = uint(f) // float64 → uint (會捨去小數部分)
fmt.Printf("i=%d, f=%.2f, u=%d\n", i, f, u)
}
注意:從浮點數轉成整數會 截斷小數,不會四捨五入。
2.2 字串與位元組切片之間的轉換
package main
import "fmt"
func main() {
// string → []byte
s := "Golang"
b := []byte(s) // 每個 rune 以 UTF-8 編碼存入 byte slice
// []byte → string
s2 := string(b)
fmt.Printf("%s -> %v -> %s\n", s, b, s2)
}
2.3 rune(int32)與字元的相互轉換
package main
import "fmt"
func main() {
var r rune = '中' // rune 本質上是 int32
fmt.Printf("rune: %U, int32: %d\n", r, r)
// rune → string(單一字元)
ch := string(r)
fmt.Println("字元字串:", ch)
}
2.4 自訂型別的轉換
自訂型別(type alias)與底層型別之間可以相互轉換,但必須顯式寫出。
package main
import "fmt"
type Meter float64
type Second int
func main() {
var distance Meter = 123.45
var time Second = 10
// Meter → float64
d := float64(distance)
// Second → int
t := int(time)
fmt.Printf("距離: %.2f m, 時間: %d s\n", d, t)
}
2.5 介面(interface)與具體型別的斷言
雖然不屬於「型別轉換」的範疇,但在使用 interface{} 時常會需要 型別斷言(type assertion)或 型別切換(type switch),這也是型別相關的重要技巧。
package main
import "fmt"
func main() {
var v interface{} = 3.14
// 型別斷言
if f, ok := v.(float64); ok {
fmt.Println("float64 value:", f)
}
// 型別切換
switch val := v.(type) {
case int:
fmt.Println("int:", val)
case float64:
fmt.Println("float64:", val)
default:
fmt.Println("unknown type")
}
}
3. 為什麼不能直接相加不同型別?
Go 的設計哲學之一是 「型別安全」。如果允許隱式轉型,會增加隱藏錯誤的機會。以下示範編譯錯誤:
package main
func main() {
var a int = 5
var b uint = 3
// fmt.Println(a + b) // ❌ 編譯錯誤:cannot use b (type uint) as type int in addition
}
必須先將其中一個變數顯式轉成相同型別,才能進行運算。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方式 |
|---|---|---|
| 浮點數 → 整數時的截斷 | 直接轉換會捨去小數,可能導致資料遺失。 | 若需四捨五入,先使用 math.Round 再轉型。 |
| 字串與 []byte 互轉的效能問題 | 每次轉換都會產生新記憶體,頻繁操作會降低效能。 | 盡量在需要時才轉換,或使用 bytes.Buffer 累積。 |
| 自訂型別與底層型別的混用 | 兩者不等價,直接相加會編譯失敗。 | 明確寫出轉換 float64(m) 或 Meter(m)。 |
| 介面斷言失敗未檢查 | 直接使用斷言會在失敗時 panic。 | 使用 value, ok := v.(T) 檢查 ok,或採用 type switch。 |
| 過度依賴型別推斷 | 雖然方便,但在公共 API 中隱藏型別會降低可讀性。 | 在函式簽名、結構體欄位等公開介面上,仍建議明確宣告型別。 |
最佳實踐
- 盡量保持型別一致:在同一算式或資料結構內,使用相同的型別,減少轉換次數。
- 使用
const搭配未明確型別:常數在未指定型別時會根據使用情境自動推斷,能避免不必要的轉型。 - 在需要精確控制時,顯式轉型:尤其是跨平台(32/64 位)或與外部 C 函式庫互動時。
- 檢查型別斷言結果:永遠使用
value, ok := iface.(T),或switch v := iface.(type) {}。 - 遵守 Go 官方的命名與風格:例如
int、uint、float64為首選,除非有特別需求才使用int32、int64。
實際應用場景
1. 讀取 JSON 並轉型
package main
import (
"encoding/json"
"fmt"
)
func main() {
raw := []byte(`{"id":123,"price":9.99,"name":"筆記本"}`)
// 使用 map[string]interface{} 讀取
var data map[string]interface{}
if err := json.Unmarshal(raw, &data); err != nil {
panic(err)
}
// 必須顯式轉型才能使用
id := int(data["id"].(float64)) // JSON 中數字預設為 float64
price := data["price"].(float64) // 直接使用 float64
name := data["name"].(string)
fmt.Printf("ID: %d, Price: %.2f, Name: %s\n", id, price, name)
}
此例展示了 JSON 解析後的型別斷言,以及將 float64 轉成 int 的實務需求。
2. 與資料庫交互(SQL driver)
SQL driver 常使用 []byte 或 string 來回傳欄位值,開發者需要在程式內做相應的轉換。
package main
import (
"database/sql"
_ "github.com/mattn/go-sqlite3"
"log"
)
func main() {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil { log.Fatal(err) }
defer db.Close()
db.Exec(`CREATE TABLE product(id INTEGER, price REAL)`)
db.Exec(`INSERT INTO product VALUES (1, 19.95)`)
var (
id int
price float64
)
row := db.QueryRow(`SELECT id, price FROM product WHERE id = ?`, 1)
if err := row.Scan(&id, &price); err != nil {
log.Fatal(err)
}
log.Printf("Product ID=%d, Price=%.2f\n", id, price)
}
在 Scan 時,driver 會自動把 SQLite 的 INTEGER、REAL 轉成 Go 的 int、float64,這正是 型別推斷 與 自動轉換 的好例子。
3. 計算時間差(time.Duration)
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now()
time.Sleep(1500 * time.Millisecond)
elapsed := time.Since(start) // elapsed 為 time.Duration
seconds := float64(elapsed) / 1e9 // 轉成秒(float64)
fmt.Printf("執行時間: %.3f 秒\n", seconds)
}
time.Duration 本質上是 int64,但常需要轉成 float64 或 int 以便於顯示或計算。
總結
- 型別推斷 讓程式碼更簡潔,但僅限於局部變數或有明確初始值的情況。
- 顯式型別轉換 是保證型別安全的唯一途徑,特別在不同數值型別、字串與位元組切片、或自訂型別之間切換時必不可少。
- 常見陷阱包括浮點數截斷、字串/切片的頻繁轉換、介面斷言未檢查等,遵守最佳實踐可大幅降低錯誤率。
- 在實務開發(JSON 解析、資料庫存取、時間計算)中,正確的型別轉換與推斷是保證程式正確性與效能的關鍵。
掌握了 型態轉換與型態推斷,你就能在 Go 專案中更自信地處理各種資料,寫出既安全又易讀的程式碼。祝你在 Golang 的學習之路上越走越遠!