本章節深入探討 Go 語言的核心資料結構,包括陣列、切片、字典、結構體、介面和指標。

陣列與切片#

陣列基礎#

陣列是固定長度的同型別元素序列。陣列長度是型別的一部分。

// 陣列宣告
var arr1 [5]int                    // 零值初始化
arr2 := [5]int{1, 2, 3, 4, 5}      // 完整初始化
arr3 := [...]int{1, 2, 3}          // 自動推斷長度
arr4 := [5]int{0: 1, 4: 5}         // 指定索引初始化

[3]int[5]int不同的型別。陣列的長度是編譯時期常數,無法在執行時期改變。

切片本質#

切片是對底層陣列的一個「視窗」,包含三個欄位:

type slice struct {
    array unsafe.Pointer  // 指向底層陣列的指標
    len   int             // 切片長度
    cap   int             // 切片容量
}
// 切片建立方式
s1 := make([]int, 5)        // 長度 5,容量 5
s2 := make([]int, 5, 10)    // 長度 5,容量 10
s3 := []int{1, 2, 3}        // 字面量初始化
s4 := arr[1:4]              // 從陣列切取

切片視窗比喻#

可以把切片想像成一個可以移動和伸縮的視窗,視窗後面是底層陣列:

arr := [8]int{0, 1, 2, 3, 4, 5, 6, 7}
s := arr[2:6]  // s = [2, 3, 4, 5]

// 底層陣列:[0, 1, 2, 3, 4, 5, 6, 7]
//                  ↑──視窗──↑
// len(s) = 4(視窗寬度)
// cap(s) = 6(從起點到陣列末端的距離)

切片只能向右擴展(增加容量),無法向左擴展。一旦切片的起點確定,容量的上限就固定了。

切片擴容機制#

s := make([]int, 0, 4)
s = append(s, 1, 2, 3, 4)  // 長度 4,容量 4
s = append(s, 5)           // 觸發擴容

// 擴容規則(Go 1.18+):
// - 容量 < 256:新容量 = 舊容量 * 2
// - 容量 >= 256:新容量 = 舊容量 * 1.25 + 192(約略)

擴容會建立新的底層陣列並複製元素。這意味著:

  1. 擴容操作有效能開銷
  2. 擴容後切片不再與原底層陣列共用記憶體
切片共用底層陣列的陷阱
arr := [5]int{1, 2, 3, 4, 5}
s1 := arr[1:4]  // [2, 3, 4]
s2 := arr[2:5]  // [3, 4, 5]

// s1 和 s2 共用同一個底層陣列
s1[1] = 100     // 修改 s1
fmt.Println(s2) // [100, 4, 5] - s2 也被影響!

// 解決方案:使用 copy 建立獨立切片
s3 := make([]int, len(s1))
copy(s3, s1)

字典(Map)#

字典基礎#

字典是鍵值對的集合,提供常數時間的查找、插入和刪除操作。

// 字典建立
m1 := make(map[string]int)
m2 := map[string]int{
    "apple":  5,
    "banana": 3,
}

// 基本操作
m1["key"] = 10           // 插入/更新
value := m1["key"]       // 讀取
delete(m1, "key")        // 刪除
value, ok := m1["key"]   // 檢查是否存在

鍵型別限制#

字典的鍵型別必須是可比較的(comparable)。以下型別不能作為鍵:

  • 函式型別
  • 字典型別
  • 切片型別
// 合法的鍵型別
map[int]string           // 整數
map[string]int           // 字串
map[[3]int]string        // 陣列(固定長度)
map[struct{x,y int}]int  // 結構體(欄位都可比較)

// 非法的鍵型別
// map[[]int]string      // 錯誤:切片不可比較
// map[func()]string     // 錯誤:函式不可比較
// map[map[int]int]int   // 錯誤:字典不可比較

字典的零值與 nil#

var m map[string]int  // nil 字典

