Golang語言之數組、切片與子切片

一、數組

先記住數組的核心特點:盒子大小一旦定了就改不了(長度固定),但盒子里的東西能換(元素值可變)。就像你買了個能裝 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訪問最后一個元素,避免硬編碼

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/96757.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/96757.shtml
英文地址,請注明出處:http://en.pswp.cn/web/96757.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

速通ACM省銅第四天 賦源碼(G-C-D, Unlucky!)

目錄 引言&#xff1a; G-C-D, Unlucky! 題意分析 邏輯梳理 代碼實現 結語&#xff1a; 引言&#xff1a; 因為今天打了個ICPC網絡賽&#xff0c;導致坐牢了一下午&#xff0c;沒什么時間打題目了&#xff0c;就打了一道題&#xff0c;所以&#xff0c;今天我們就只講一題了&…

數據鏈路層總結

目錄 &#xff08;一&#xff09;以太網&#xff08;IEEE 802.3&#xff09; &#xff08;1&#xff09;以太網的幀格式 &#xff08;2&#xff09;幀協議類型字段 ①ARP協議 &#xff08;橫跨網絡層和數據鏈路層的協議&#xff09; ②RARP協議 &#xff08;二&#xff…

Scala 新手實戰三案例:從循環到條件,搞定基礎編程場景

Scala 新手實戰三案例&#xff1a;從循環到條件&#xff0c;搞定基礎編程場景 對 Scala 新手來說&#xff0c;單純記語法容易 “學完就忘”&#xff0c;而通過小而精的實戰案例鞏固知識點&#xff0c;是掌握語言的關鍵。本文精選三個高頻基礎場景 ——9 乘 9 乘法口訣表、成績等…

java學習筆記----標識符與變量

1.什么是標識符?Java中變量、方法、類等要素命名時使用的字符序列&#xff0c;稱為標識符。 技巧:凡是自己可以起名字的地方都叫標識符。 比如:類名、方法名、變量名、包名、常量名等 2.標識符的命名規則由26個英文字母大小寫&#xff0c;0-9&#xff0c;或$組成 數字不可以開…

AI產品經理面試寶典第93天:Embedding技術選型與場景化應用指南

1. Embedding技術演進全景解析 1.1 稀疏向量:關鍵詞匹配的基石 1.1.1 問:請說明稀疏向量的適用場景及技術特點 答:稀疏向量適用于關鍵詞精確匹配場景,典型實現包括TF-IDF、BM25和SPLADE。其技術特征表現為50,000+高維向量且95%以上位置為零值,通過余弦或點積計算相似度…

【Mermaid.js】從入門到精通:完美處理節點中的空格、括號和特殊字符

文章標簽&#xff1a; Mermaid, Markdown, 前端開發, 數據可視化, 流程圖 文章摘要&#xff1a; 你是否在使用 Mermaid.js 繪制流程圖時&#xff0c;僅僅因為節點文本里加了一個空格或括號&#xff0c;整個圖就渲染失敗了&#xff1f;別擔心&#xff0c;這幾乎是每個 Mermaid 新…

多技術融合提升環境生態水文、土地土壤、農業大氣等領域的數據分析與項目科研水平

一&#xff1a;空間數據獲取與制圖1.1 軟件安裝與應用1.2 空間數據介紹1.3海量空間數據下載1.4 ArcGIS軟件快速入門1.5 Geodatabase地理數據庫二&#xff1a;ArcGIS專題地圖制作2.1專題地圖制作規范2.2 空間數據的準備與處理2.3 空間數據可視化&#xff1a;地圖符號與注記2.4 研…

【音視頻】Android NDK 與.so庫適配

一、名詞解析 名詞全稱核心說明Android NDKNative Development Kit在SDK基礎上增加“原生”開發能力&#xff0c;支持使用C/C編寫代碼&#xff0c;用于開發需要調用底層能力的模塊&#xff08;如音視頻、加密算法等&#xff09;.so庫Shared Object即共享庫&#xff0c;由NDK編…

SpringBoot 輕量級一站式日志可視化與JVM監控

一、項目初衷Java 應用開發的同學都知道&#xff0c;項目上線后&#xff0c;日志的可視化查詢與 JVM 的可視化監控是一件非常重要的事。 市面上成熟方案一般是采用 ELK/EFK 實現日志可視化&#xff0c;采用 Actuator Prometheus Grafana 實現 JVM 監控。 這兩套都是非常優秀的…

【Leetcode hot 100】101.對稱二叉樹

