一、數組
先記住數組的核心特點:盒子大小一旦定了就改不了(長度固定),但盒子里的東西能換(元素值可變)。就像你買了個能裝 3 個蘋果的鐵皮盒,想多裝 1 個都不行,但里面的蘋果可以換成橘子。
1.1 數組的定義:必須指定 “盒子容量”
Golang 里數組的 “容量”(長度)是類型的一部分,比如 [3]int
和 [5]int
是完全不同的類型。
package mainimport "fmt"func main() {// 定義數組的 3 種方式var a1 [3]int // 方式 1:聲明變量,指定長度a2 := [3]int{1, 2, 3} // 方式 2:初始化時指定長度和元素a3 := [...]int{1, 2, 3} // 方式 3:省略長度,由編譯器推導fmt.Println("a1:", a1) // 輸出:a1: [0 0 0](默認初始化為零值)fmt.Println("a2:", a2) // 輸出:a2: [1 2 3]fmt.Println("a3:", a3) // 輸出:a3: [1 2 3]
}
坑點:數組不能像 Python 列表那樣 “動態加元素”,比如 a0[3] = 4
會報錯 —— 因為 [3]int
只有 0、1、2 三個索引。
1.2 數組是 “值傳遞”:賦值會拷貝整個盒子
這是數組最容易踩的坑!當你把一個數組賦值給另一個變量時,Golang 會復制整個數組的內容,而不是共享同一個盒子。看代碼:
func main() {// 1. 先理解普通變量的值傳遞(類比數組值傳遞)var b = 10 // b在內存中占8字節(64位系統int),地址&bb1 := b // 拷貝b的值(10)到新內存地址&b1,b和b1是獨立變量b = 20 // 修改b的值,只影響b的內存地址,b1不變// 打印對比:地址不同,值不同fmt.Printf("b: 值=%v, 地址=%p\n", b, &b) // 輸出:b: 值=20, 地址=0xc0000a6058fmt.Printf("b1: 值=%v, 地址=%p\n", b1, &b1) // 輸出:b1: 值=10, 地址=0xc0000a6070// 2. 數組的值傳遞(核心:拷貝整個數組的所有元素,不是地址)a0 := [3]int{1, 2, 3} // a0占用24字節(3個int,每個8字節),地址&a0a1 := a0 // 拷貝a0的24字節到新地址&a1,a0和a1是獨立數組a0[0] = 100 // 修改a0[0](地址&a0[0]),不影響a1[0](地址&a1[0])// 打印數組整體信息:地址不同(數組對象地址)fmt.Printf("a0: 值=%v, 數組地址=%p, 長度=%d\n", a0, &a0, len(a0)) // 輸出:a0: 值=[100 2 3], 數組地址=0xc0000a8000, 長度=3fmt.Printf("a1: 值=%v, 數組地址=%p, 長度=%d\n", a1, &a1, len(a1)) // 輸出:a1: 值=[1 2 3], 數組地址=0xc0000a8018, 長度=3// 打印數組元素地址:每個元素地址都不同(證明拷貝了所有元素)fmt.Printf("a0[0]地址=%p, a0[1]地址=%p, a0[2]地址=%p\n", &a0[0], &a0[1], &a0[2])// 輸出:a0[0]地址=0xc0000a8000, a0[1]地址=0xc0000a8008, a0[2]地址=0xc0000a8010fmt.Printf("a1[0]地址=%p, a1[1]地址=%p, a1[2]地址=%p\n", &a1[0], &a1[1], &a1[2])// 輸出:a1[0]地址=0xc0000a8018, a1[1]地址=0xc0000a8020, a1[2]地址=0xc0000a8028
}
總結:數組賦值 = 拷貝新數組,修改一個不會影響另一個。如果數組很大(比如 10 萬元素),這種拷貝會浪費內存,這也是為什么 Golang 更常用切片的原因。
1.3 數組的內存結構:順序存儲,首地址 = 第一個元素地址
數組的元素在內存中是 “挨在一起” 存儲的,沒有空隙。比如 [3]int
在 64 位系統中,每個 int
占 8 字節,整個數組占 24 字節,且數組的地址等于第一個元素的地址:
特殊情況:字符串數組的內存
如果數組元素是字符串(比如 [5]string
),內存結構會不一樣:數組中存的不是字符串本身,而是 “字符串的指針 + 長度”(共 16 字節),因為字符串長度不固定,直接存在數組里會 “撐爆”。看代碼:
func main() {// 1. int數組的內存結構(值直接存儲在數組內存中)a0 := [3]int{1, 2, 3} // 64位系統:每個int=8字節,數組總大小=3*8=24字節fmt.Printf("數組a0的地址: %p\n", &a0) // 數組地址=第一個元素地址fmt.Printf("a0[0]的地址: %p(值=%d)\n", &a0[0], a0[0]) // 第一個元素地址fmt.Printf("a0[1]的地址: %p(值=%d)\n", &a0[1], a0[1]) // 比a0[0]大8字節(連續)fmt.Printf("a0[2]的地址: %p(值=%d)\n", &a0[2], a0[2]) // 比a0[1]大8字節(連續)// 運行結果(地址規律):// 數組a0的地址: 0xc0000a8000// a0[0]的地址: 0xc0000a8000(值=1) → 和數組地址相同// a0[1]的地址: 0xc0000a8008(值=2) → 0xc0000a8000 + 8// a0[2]的地址: 0xc0000a8010(值=3) → 0xc0000a8008 + 8// 2. string數組的內存結構(數組中存“指針+長度”,不是字符串本身)// 字符串在Golang中是結構體:type string struct { ptr *byte; len int } → 共16字節(8+8)aStr := [3]string{"a", // 長度1,指針指向存儲'a'的內存"bc", // 長度2,指針指向存儲'b'+'c'的內存"你好", // 長度2(rune數),但字節數3(UTF-8),指針指向對應字節}fmt.Println("\nstring數組元素地址(間隔16字節):")for i := 0; i < 3; i++ {fmt.Printf("aStr[%d]: 值=%s, 地址=%p, 字符串長度(字節數)=%d\n", i, aStr[i], &aStr[i], len(aStr[i]))}// 運行結果(地址間隔16字節):// aStr[0]: 值=a, 地址=0xc0000d0000, 字符串長度(字節數)=1// aStr[1]: 值=bc, 地址=0xc0000d0010(0xc0000d0000 + 16), 字符串長度(字節數)=2// aStr[2]: 值=你好, 地址=0xc0000d0020(0xc0000d0010 + 16), 字符串長度(字節數)=6// 驗證:string數組元素地址間隔=16字節(指針8+長度8)fmt.Printf("aStr[0]到aStr[1]地址差: %d字節\n", &aStr[1] - &aStr[0]) // 輸出:aStr[0]到aStr[1]地址差: 16字節
}
1.4 數組的常用操作:訪問、修改、遍歷
訪問元素:用數組
[索引]
,索引從 0 開始,最后一個元素的索引是len(數組)-1
(避免越界)。修改元素:直接賦值
數組[索引] = 新值
(元素值可變,地址不變)。遍歷元素:用
for
循環或for range
(推薦后者,更簡潔)。
代碼示例:
func main() {a7 := [9]int{100, 200, 300, 400, 500, 600, 700, 800, 900}// 1. 元素訪問:索引從0開始,最后一個元素=len(a7)-1(通用寫法,避免硬編碼)fmt.Println("a7的長度:", len(a7)) // 輸出:9fmt.Println("第一個元素(索引0):", a7[0]) // 輸出:100fmt.Println("最后一個元素(索引len-1):", a7[len(a7)-1]) // 輸出:900fmt.Println("倒數第二個元素(索引len-2):", a7[len(a7)-2]) // 輸出:800// 2. 元素修改:修改值,元素地址不變(數組內存固定,只換“內容”)fmt.Printf("修改前:a7[8]值=%d, 地址=%p\n", a7[8], &a7[8]) // 輸出:修改前:a7[8]值=900, 地址=0xc0000b0048a7[8] = 999 // 修改值fmt.Printf("修改后:a7[8]值=%d, 地址=%p\n", a7[8], &a7[8]) // 輸出:修改后:a7[8]值=999, 地址=0xc0000b0048(地址不變)// 3. for循環遍歷(適合需要索引控制的場景,如跳過某些元素)fmt.Println("\nfor循環遍歷(只打印偶數索引元素):")for i := 0; i < len(a7); i += 2 { // i步長2,只遍歷0、2、4、6、8索引fmt.Printf("索引%d: 值=%d\n", i, a7[i])}// 運行結果:// 索引0: 值=100// 索引2: 值=300// 索引4: 值=500// 索引6: 值=700// 索引8: 值=999// 4. for range遍歷(適合簡單遍歷,返回索引i和元素值v)// 注意:v是元素的拷貝,修改v不影響原數組fmt.Println("\nfor range遍歷(驗證v是拷貝):")for i, v := range a7 {v += 100 // 修改v(拷貝值),原數組不變fmt.Printf("索引%d: 原數組值=%d, 拷貝值v=%d\n", i, a7[i], v)}// 運行結果(原數組值不變,v是修改后的值):// 索引0: 原數組值=100, 拷貝值v=200// 索引1: 原數組值=200, 拷貝值v=300// ...(后續索引同理)
}
二、切片
數組的 “固定大小” 太死板,所以 Golang 設計了切片(slice) —— 它像一個 “魔法盒子”,底層依賴數組,但可以動態調整大小(本質是擴容時換一個更大的底層數組)。
2.1 切片和數組的核心區別
特性 | 數組 | 切片 |
---|---|---|
長度 | 固定(類型的一部分) | 可變(動態調整) |
類型表示 | [n]T (如?[3]int ) | []T (如?[]int ) |
底層依賴 | 無(自身就是存儲) | 依賴數組(切片是 “視圖”) |
賦值行為 | 值傳遞(拷貝整個數組) | 引用傳遞(拷貝 “視圖信息”) |
2.2?切片定義:3 種方式 + header 結構驗證
字面量定義(len=cap);make 定義(指定 len+cap,cap 省略默認 = len);從數組 / 切片派生(子切片基礎);header 結構(array 指針、len、cap)的打印驗證。
package main
import "fmt"func main() {// 1. 字面量定義切片(類似數組,無長度)s1 := []int{1, 2, 3} // 切片header包含3部分:array指針(指向底層數組)、len(可用元素數)、cap(底層數組容量)fmt.Printf("s1: 值=%v, 類型=%T\n", s1, s1)fmt.Printf("s1: len=%d, cap=%d\n", len(s1), cap(s1)) // len=3, cap=3(底層數組長度=3)fmt.Printf("s1: header地址=%p, 底層數組地址(array指針)=%p\n", &s1, &s1[0])// 運行結果:// s1: 值=[1 2 3], 類型=[]int// s1: len=3, cap=3// s1: header地址=0xc000008030, 底層數組地址(array指針)=0xc0000140d8// 2. make定義切片(推薦,可控制len和cap,避免頻繁擴容)// 語法:make([]T, len, cap) → cap必須≥len,否則編譯錯誤s2 := make([]int, 3) // cap省略,默認=len=3,底層數組是[0,0,0]s3 := make([]int, 1, 2) // len=1(可用元素1個),cap=2(底層數組能存2個元素)s4 := make([]string, 2, 5)// 字符串切片,len=2(零值=""),cap=5// 打印s2細節(len=3, cap=3,元素為零值0)fmt.Printf("\ns2: 值=%v, len=%d, cap=%d\n", s2, len(s2), cap(s2)) // 輸出:s2: 值=[0 0 0], len=3, cap=3fmt.Printf("s2[0]地址=%p(底層數組地址)\n", &s2[0]) // 輸出:s2[0]地址=0xc0000140f0(底層數組獨立于s1)// 打印s3細節(len=1,只能訪問s3[0],s3[1]雖在底層數組但不可訪問)fmt.Printf("\ns3: 值=%v, len=%d, cap=%d\n", s3, len(s3), cap(s3)) // 輸出:s3: 值=[0], len=1, cap=2// fmt.Println(s3[1]) // 運行錯誤:index out of range [1] with length 1(len=1,索引1越界)// 打印s4細節(字符串切片零值為"",每個元素占16字節,底層數組存指針+長度)fmt.Printf("\ns4: 值=%v, len=%d, cap=%d\n", s4, len(s4), cap(s4)) // 輸出:s4: 值=["" ""], len=2, cap=5fmt.Printf("s4[0]地址=%p, s4[1]地址=%p(間隔16字節)\n", &s4[0], &s4[1])// 輸出:s4[0]地址=0xc0000d0000, s4[1]地址=0xc0000d0010(間隔16字節)// 3. 從數組派生切片(切片底層數組=原數組,header的array指針指向數組起始位置)arr := [5]int{1, 2, 3, 4, 5} // 原數組:len=5, cap=5s5 := arr[1:3] // 切取索引1-2(前包后不包),len=2, cap=5-1=4(cap=原數組cap - start)fmt.Printf("\ns5(從數組派生): 值=%v, len=%d, cap=%d\n", s5, len(s5), cap(s5)) // 輸出:s5: 值=[2 3], len=2, cap=4fmt.Printf("s5底層數組地址=%p, 原數組arr[1]地址=%p(相同,證明共享數組)\n", &s5[0], &arr[1])// 輸出:s5底層數組地址=0xc000014118, 原數組arr[1]地址=0xc000014118(共享底層數組)
}
2.3?切片引用傳遞:header 拷貝 + 底層數組共享
切片賦值 / 傳參拷貝的是 header(24 字節:array 指針 8+len8+cap8);共享底層數組時修改元素互影響;header 地址不同但 array 指針相同。
// 定義函數:接收切片參數,驗證引用傳遞
func showAddr(arr []int) []int {// 1. 打印函數內切片的header地址和底層數組地址// 重點:&arr是函數內切片的header地址(和main中a0的header地址不同)// &arr[0]是底層數組地址(和main中a0的底層數組地址相同)fmt.Printf("函數內(未append): \n")fmt.Printf(" 切片值=%v, header地址=%p, 底層數組地址=%p\n", arr, &arr, &arr[0])fmt.Printf(" len=%d, cap=%d\n", len(arr), cap(arr))// 2. append元素(未擴容,因為3+2=5 ≤ cap=3?不,原cap=3,3+2=5>3,會擴容)arr = append(arr, 123, 321) // 3. 打印擴容后的數據:底層數組地址變化(新數組),header地址不變(還是函數內的arr header)fmt.Printf("函數內(append后): \n")fmt.Printf(" 切片值=%v, header地址=%p, 底層數組地址=%p\n", arr, &arr, &arr[0])fmt.Printf(" len=%d, cap=%d\n", len(arr), cap(arr))return arr // 返回的是函數內arr的header拷貝(array指針指向新底層數組)
}func main() {// 原切片a0:len=3, cap=3,底層數組[1,2,3]a0 := []int{1, 2, 3}fmt.Printf("main內(初始a0): \n")fmt.Printf(" 切片值=%v, header地址=%p, 底層數組地址=%p\n", a0, &a0, &a0[0])fmt.Printf(" len=%d, cap=%d\n", len(a0), cap(a0))// 運行結果:// main內(初始a0): // 切片值=[1 2 3], header地址=0xc000008030, 底層數組地址=0xc0000140d8// len=3, cap=3// 調用函數:傳遞a0的header拷貝(array指針=0xc0000140d8, len=3, cap=3)a2 := showAddr(a0)// 打印main內最終狀態:a0未變(底層數組還是老的),a2是新切片(底層數組新的)fmt.Printf("\nmain內(最終): \n")fmt.Printf(" a0值=%v, header地址=%p, 底層數組地址=%p, len=%d, cap=%d\n", a0, &a0, &a0[0], len(a0), cap(a0))fmt.Printf(" a2值=%v, header地址=%p, 底層數組地址=%p, len=%d, cap=%d\n", a2, &a2, &a2[0], len(a2), cap(a2))// 運行結果:// main內(最終): // a0值=[1 2 3], header地址=0xc000008030, 底層數組地址=0xc0000140d8, len=3, cap=3// a2值=[1 2 3 123 321], header地址=0xc000008060, 底層數組地址=0xc00000e3f0, len=5, cap=6// 關鍵結論:// 1. a0和函數內arr的header地址不同(0xc000008030 vs 0xc000008078)→ 證明拷貝了header// 2. 未擴容前,a0和函數內arr的底層數組地址相同(0xc0000140d8)→ 共享底層數組// 3. 擴容后,函數內arr的底層數組地址變了(0xc00000e3f0)→ 新數組// 4. a0的底層數組仍為老地址→ a0不變,a2用新數組→ 兩者獨立
}
2.4?append 操作:擴容策略 + 不同場景驗證
append 未擴容(len 增加,cap 不變,共享底層數組);append 擴容(1.18 + 策略:cap<256 翻倍,cap≥256 按 1.25+192);擴容后底層數組更換。
func main() {// 場景1:append未擴容(len+新增元素數 ≤ cap)s1 := make([]int, 2, 5) // len=2, cap=5,底層數組[0,0,_,_,_](_表示未使用)fmt.Printf("s1初始: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", s1, len(s1), cap(s1), &s1[0])// 輸出:s1初始: 值=[0 0], len=2, cap=5, 底層數組地址=0xc0000140d8// append 2個元素(2+2=4 ≤ cap=5,未擴容)s1 = append(s1, 3, 4)fmt.Printf("s1 append后: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", s1, len(s1), cap(s1), &s1[0])// 輸出:s1 append后: 值=[0 0 3 4], len=4, cap=5, 底層數組地址=0xc0000140d8(地址不變)// 場景2:append擴容(len+新增元素數 > cap)→ 1.18+擴容策略驗證// 子場景2.1:cap<256 → 新cap=原cap*2s2 := make([]int, 3, 3) // 原cap=3 <256,新增2個元素(3+2=5>3)fmt.Printf("\ns2初始: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", s2, len(s2), cap(s2), &s2[0])// 輸出:s2初始: 值=[0 0 0], len=3, cap=3, 底層數組地址=0xc0000140f0s2 = append(s2, 100, 200) // 擴容:新cap=3*2=6fmt.Printf("s2 append后: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", s2, len(s2), cap(s2), &s2[0])// 輸出:s2 append后: 值=[0 0 0 100 200], len=5, cap=6, 底層數組地址=0xc00000e3f0(地址變了)// 子場景2.2:cap≥256 → 新cap=原cap*1.25 + 192(驗證)// 先創建一個cap=256的切片s3 := make([]int, 0, 256)for i := 0; i < 256; i++ {s3 = append(s3, i) // 填充256個元素,len=256, cap=256}fmt.Printf("\ns3初始: len=%d, cap=%d, 底層數組地址=%p\n", len(s3), cap(s3), &s3[0])// 輸出:s3初始: len=256, cap=256, 底層數組地址=0xc00008a000// append 1個元素(256+1=257>256,擴容)s3 = append(s3, 256)// 計算新cap:256*1.25 + 192 = 320 + 192 = 512?不,Golang中是整數計算:256 * 5 /4 + 192 = 320 + 192 = 512fmt.Printf("s3 append后: len=%d, cap=%d, 底層數組地址=%p\n", len(s3), cap(s3), &s3[0])// 輸出:s3 append后: len=257, cap=512, 底層數組地址=0xc0000c8000(地址變了,cap=512)// 場景3:append多個元素,多次擴容s4 := []int{1} // len=1, cap=1fmt.Printf("\ns4擴容過程:\n")for i := 2; i <= 10; i++ {s4 = append(s4, i)fmt.Printf(" 追加%d后: len=%d, cap=%d, 底層數組地址=%p\n", i, len(s4), cap(s4), &s4[0])}// 運行結果(cap<256,每次擴容翻倍):// s4擴容過程:// 追加2后: len=2, cap=2, 底層數組地址=0xc000014130(原cap=1→2)// 追加3后: len=3, cap=4, 底層數組地址=0xc000014140(原cap=2→4)// 追加4后: len=4, cap=4, 底層數組地址=0xc000014140(未擴容)// 追加5后: len=5, cap=8, 底層數組地址=0xc000014160(原cap=4→8)// 追加6后: len=6, cap=8, 底層數組地址=0xc000014160(未擴容)// 追加7后: len=7, cap=8, 底層數組地址=0xc000014160(未擴容)// 追加8后: len=8, cap=8, 底層數組地址=0xc000014160(未擴容)// 追加9后: len=9, cap=16, 底層數組地址=0xc0000141a0(原cap=8→16)// 追加10后: len=10, cap=16, 底層數組地址=0xc0000141a0(未擴容)
}
2.5?切片實戰:數組相鄰元素求和
從數組派生切片的思路;切片長度計算(相鄰和個數 = 數組長度 - 1);循環賦值的細節。
func main() {// 需求:有數組[1,4,9,16,2,5,10,15],生成新切片,元素是數組相鄰2項的和// 步驟1:定義原數組(固定長度8)arr := [8]int{1, 4, 9, 16, 2, 5, 10, 15}fmt.Printf("原數組: 值=%v, 長度=%d, 類型=%T\n", arr, len(arr), arr)// 輸出:原數組: 值=[1 4 9 16 2 5 10 15], 長度=8, 類型=[8]int// 步驟2:計算結果切片的長度(關鍵邏輯)// 相鄰2項和的個數 = 數組元素個數 - 1(8個元素→7個和)resultLen := len(arr) - 1 fmt.Printf("結果切片長度: %d\n", resultLen) // 輸出:7// 步驟3:創建結果切片(用make,指定len=resultLen,cap默認=resultLen)result := make([]int, resultLen) fmt.Printf("初始化結果切片: 值=%v, len=%d, cap=%d\n", result, len(result), cap(result))// 輸出:初始化結果切片: 值=[0 0 0 0 0 0 0], len=7, cap=7(零值切片)// 步驟4:循環計算相鄰和(i從0到resultLen-1,共7次)for i := 0; i < resultLen; i++ {// 邏輯:第i個和 = 數組第i個元素 + 數組第i+1個元素sum := arr[i] + arr[i+1]result[i] = sum // 給結果切片的第i個元素賦值(覆蓋零值)fmt.Printf("第%d次循環: arr[%d]+arr[%d]=%d+%d=%d → result[%d]=%d\n", i+1, i, i+1, arr[i], arr[i+1], sum, i, sum)}// 循環運行詳情:// 第1次循環: arr[0]+arr[1]=1+4=5 → result[0]=5// 第2次循環: arr[1]+arr[2]=4+9=13 → result[1]=13// 第3次循環: arr[2]+arr[3]=9+16=25 → result[2]=25// 第4次循環: arr[3]+arr[4]=16+2=18 → result[3]=18// 第5次循環: arr[4]+arr[5]=2+5=7 → result[4]=7// 第6次循環: arr[5]+arr[6]=5+10=15 → result[5]=15// 第7次循環: arr[6]+arr[7]=10+15=25 → result[6]=25// 步驟5:輸出最終結果fmt.Printf("\n最終結果:相鄰兩項和的新切片: %v\n", result)// 輸出:最終結果:相鄰兩項和的新切片: [5 13 25 18 7 15 25]
}
三、子切片
子切片是從數組或切片中 “切取一部分” 得到的新切片,核心特點:不擴容時共享底層數組,就像給原數組 / 切片開了個 “局部窗口”。
3.1 子切片的語法:slice[start:end]
(前包后不包)
語法規則很簡單:
start
:切取的起始索引(默認 0,必須 ≤ end)。end
:切取的結束索引(默認len(原切片)
,必須 ≤cap(原切片)
)。結果切片的 len = end - start。
結果切片的 cap = cap(原切片) - start(底層數組從 start 到末尾的長度)。
func main() {// 基礎切片:len=6, cap=6,底層數組[1,2,3,4,5,6](索引0-5)base := []int{1, 2, 3, 4, 5, 6}fmt.Printf("基礎切片base: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", base, len(base), cap(base), &base[0])// 輸出:基礎切片base: 值=[1 2 3 4 5 6], len=6, cap=6, 底層數組地址=0xc0000140d8// 場景1:start缺省(=0),end=3 → base[0:3]sub1 := base[:3] // len=3-0=3,cap=6-0=6(start=0,cap=原cap)fmt.Printf("\nsub1=base[:3]: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sub1, len(sub1), cap(sub1), &sub1[0])// 輸出:sub1=base[:3]: 值=[1 2 3], len=3, cap=6, 底層數組地址=0xc0000140d8(共享)// 場景2:end缺省(=len(base)=6),start=2 → base[2:6]sub2 := base[2:] // len=6-2=4,cap=6-2=4(start=2,cap=原cap-2)fmt.Printf("sub2=base[2:]: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sub2, len(sub2), cap(sub2), &sub2[0])// 輸出:sub2=base[2:]: 值=[3 4 5 6], len=4, cap=4, 底層數組地址=0xc0000140f0(base[2]地址)// 場景3:start=2, end=5 → base[2:5]sub3 := base[2:5] // len=5-2=3,cap=6-2=4(end=5≤cap=6,合法)fmt.Printf("sub3=base[2:5]: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sub3, len(sub3), cap(sub3), &sub3[0])// 輸出:sub3=base[2:5]: 值=[3 4 5], len=3, cap=4, 底層數組地址=0xc0000140f0(共享)// 場景4:start=end=3 → base[3:3](空切片)sub4 := base[3:3] // len=3-3=0,cap=6-3=3(空切片,但有cap,可append)fmt.Printf("sub4=base[3:3]: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sub4, len(sub4), cap(sub4), &sub4[0]) // 注意:空切片&sub4[0]會panic嗎?// 運行錯誤:panic: runtime error: index out of range [0] with length 0(len=0,不能訪問[0])// 修正:空切片的底層數組地址需通過append后驗證,或用reflect包(新手暫不涉及)fmt.Printf("sub4=base[3:3]: 值=%v, len=%d, cap=%d\n", sub4, len(sub4), cap(sub4))// 輸出:sub4=base[3:3]: 值=[], len=0, cap=3// 場景5:end=cap(base)=6,start=4 → base[4:6]sub5 := base[4:6] // len=6-4=2,cap=6-4=2(end=cap,合法)fmt.Printf("sub5=base[4:6]: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sub5, len(sub5), cap(sub5), &sub5[0])// 輸出:sub5=base[4:6]: 值=[5 6], len=2, cap=2, 底層數組地址=0xc000014100(base[4]地址)// 場景6:start=cap(base)=6,end=6 → base[6:6](空切片,cap=0)sub6 := base[6:6] // len=6-6=0,cap=6-6=0(cap=0,append會直接擴容)fmt.Printf("sub6=base[6:6]: 值=%v, len=%d, cap=%d\n", sub6, len(sub6), cap(sub6))// 輸出:sub6=base[6:6]: 值=[], len=0, cap=0// 場景7:end>cap(base) → 錯誤(end不能超過cap)// sub7 := base[2:7] // 編譯通過,但運行錯誤:runtime error: slice bounds out of range [:7] with capacity 6// 場景8:start>end → 錯誤// sub8 := base[4:2] // 編譯錯誤:invalid slice bound: 4 > 2
}
3.2 子切片與原切片:共享底層數組(未擴容)vs 獨立(擴容)
未擴容時,修改子切片→影響原切片;修改原切片→影響子切片;擴容后,兩者底層數組獨立,修改互不影響。
func main() {// 原切片:len=6, cap=6,底層數組[1,2,3,4,5,6]origin := []int{1, 2, 3, 4, 5, 6}fmt.Printf("初始原切片origin: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", origin, len(origin), cap(origin), &origin[0])// 輸出:初始原切片origin: 值=[1 2 3 4 5 6], len=6, cap=6, 底層數組地址=0xc0000140d8// 1. 未擴容:子切片與原切片共享底層數組sub := origin[1:4] // len=3, cap=5,底層數組=origin的底層數組(地址相同)fmt.Printf("未擴容子切片sub: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sub, len(sub), cap(sub), &sub[0])// 輸出:未擴容子切片sub: 值=[2 3 4], len=3, cap=5, 底層數組地址=0xc0000140e0(origin[1]地址)// 案例1:修改子切片sub的元素 → 原切片origin對應位置變化sub[0] = 200 // sub[0]對應origin[1]fmt.Printf("\n修改sub[0]=200后:\n")fmt.Printf("sub: 值=%v\n", sub) // 輸出:sub: 值=[200 3 4]fmt.Printf("origin: 值=%v\n", origin) // 輸出:origin: 值=[1 200 3 4 5 6](origin[1]變了)// 案例2:修改原切片origin的元素 → 子切片sub對應位置變化origin[3] = 400 // origin[3]對應sub[2]fmt.Printf("\n修改origin[3]=400后:\n")fmt.Printf("origin: 值=%v\n", origin) // 輸出:origin: 值=[1 200 3 400 5 6]fmt.Printf("sub: 值=%v\n", sub) // 輸出:sub: 值=[200 3 400](sub[2]變了)// 2. 子切片擴容:底層數組更換,與原切片獨立// sub當前len=3, cap=5,append 3個元素(3+3=6>5 → 擴容)sub = append(sub, 500, 600, 700)fmt.Printf("\nsub append后(擴容):\n")fmt.Printf("sub: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sub, len(sub), cap(sub), &sub[0])// 輸出:sub: 值=[200 3 400 500 600 700], len=6, cap=10, 底層數組地址=0xc00000e3f0(新地址)fmt.Printf("origin: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", origin, len(origin), cap(origin), &origin[0])// 輸出:origin: 值=[1 200 3 400 5 6], len=6, cap=6, 底層數組地址=0xc0000140d8(老地址)// 案例3:擴容后修改sub → 不影響originsub[0] = 2000fmt.Printf("\n擴容后修改sub[0]=2000:\n")fmt.Printf("sub: 值=%v\n", sub) // 輸出:sub: 值=[2000 3 400 500 600 700]fmt.Printf("origin: 值=%v\n", origin) // 輸出:origin: 值=[1 200 3 400 5 6](不變)// 案例4:擴容后修改origin → 不影響suborigin[1] = 20fmt.Printf("\n擴容后修改origin[1]=20:\n")fmt.Printf("origin: 值=%v\n", origin) // 輸出:origin: 值=[1 20 3 400 5 6]fmt.Printf("sub: 值=%v\n", sub) // 輸出:sub: 值=[2000 3 400 500 600 700](不變)
}
3.3 從數組派生子切片:修改切片影響數組
數組派生切片后,切片的底層數組 = 原數組;修改切片元素→原數組對應元素變化;數組長度固定,切片 append 擴容后與數組獨立。
func main() {// 原數組:len=6, cap=6,內存固定arr := [6]int{1, 2, 3, 4, 5, 6}fmt.Printf("初始數組arr: 值=%v, 類型=%T, 數組地址=%p, arr[1]地址=%p\n", arr, arr, &arr, &arr[1])// 輸出:初始數組arr: 值=[1 2 3 4 5 6], 類型=[6]int, 數組地址=0xc0000a8000, arr[1]地址=0xc0000a8008// 1. 從數組派生切片:slice=arr[start:end]sliceFromArr := arr[1:4] // 切取arr[1]、arr[2]、arr[3],len=3, cap=6-1=5fmt.Printf("從數組派生的切片sliceFromArr: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sliceFromArr, len(sliceFromArr), cap(sliceFromArr), &sliceFromArr[0])// 輸出:sliceFromArr: 值=[2 3 4], len=3, cap=5, 底層數組地址=0xc0000a8008(和arr[1]地址相同)// 案例1:修改切片元素 → 原數組變化sliceFromArr[0] = 200 // 切片[0]對應arr[1]fmt.Printf("\n修改sliceFromArr[0]=200后:\n")fmt.Printf("sliceFromArr: 值=%v\n", sliceFromArr) // 輸出:[200 3 4]fmt.Printf("arr: 值=%v\n", arr) // 輸出:[1 200 3 4 5 6](arr[1]變了)// 案例2:修改原數組元素 → 切片變化arr[3] = 400 // arr[3]對應切片[2]fmt.Printf("\n修改arr[3]=400后:\n")fmt.Printf("arr: 值=%v\n", arr) // 輸出:[1 200 3 400 5 6]fmt.Printf("sliceFromArr: 值=%v\n", sliceFromArr) // 輸出:[200 3 400](切片[2]變了)// 2. 切片append擴容:與原數組獨立// 切片當前len=3, cap=5,append 3個元素(3+3=6>5 → 擴容)sliceFromArr = append(sliceFromArr, 500, 600, 700)fmt.Printf("\nsliceFromArr append后(擴容):\n")fmt.Printf("sliceFromArr: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", sliceFromArr, len(sliceFromArr), cap(sliceFromArr), &sliceFromArr[0])// 輸出:sliceFromArr: 值=[200 3 400 500 600 700], len=6, cap=10, 底層數組地址=0xc00000e3f0(新地址)fmt.Printf("arr: 值=%v, 數組地址=%p\n", arr, &arr) // 輸出:arr: 值=[1 200 3 400 5 6], 數組地址=0xc0000a8000(老地址,數組不變)// 案例3:擴容后修改切片 → 不影響數組sliceFromArr[0] = 2000fmt.Printf("\n擴容后修改sliceFromArr[0]=2000:\n")fmt.Printf("sliceFromArr: 值=%v\n", sliceFromArr) // 輸出:[2000 3 400 500 600 700]fmt.Printf("arr: 值=%v\n", arr) // 輸出:[1 200 3 400 5 6](不變)
}
3.4 子切片實用技巧:避免共享影響(make+copy)
當需要子切片但不想影響原切片時,用make創建新切片,copy拷貝元素,實現底層數組獨立。
func main() {// 原切片:len=5, cap=5,底層數組[10,20,30,40,50]origin := []int{10, 20, 30, 40, 50}fmt.Printf("原切片origin: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", origin, len(origin), cap(origin), &origin[0])// 輸出:origin: 值=[10 20 30 40 50], len=5, cap=5, 底層數組地址=0xc0000140d8// 需求:取origin[1:4]的子切片,但修改子切片不影響origin// 方法1:直接切取(共享底層數組,會影響)subShare := origin[1:4]subShare[0] = 200fmt.Printf("\n直接切取subShare修改后:\n")fmt.Printf("subShare: 值=%v\n", subShare) // 輸出:[200 30 40]fmt.Printf("origin: 值=%v\n", origin) // 輸出:[10 200 30 40 50](被影響)// 恢復origin的值origin[1] = 20fmt.Printf("\n恢復origin后:%v\n", origin) // 輸出:[10 20 30 40 50]// 方法2:make+copy(獨立底層數組,不影響)// 步驟1:切取子切片(臨時,用于獲取元素)subTemp := origin[1:4] // 步驟2:創建新切片,len和cap與subTemp相同(或自定義)subIndependent := make([]int, len(subTemp), cap(subTemp)) // 步驟3:copy元素(copy(dst, src),返回拷貝的元素個數)copyCount := copy(subIndependent, subTemp) fmt.Printf("\nmake+copy創建的subIndependent:\n")fmt.Printf("拷貝元素個數: %d\n", copyCount) // 輸出:3(subTemp有3個元素)fmt.Printf("subIndependent: 值=%v, len=%d, cap=%d, 底層數組地址=%p\n", subIndependent, len(subIndependent), cap(subIndependent), &subIndependent[0])// 輸出:subIndependent: 值=[20 30 40], len=3, cap=4, 底層數組地址=0xc0000140f0(新地址,和origin不同)// 步驟4:修改subIndependent,驗證不影響originsubIndependent[0] = 2000fmt.Printf("\n修改subIndependent[0]=2000后:\n")fmt.Printf("subIndependent: 值=%v\n", subIndependent) // 輸出:[2000 30 40]fmt.Printf("origin: 值=%v\n", origin) // 輸出:[10 20 30 40 50](未被影響)// 關鍵:copy的細節(拷貝長度取dst和src的較小值)dstShort := make([]int, 2) // dst len=2srcLong := []int{1,2,3,4} // src len=4copyCount2 := copy(dstShort, srcLong)fmt.Printf("\ncopy(dstShort, srcLong):\n")fmt.Printf("拷貝元素個數: %d\n", copyCount2) // 輸出:2(取dst和src的較小len)fmt.Printf("dstShort: 值=%v\n", dstShort) // 輸出:[1 2](只拷貝前2個元素)
}
四、全知識點對照表
代碼示例場景 | 對應核心知識點 | 易錯點提醒 |
數組[3]int vs [5]int | 數組長度是類型的一部分,不同長度是不同類型 | 變量不能作為數組長度(必須編譯時確定) |
數組賦值a1 := a0 | 數組值傳遞,拷貝整個數組,修改互不影響 | 大數組賦值浪費內存(推薦用切片) |
切片make([]int,1,2) | 切片 header 含 array 指針 + len+cap,cap≥len | len 是可用元素數,cap 是底層數組容量 |
切片傳參showAddr(a0) | 切片引用傳遞,拷貝 header(24 字節),共享底層數組 | header 地址不同,但 array 指針可能相同 |
切片 append 擴容 | 1.18 + 策略:cap<256 翻倍,cap≥256 按 1.25+192 | 擴容后底層數組更換,原切片不變 |
子切片base[1:4] | len=4-1=3,cap = 原 cap-1,共享底層數組(未擴容) | end 不能超過 cap,start 不能大于 end |
子切片 append 擴容 | 擴容后底層數組獨立,與原切片 / 數組互不影響 | 空切片(start=end)也有 cap,可 append |
切片copy(new, sub) | copy 拷貝元素,新切片有獨立底層數組,避免共享影響 | copy 長度取 dst 和 src 的較小值 |
string 數組元素地址間隔 16 字節 | string 是結構體(指針 8 + 長度 8),數組存結構體 | 字符串本身存在其他內存,數組存的是引用 |
數組越界a0[3] | 數組長度固定,索引不能超過 len-1,運行時 panic | 用len(a)-1訪問最后一個元素,避免硬編碼 |