Go 資料與記憶體分配 (Data)
整理自 Effective Go - Data,涵蓋 new / make、複合字面量、陣列 / 切片 / 映射 與 append。
1. 分配:new 與 make
| 函數 | 用途 | 回傳型別 | 適用類型 |
|---|---|---|---|
new(T) | 分配已置零的記憶體 | *T(指標) | 任意型別 |
make(T, args) | 初始化內部結構 | T(值,非指標) | 僅 slice、map、channel |
new(T)
- 只做「置零」,不呼叫建構函式;回傳的是指向零值的指標。
- 設計資料結構時,可善用「零值即可用」:例如
bytes.Buffer、sync.Mutex的零值都可直接使用。
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // 零值,可直接使用
make(T, args)
- slice、map、channel 底層是引用型別,必須先初始化(例如切片的 pointer、len、cap)才能用,因此用
make。 new([]int)得到的是「指向 nil slice 的指標」,實務上幾乎用make([]int, len, cap)。
// 不建議
var p *[]int = new([]int) // *p == nil
*p = make([]int, 100, 100)
// 建議
v := make([]int, 100)
2. 建構函式與複合字面量 (Composite Literals)
- 零值不夠用時,可用複合字面量建立並初始化實例。
- 回傳局部變數的位址在 Go 是安全的(編譯器會做逃逸分析)。
// 依欄位順序
f := &File{fd, name, nil, 0}
// 欄位: 值,順序可打散,未給的為零值
f := &File{fd: fd, name: name}
// 無欄位 = 零值,等價於 new(File)
&File{}
複合字面量也可用於 array、slice、map(標籤為索引或鍵):
a := [...]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
s := []string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
m := map[int]string{Enone: "no error", Eio: "Eio", Einval: "invalid argument"}
3. 陣列 (Arrays)
陣列是長度固定且索引連續的資料結構。與 C 的差異:
- 陣列是值:賦值會複製整個陣列;傳入函數也是複本,不是指標。
- 長度是型別的一部分:
[10]int與[20]int是不同型別。
宣告與初始化
// 先宣告後賦值(未給的為零值)
var a [3]int
a[0] = 10
// 字面量,長度寫死
b := [3]int{1, 2, 3}
// 編譯器推斷長度
c := [...]int{4, 5, 6, 7} // 長度為 4
// 指定索引賦值(複合字面量,索引為標籤)
d := [5]int{1: 10, 4: 40} // [0, 10, 0, 0, 40]
記憶體佈局
陣列在記憶體中是連續區塊,大小在編譯時就確定,存取快,但長度不可變。
遍歷
配合 for range:
arr := [3]string{"Go", "Rust", "C++"}
for i, v := range arr {
fmt.Printf("索引:%d, 數值:%s\n", i, v)
}
// 不需索引時用 _ 忽略
for _, v := range arr {
fmt.Println(v)
}
關鍵特性
- 值型別:賦值或當參數傳遞時會複製整個陣列;函數內修改不影響呼叫端。
- 長度不可變:需動態大小時應改用 Slice。
需要「像 C 一樣傳址」時,可傳指標;但慣用寫法是用 slice。
func Sum(a *[3]float64) (sum float64) {
for _, v := range *a {
sum += v
}
return
}
func main() {
array := [...]float64{7.0, 8.5, 9.1}
x := Sum(&array)
fmt.Println(x)
}
陣列 vs 切片
| 特性 | 陣列 [N]T | 切片 []T |
|---|---|---|
| 長度 | 固定 | 動態 |
| 傳遞/賦值 | 值拷貝 | 引用(header 為值) |
| 實務使用 | 固定格式、配置 | 絕大多數場景 |
Struct 陣列
標準寫法:先定義型別
type User struct {
ID int
Name string
}
users := [2]User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
匿名 Struct(臨時、單處使用)
config := [2]struct {
Endpoint string
Port int
}{
{Endpoint: "localhost", Port: 8080},
{Endpoint: "127.0.0.1", Port: 9000},
}
存放指標(大 struct 或需在傳遞後修改原物件)
type Item struct { Title string }
items := [2]*Item{
&Item{Title: "Book"},
&Item{Title: "Game"},
}
| 儲存方式 | 優點 | 缺點 |
|---|---|---|
[N]Struct | 連續記憶體、快取友善 | 大陣列賦值/傳參拷貝貴 |
[N]*Struct | 只拷貝指標 | 存取需尋址 |
巢狀初始化(內層型別可省略)
type Point struct { X, Y int }
points := [3]Point{
{1, 2}, {3, 4}, {5, 6},
}
實務建議: API、資料庫等動態資料多用
[]User(Slice);陣列適合固定長度配置或靜態資料。
4. 切片 (Slices)
- 切片是對底層陣列的引用;多個 slice 可指向同一陣列。
- 函數內對元素的修改,呼叫端會看到(等同於傳底層陣列的引用);但 slice 本身(header)是傳值,所以
append後要回傳新 slice 並重新賦值。 - 可以把 Slice 想像成一個視窗,它透視著底層的陣列。
內部結構 (The Header)
Slice 是一個很小的結構體(Header),在 64 位元架構下只佔 24 bytes:
| 欄位 | 大小 | 說明 |
|---|---|---|
| Pointer | 8 bytes | 指向底層陣列的起始位置 |
| Len | 8 bytes | 目前視窗看到的元素個數 |
| Cap | 8 bytes | 從指標位置開始算,底層陣列總共能裝多少元素 |