// 讀取 nil 字典不會 panic
v := m["key"]  // v = 0(零值)

// 寫入 nil 字典會 panic!
// m["key"] = 1  // panic: assignment to entry in nil map

// 必須先初始化
m = make(map[string]int)
m["key"] = 1  // 正常

遍歷字典#

m := map[string]int{"a": 1, "b": 2, "c": 3}

// 遍歷鍵值對
for key, value := range m {
    fmt.Println(key, value)
}

// 僅遍歷鍵
for key := range m {
    fmt.Println(key)
}

字典的遍歷順序是隨機的。如果需要有序遍歷,必須先將鍵排序:

keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Println(k, m[k])
}

結構體與方法#

結構體定義#

type Person struct {
    Name string
    Age  int
    addr string  // 私有欄位(小寫開頭)
}

// 建立結構體實體
p1 := Person{Name: "Alice", Age: 30}
p2 := Person{"Bob", 25, "taipei"}  // 按順序,需提供所有欄位
p3 := new(Person)                   // 回傳 *Person,零值初始化

方法定義#

方法是與特定型別關聯的函式:

// 值接收者
func (p Person) Greet() string {
    return "Hello, " + p.Name
}

// 指標接收者
func (p *Person) SetAge(age int) {
    p.Age = age  // 可以修改接收者
}

// 使用方法
person := Person{Name: "Alice", Age: 30}
fmt.Println(person.Greet())  // 值或指標都可呼叫

person.SetAge(31)            // 自動取址
(&person).SetAge(32)         // 等效

選擇接收者型別的建議

  • 需要修改接收者狀態 -> 指標接收者
  • 接收者是大型結構體 -> 指標接收者(避免複製)
  • 一致性:同一型別的方法應使用相同的接收者型別

結構體嵌入#

Go 使用嵌入(embedding)取代繼承:

type Animal struct {
    Name string
}

func (a Animal) Speak() string {
    return "..."
}

type Dog struct {
    Animal  // 嵌入 Animal
    Breed   string
}

func (d Dog) Speak() string {
    return "Woof!"  // 覆寫方法
}

// 使用
dog := Dog{
    Animal: Animal{Name: "Buddy"},
    Breed:  "Golden Retriever",
}
fmt.Println(dog.Name)    // 直接存取嵌入欄位
fmt.Println(dog.Speak()) // Woof!
嵌入與組合的差異
// 嵌入(embedding)- 匿名欄位
type Manager struct {
    Person  // 可以直接呼叫 m.Name, m.Greet()
    Team    []Person
}

// 組合(composition)- 具名欄位
type Manager struct {
    Employee Person  // 必須透過 m.Employee.Name, m.Employee.Greet()
    Team     []Person
}

嵌入會將內部型別的方法「提升」到外部型別,使其可以直接呼叫。

介面型別#

介面定義與實作#

Go 的介面是隱式實作的(duck typing):

// 定義介面
type Writer interface {
    Write(data []byte) (int, error)
}

type Reader interface {
    Read(buf []byte) (int, error)
}

// 介面組合
type ReadWriter interface {
    Reader
    Writer
}

// 隱式實作
type FileBuffer struct {
    data []byte
}

func (f *FileBuffer) Write(data []byte) (int, error) {
    f.data = append(f.data, data...)
    return len(data), nil
}

func (f *FileBuffer) Read(buf []byte) (int, error) {
    n := copy(buf, f.data)
    return n, nil
}

// FileBuffer 自動實作了 Writer、Reader、ReadWriter 介面

Go 的介面實作不需要 implements 關鍵字。只要型別實作了介面定義的所有方法,就自動滿足該介面。

空介面#

interface{} 是空介面,可以儲存任何型別的值:

var any interface{}

any = 42
any = "hello"
any = []int{1, 2, 3}

// Go 1.18+ 可以使用 any 作為 interface{} 的別名
var value any = "test"

介面值的內部結構#

