本文 AI 產出,尚未審核

Golang

單元:基本語法與資料型態

主題:型態轉換與型態推斷


簡介

在 Go 語言中,**型別(type)**是編譯器檢查程式正確性的核心。正確的型別使用不僅能避免執行時錯誤,還能提升程式的可讀性與效能。
本單元聚焦於兩個常見且必備的技巧——型態轉換(type conversion)型態推斷(type inference)。了解它們的運作原理,能讓你在處理數值、字串、切片或自訂結構時,寫出更安全、更簡潔的程式碼。

對於剛踏入 Go 的新手,往往會把「轉型」與「強制轉型」混為一談;而中階開發者則常在大型專案中因為型別不一致而產生維護困難。透過本篇教學,你將掌握:

  1. 何時需要顯式轉型、何時可以交給編譯器自動推斷。
  2. 常見的轉型寫法與語法細節。
  3. 在實務開發中避免型別相關 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 中隱藏型別會降低可讀性。 在函式簽名、結構體欄位等公開介面上,仍建議明確宣告型別。

最佳實踐

  1. 盡量保持型別一致:在同一算式或資料結構內,使用相同的型別,減少轉換次數。
  2. 使用 const 搭配未明確型別:常數在未指定型別時會根據使用情境自動推斷,能避免不必要的轉型。
  3. 在需要精確控制時,顯式轉型:尤其是跨平台(32/64 位)或與外部 C 函式庫互動時。
  4. 檢查型別斷言結果:永遠使用 value, ok := iface.(T),或 switch v := iface.(type) {}
  5. 遵守 Go 官方的命名與風格:例如 intuintfloat64 為首選,除非有特別需求才使用 int32int64

實際應用場景

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 常使用 []bytestring 來回傳欄位值,開發者需要在程式內做相應的轉換。

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 的 INTEGERREAL 轉成 Go 的 intfloat64,這正是 型別推斷自動轉換 的好例子。

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,但常需要轉成 float64int 以便於顯示或計算。


總結

  • 型別推斷 讓程式碼更簡潔,但僅限於局部變數或有明確初始值的情況。
  • 顯式型別轉換 是保證型別安全的唯一途徑,特別在不同數值型別、字串與位元組切片、或自訂型別之間切換時必不可少。
  • 常見陷阱包括浮點數截斷、字串/切片的頻繁轉換、介面斷言未檢查等,遵守最佳實踐可大幅降低錯誤率。
  • 在實務開發(JSON 解析、資料庫存取、時間計算)中,正確的型別轉換與推斷是保證程式正確性與效能的關鍵。

掌握了 型態轉換與型態推斷,你就能在 Go 專案中更自信地處理各種資料,寫出既安全又易讀的程式碼。祝你在 Golang 的學習之路上越走越遠!