問題鏈接 101.對稱二叉樹 問題描述 給你一個二叉樹的根節點 root &#xff0c; 檢查它是否軸對稱。 示例 1&#xff1a; 輸入&#xff1a;root [1,2,2,3,4,4,3] 輸出&#xff1a;true 示例 2&#xff1a; 輸入&#xff1a;root [1,2,2,null,3,null,3] 輸出&#xff1a;…

Zynq開發實踐(FPGA之選擇開發板)

【 聲明&#xff1a;版權所有&#xff0c;歡迎轉載&#xff0c;請勿用于商業用途。 聯系信箱&#xff1a;feixiaoxing 163.com】我們之所以選用zynq開發板&#xff0c;就在于它支持arm軟件開發&#xff0c;也支持fpga開發&#xff0c;甚至可以運行linux&#xff0c;這是之前沒有…

Flutter Riverpod 3.0 發布,大規模重構下的全新狀態管理框架

在之前的 《注解模式下的 Riverpod 有什么特別之處》我們聊過 Riverpod 2.x 的設計和使用原理&#xff0c;同時當時我們就聊到作者已經在開始探索 3.0 的重構方式&#xff0c;而現在隨著 Riverpod 3.0 的發布&#xff0c;riverpod 帶來了許多細節性的變化。 當然&#xff0c;這…

Xcode 上傳 ipa 全流程詳解 App Store 上架流程、uni-app 生成 ipa 文件上傳與審核指南

對于 iOS 開發者而言&#xff0c;應用開發完成后最重要的一步就是將應用打包為 ipa 文件&#xff0c;并上傳至 App Store Connect 進行分發或上架。 其中&#xff0c;Xcode 上傳 ipa 是最常見的方法&#xff0c;但很多開發者在實際操作中常常遇到卡住、上傳失敗或簽名錯誤等問題…

快速選中對象

圖片要求 圖片背景單純&#xff0c;對象邊緣比較清晰 對象選擇工具 選擇對象選擇工具后&#xff0c;畫出大致區域&#xff0c;系統將自動分析圖片內容&#xff0c;從而實現快速選擇圖片中的一個惑多個對象他有兩種模式&#xff0c;分別是舉行與套索模式。使用時可以先選中對象的…

點到點鏈路上的OSPF動態路由(2025年9月10日)

一、前言前面我們已經分享過了靜態路由、缺省路由、浮動靜態路由這些靜態路由的配置。接下來將會 陸陸續續開始分享動態路由以及其他路由配置。博主這里是一個新人&#xff0c;了解這些路由配置不是自上而下的&#xff0c;而是自下而上的&#xff0c;也就是說通過實驗去理解原理…

技術視界 | 末端執行器:機器人的“手”,如何賦予機器以生命?

在現代自動化系統中&#xff0c;末端執行器&#xff08;End Effector&#xff09;作為機器人與物理世界交互的“手”&#xff0c;發揮著至關重要的作用。它直接安裝在機械臂末端&#xff0c;不僅是機器人實現“抓取、感知和操作”三大核心功能的關鍵部件&#xff0c;更是整個自…

滑動窗口概述

滑動窗口算法簡介滑動窗口是一種用于處理數組或字符串子區間問題的高效算法。它通過維護一個動態窗口&#xff08;通常由兩個指針表示&#xff09;來避免重復計算&#xff0c;將時間復雜度從O(n)優化到O(n)。基本實現步驟初始化窗口指針&#xff1a;通常使用left和right指針表示…

AI 創建學生管理系統

使用騰訊元寶創建&#xff0c;整體效果不錯。修正2個bug跑起來&#xff0c;達到了需要的功能先上效果圖&#xff1a;按鈕分類別配色&#xff0c;界面清爽。喜歡這布局創建過程&#xff1a;prompt: 使用最新穩定vue版&#xff0c;使用pinia存儲&#xff0c;基于typescript, 樣式…

ASP.NET Core 中的簡單授權

ASP.NET Core 中的授權通過 [Authorize] 屬性及其各種參數控制。 在其最基本的形式中&#xff0c;通過向控制器、操作或 [Authorize] Page 應用 Razor 屬性&#xff0c;可限制為僅允許經過身份驗證的用戶訪問該組件。 使用 [Authorize] 屬性 以下代碼限制為僅允許經過身份驗證…

leetcode 493 翻轉對

一、題目描述 二、解題思路 本題的思路與逆序數的思路相似&#xff0c;采用歸并排序的思路來實現。leetcode LCR 170.交易逆序對的總數-CSDN博客 注意&#xff1a;但是逆序數的ret更新在左、右區間合并時更新&#xff0c;但本題ret更新在左、右區間合并前更新。 三、代碼實現…