介面值由兩部分組成:

// 介面內部結構(概念)
type iface struct {
    tab  *itab          // 型別資訊
    data unsafe.Pointer // 實際值的指標
}
var w Writer
var fb *FileBuffer

// w 的 tab 和 data 都是 nil
fmt.Println(w == nil)  // true

w = fb  // fb 是 nil 指標

// w 的 tab 指向 *FileBuffer 型別資訊
// w 的 data 是 nil
fmt.Println(w == nil)  // false!

即使將 nil 指標賦值給介面,介面值也不等於 nil。這是常見的陷阱,應該避免將 nil 指標傳遞給介面型別。

判斷介面是否真正為空
func IsNil(i interface{}) bool {
    if i == nil {
        return true
    }
    v := reflect.ValueOf(i)
    switch v.Kind() {
    case reflect.Ptr, reflect.Map, reflect.Slice,
         reflect.Chan, reflect.Func, reflect.Interface:
        return v.IsNil()
    }
    return false
}

指標操作#

指標基礎#

x := 42
p := &x      // 取址
*p = 100     // 透過指標修改值
fmt.Println(x)  // 100

// new 函式
ptr := new(int)  // 組態記憶體並回傳指標
*ptr = 42

可定址性規則#

不是所有值都可以取址。以下值是不可定址的

  • 常數
  • 字面量(除了複合字面量)
  • 函式呼叫的回傳值
  • 型別轉換的結果
  • 算術運算結果
// 不可定址
// _ = &10        // 錯誤:常數不可定址
// _ = &"hello"   // 錯誤:字串字面量不可定址
// _ = &getValue() // 錯誤:函式回傳值不可定址

// 可定址
var x int = 10
_ = &x           // 正確:變數可定址

s := []int{1, 2, 3}
_ = &s[0]        // 正確:切片元素可定址

// 注意:陣列字面量元素不可定址
// arr := [3]int{1, 2, 3}
// _ = &arr[0]   // 正確:變數陣列的元素可定址
// _ = &([3]int{1, 2, 3}[0])  // 錯誤:字面量陣列的元素不可定址

unsafe.Pointer#

unsafe.Pointer 是通用指標型別,可以在不同指標型別間轉換:

import "unsafe"

var x int64 = 1

// 取得 x 的位址並轉換為 unsafe.Pointer
ptr := unsafe.Pointer(&x)

// 轉換為 *int64 並存取
y := *(*int64)(ptr)
fmt.Println(y)  // 1

// 指標運算
// 取得 x 後面一個位元組的位址
nextByte := unsafe.Pointer(uintptr(ptr) + 1)

unsafe.Pointer 繞過了 Go 的型別安全機制,應該謹慎使用。錯誤的使用可能導致記憶體損壞或未定義行為。

常見的 unsafe 操作模式
// 模式 1:不同指標型別轉換
var f float64 = 1.5
bits := *(*uint64)(unsafe.Pointer(&f))

// 模式 2:指標運算
type Header struct {
    Length int
    Data   [10]byte
}

h := &Header{Length: 5}
dataPtr := unsafe.Pointer(uintptr(unsafe.Pointer(h)) +
                          unsafe.Offsetof(h.Data))

// 模式 3:與 C 程式碼互動(cgo)
// 將 Go 指標轉換為 C 可用的指標

指標與值的選擇#

// 回傳指標的函式
func NewPerson(name string) *Person {
    return &Person{Name: name}  // Go 會自動將變數分配到堆上
}

// 回傳值的函式
func CreatePoint(x, y int) Point {
    return Point{x, y}  // 小型結構體,複製成本低
}

選擇指標的情況

  1. 需要修改接收者/參數
  2. 結構體較大(通常 > 64 位元組)
  3. 需要表示「無值」的狀態(nil)

選擇值的情況

  1. 小型、不可變的資料
  2. 基本型別(int、string 等)
  3. 不需要共用狀態