本章節深入探討 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(約略)擴容會建立新的底層陣列並複製元素。這意味著:
- 擴容操作有效能開銷
- 擴容後切片不再與原底層陣列共用記憶體
切片共用底層陣列的陷阱
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} // 小型結構體,複製成本低
}選擇指標的情況:
- 需要修改接收者/參數
- 結構體較大(通常 > 64 位元組)
- 需要表示「無值」的狀態(nil)
選擇值的情況:
- 小型、不可變的資料
- 基本型別(int、string 等)
- 不需要共用狀態