第一部分结构体Struct1.1 什么是结构体结构体是 Go 语言中用来组合多个不同类型字段的复合数据类型。你可以把它理解为一个模板或蓝图用来描述一个事物的多个属性。package main import fmt // 定义一个结构体类型 type Vertex struct { X int Y int } func main() { v : Vertex{1, 2} fmt.Println(v) // {1 2} }底层逻辑结构体在内存中是连续分配的字段按照声明顺序依次排列每个字段占据固定大小的内存空间结构体是值类型赋值时会复制整个结构体的数据关键概念type关键字定义新类型struct关键字声明这是一个结构体大写字母开头的字段名如X、Y是导出的可被其他包访问小写字母开头的字段名只能在当前包内访问1.2 结构体字段访问通过点号.来访问结构体的字段可以读取也可以修改。package main import fmt type Vertex struct { X int Y int } func main() { v : Vertex{1, 2} v.X 4 // 修改 X 字段为 4 fmt.Println(v.X) // 输出: 4 fmt.Println(v.Y) // 输出: 2 }底层逻辑点号.运算符在编译时会计算字段相对于结构体起始地址的偏移量访问v.X时Go 编译器会生成类似*(base_address offset_of_X)的机器码结构体字段在内存中的布局与声明顺序一致1.3 指向结构体的指针你可以使用指针来引用结构体。指针存储的是结构体在内存中的地址而不是结构体的副本。package main import fmt type Vertex struct { X int Y int } func main() { v : Vertex{1, 2} p : v // p 是指向 v 的指针 p.X 1e9 // 通过指针修改字段Go 会自动解引用 fmt.Println(v) // 输出: {1000000000 2} }底层逻辑v取结构体 v 的内存地址p.X实际上是(*p).X的语法糖Go 自动帮你解引用通过指针修改字段时修改的是原始结构体不是副本传递指针给函数时函数内修改会影响原始结构体避免大结构体复制的性能开销何时使用指针需要在函数中修改原始结构体时结构体很大复制成本高时需要共享同一个结构体实例时1.4 结构体字面值Struct Literals创建结构体时可以用字面值语法有两种写法package main import fmt type Vertex struct { X, Y int } var ( // 写法一按字段顺序列出值必须按声明顺序 v1 Vertex{1, 2} // 类型是 Vertex // 写法二用字段名指定值推荐更清晰顺序可以打乱 v2 Vertex{X: 1} // Y 默认为零值 0 // 写法三完全省略字段名按顺序不推荐用于字段多的情况 v3 Vertex{} // X:0, Y:0全是零值 // 写法四指针类型 p Vertex{1, 2} // 类型是 *Vertex ) func main() { fmt.Println(v1, v2, v3, p) }关键规则如果用了字段名: 值的语法字段顺序可以随意未指定的字段自动为零值如果不用字段名必须严格按照声明顺序填写所有字段Vertex{1, 2}返回的是指向结构体的指针零值规则int→0, float→0.0, string→, bool→false, 指针→nil第二部分切片Slice2.1 什么是切片切片是对数组的一个连续片段的引用它是 Go 语言中最常用的数据结构之一。package main import fmt func main() { // 先创建一个数组 primes : [6]int{2, 3, 5, 7, 11, 13} // 从数组创建一个切片取索引 1 到 3不含 4 var s []int primes[1:4] fmt.Println(s) // [3 5 7] }底层逻辑重点切片在底层由三个部分组成slice headerstruct SliceHeader { Data uintptr // 指向底层数组的指针 Len int // 切片的长度当前有多少个元素 Cap int // 切片的容量从起始位置到底层数组末尾的元素个数 }切片本身不存储数据它只是对底层数组的窗口或视图primes[1:4]表示从索引 1 开始到索引 4 结束不含所以取的是primes[1]、primes[2]、primes[3]切片的下界可以省略默认为 0上界也可以省略默认为数组长度2.2 切片就像数组的引用切片不拥有数据它引用的是底层数组。修改切片的元素会修改底层数组反之亦然。package main import fmt func main() { names : [4]string{ John, Paul, George, Ringo, } fmt.Println(names) a : names[0:2] // [John Paul] b : names[1:3] // [Paul George] fmt.Println(a, b) // 修改 b[0]实际上修改的是底层数组的 names[1] b[0] XXX fmt.Println(a, b) fmt.Println(names) // [John XXX George Ringo] —— a 也受到了影响 }底层逻辑a和b共享同一个底层数组a的 Data 指针指向names[0]b的 Data 指针指向names[1]修改b[0]就是修改底层数组的names[1]a也能看到这个变化这就是为什么切片叫引用——多个切片可以指向同一段内存2.3 切片字面值切片字面值的语法和数组类似但不需要指定长度。package main import fmt func main() { // 切片字面值不需要指定长度 q : []int{2, 3, 5, 7, 11, 13} fmt.Println(q) // 修改元素 q[0] 100 fmt.Println(q) // [100 3 5 7 11 13] }与数组的区别数组[6]int{...}—— 方括号里有数字长度是类型的一部分切片[]int{...}—— 方括号里没有数字长度是动态的2.4 切片的默认行为切片时省略边界Go 会使用合理的默认值。package main import fmt func main() { s : []int{2, 3, 5, 7, 11, 13} printSlice(s) // len6 cap6 [2 3 5 7 11 13] // 省略下界默认为 0 s s[:0] printSlice(s) // len0 cap6 [] // 省略上界默认为切片的长度 s s[:4] printSlice(s) // len4 cap6 [2 3 5 7] // 两个都省略等价于 s[0:len(s)] s s[2:] printSlice(s) // len2 cap4 [5 7] } func printSlice(s []int) { fmt.Printf(len%d cap%d %v\n, len(s), cap(s), s) }默认值规则总结写法等价于说明s[:n]s[0:n]从头取 n 个元素s[n:]s[n:len(s)]从 n 取到末尾s[:]s[0:len(s)]取全部元素2.5 切片的长度与容量长度len切片当前包含的元素个数。容量cap从切片的第一个元素开始到底层数组末尾的元素个数。package main import fmt func main() { s : []int{2, 3, 5, 7, 11, 13} printSlice(s) // len6 cap6 [2 3 5 7 11 13] // 把长度设为 0但容量不变 s s[:0] printSlice(s) // len0 cap6 [] // 扩展长度到 4容量仍然足够 s s[:4] printSlice(s) // len4 cap6 [2 3 5 7] // 丢弃前两个元素长度和容量都减少 s s[2:] printSlice(s) // len2 cap4 [5 7] } func printSlice(s []int) { fmt.Printf(len%d cap%d %v\n, len(s), cap(s), s) }底层逻辑图解初始: s []int{2, 3, 5, 7, 11, 13} 底层数组: [2, 3, 5, 7, 11, 13] ↑ ↑ | | Data指向 数组末尾 len6, cap6 s s[:0]: len0, cap6容量不变只是窗口缩小了 s s[:4]: len4, cap6扩展窗口但不能超过容量 s s[2:]: len2, cap4Data指针右移2位容量从新起点算起关键规则切片不能超过其容量否则会 paniclen(s) cap(s)永远成立重新切片可以延长长度前提是容量足够2.6 零切片Nil Slice切片的零值是nil。nil 切片没有底层数组长度和容量都为 0。package main import fmt func main() { var s []int // 没有初始化s 是 nil fmt.Println(s, len(s), cap(s)) // [] 0 0 if s nil { fmt.Println(nil!) // 会执行到这里 } }注意事项var s []int声明但未初始化 → s 为 nils : []int{}或s : make([]int, 0)→ s 不是 nil只是长度为 0nil 切片和空切片的区别nil 切片没有底层数组JSON 序列化时为null空切片有底层数组长度为 0JSON 序列化时为[]对 nil 切片使用append是安全的会自动分配底层数组2.7 用 make 创建切片make是 Go 的内置函数用来创建动态大小的切片。它会在底层分配一个数组并返回指向它的切片。package main import fmt func main() { // make([]类型, 长度) —— 长度容量 a : make([]int, 5) printSlice(a, a) // a len5 cap5 [0 0 0 0 0] // make([]类型, 长度, 容量) —— 长度和容量不同 b : make([]int, 0, 5) printSlice(b, b) // b len0 cap5 [] // 扩展 b 的长度到容量 b b[:cap(b)] printSlice(b, b) // b len5 cap5 [0 0 0 0 0] // 丢弃第一个元素 b b[1:] printSlice(b, b) // b len4 cap4 [0 0 0 0] } func printSlice(name string, x []int) { fmt.Printf(%s len%d cap%d %v\n, name, len(x), cap(x), x) }关键要点make([]int, 5)→ len5, cap5元素全部为零值make([]int, 0, 5)→ len0, cap5适合后续用 append 添加元素make分配的是归零的数组所有元素初始值为类型的零值第三个参数容量是可选的不传则容量长度2.8 切片的切片多维切片切片可以包含任何类型包括其他切片。这就形成了多维切片。package main import ( fmt strings ) func main() { // 创建一个 3x3 的井字棋盘二维切片 board : [][]string{ []string{_, _, _}, []string{_, _, _}, []string{_, _, _}, } // 玩家轮流下棋 board[0][0] X board[2][2] O board[1][2] X board[1][0] O board[0][2] X // 打印棋盘 for i : 0; i len(board); i { fmt.Printf(%s\n, strings.Join(board[i], )) } }输出X _ X O _ X _ _ O底层逻辑[][]string是一个切片的切片外层切片的每个元素都是一个[]string每一行内层切片可以有不同的长度这就是不规则多维数组访问board[i][j]时先取外层切片的第 i 个元素一个切片再取这个切片的第 j 个元素2.9 追加元素到切片appendGo 提供了内置函数append来向切片添加元素。package main import fmt func main() { var s []int // nil 切片 printSlice(s) // len0 cap0 [] // append 可以作用于 nil 切片 s append(s, 0) printSlice(s) // len1 cap1 [0] // 切片会根据需要自动增长 s append(s, 1) printSlice(s) // len2 cap2 [0 1] // 可以一次添加多个元素 s append(s, 2, 3, 4) printSlice(s) // len5 cap8 [0 1 2 3 4] } func printSlice(s []int) { fmt.Printf(len%d cap%d %v\n, len(s), cap(s), s) }append 的函数签名func append(s []T, vs ...T) []T底层逻辑重点append接收一个切片和若干个值返回一个新的切片如果底层数组还有剩余容量len capappend直接在数组末尾添加元素返回的切片指向同一个底层数组如果容量不够len capappend会分配一个更大的新数组通常是原来容量的 2 倍把旧数据复制到新数组在新数组末尾添加新元素返回指向新数组的切片重要append可能返回一个新的切片指向新的底层数组所以必须用s append(s, v)接收返回值容量增长策略旧容量 1024 时新容量 旧容量 × 2旧容量 1024 时新容量 ≈ 旧容量 × 1.252.10 range 遍历切片for...range循环可以遍历切片和数组、map、字符串等。每次迭代返回两个值索引和元素的副本。pack age main import fmt func main() { var pow []int{1, 2, 4, 8, 16, 32, 64, 128} for i, v : range pow { fmt.Printf(2**%d %d\n, i, v) } }输出2**0 1 2**1 2 2**2 4 2**3 8 ...range 的灵活用法// 只取索引忽略值 for i, _ : range pow { // 使用 i } // 只取值忽略索引用 _ 占位 for _, value : range pow { // 使用 value } // 只想要索引可以省略第二个变量 for i : range pow { // 使用 i }底层逻辑range返回的值是元素的副本不是引用修改v不会影响原切片中的元素如果要修改原切片的元素需要用索引pow[i] newValue2.11 range 进阶用法package main import fmt func main() { // 用 make 创建一个长度为 10 的切片 pow : make([]int, 10) // 只用索引遍历用索引计算值 for i : range pow { pow[i] 1 uint(i) // 1 左移 i 位等于 2**i } // 只取值遍历忽略索引 for _, value : range pow { fmt.Printf(%d\n, value) } }输出1 2 4 8 16 32 64 128 256 512关键技巧1 uint(i)是位运算1 左移 i 位结果是 2 的 i 次方需要把iint 类型转换为uint类型因为移位运算符要求无符号整数range可以省略值变量只保留索引变量2.12 综合练习用切片生成图片这是一个综合练习演示了二维切片、make创建、range遍历、类型转换的综合运用。package main import golang.org/x/tour/pic func Pic(dx, dy int) [][]uint8 { // 创建一个 dy 行的二维切片 img : make([][]uint8, dy) // 遍历每一行 for y : range img { // 每行创建 dx 个元素的切片 img[y] make([]uint8, dx) // 遍历每一列设置像素值 for x : range img[y] { img[y][x] uint8(x * y) // 类型转换int → uint8 } } return img } func main() { pic.Show(Pic) }知识点总结make([][]uint8, dy)创建外层切片dy 行每行需要单独make([]uint8, dx)创建内层切片uint8(x * y)是类型转换把 int 结果转为 uint8这个练习让你理解如何在 Go 中构建和操作多维数据结构第三部分结构体 vs 切片 对比总结特性结构体Struct切片Slice本质值类型内存连续引用类型指向底层数组创建方式Vertex{1, 2}或Vertex{1, 2}make([]T, len, cap)或字面值零值所有字段为零值nil长度/容量固定编译时确定动态len 和 cap扩展不支持append()自动扩展内存分配栈或堆取决于逃逸分析底层数组一定在堆上复制行为赋值时完整复制赋值时只复制 slice header24字节典型用途表示有固定属性的实体表示可变长度的数据集合第四部分常见易错点4.1 切片共享底层数组的陷阱a : []int{1, 2, 3, 4, 5} b : a[:3] // [1, 2, 3] b[0] 999 // a 也变成了 [999, 2, 3, 4, 5]解决如果需要独立副本用copy()函数。4.2 append 可能改变底层数组a : make([]int, 3, 5) // len3, cap5 b : a // b 和 a 共享底层数组 b append(b, 4) // 容量够共享数组 b append(b, 5) // 容量够共享数组 b append(b, 6) // 容量不够分配新数组b 指向新数组a 不变解决永远用s append(s, v)接收返回值。4.3 range 返回的是副本s : []int{1, 2, 3} for _, v : range s { v 100 // 只修改了副本s 不变 } // s 仍然是 [1, 2, 3]解决用索引修改for i : range s { s[i] 100 }4.4 结构体指针 vs 值func modify1(v Vertex) { v.X 100 } // 修改的是副本不影响原值 func modify2(v *Vertex) { v.X 100 } // 修改的是原值 v : Vertex{1, 2} modify1(v) // v 不变 modify2(v) // v.X 变成 100
Go 基础:结构体与切片
第一部分结构体Struct1.1 什么是结构体结构体是 Go 语言中用来组合多个不同类型字段的复合数据类型。你可以把它理解为一个模板或蓝图用来描述一个事物的多个属性。package main import fmt // 定义一个结构体类型 type Vertex struct { X int Y int } func main() { v : Vertex{1, 2} fmt.Println(v) // {1 2} }底层逻辑结构体在内存中是连续分配的字段按照声明顺序依次排列每个字段占据固定大小的内存空间结构体是值类型赋值时会复制整个结构体的数据关键概念type关键字定义新类型struct关键字声明这是一个结构体大写字母开头的字段名如X、Y是导出的可被其他包访问小写字母开头的字段名只能在当前包内访问1.2 结构体字段访问通过点号.来访问结构体的字段可以读取也可以修改。package main import fmt type Vertex struct { X int Y int } func main() { v : Vertex{1, 2} v.X 4 // 修改 X 字段为 4 fmt.Println(v.X) // 输出: 4 fmt.Println(v.Y) // 输出: 2 }底层逻辑点号.运算符在编译时会计算字段相对于结构体起始地址的偏移量访问v.X时Go 编译器会生成类似*(base_address offset_of_X)的机器码结构体字段在内存中的布局与声明顺序一致1.3 指向结构体的指针你可以使用指针来引用结构体。指针存储的是结构体在内存中的地址而不是结构体的副本。package main import fmt type Vertex struct { X int Y int } func main() { v : Vertex{1, 2} p : v // p 是指向 v 的指针 p.X 1e9 // 通过指针修改字段Go 会自动解引用 fmt.Println(v) // 输出: {1000000000 2} }底层逻辑v取结构体 v 的内存地址p.X实际上是(*p).X的语法糖Go 自动帮你解引用通过指针修改字段时修改的是原始结构体不是副本传递指针给函数时函数内修改会影响原始结构体避免大结构体复制的性能开销何时使用指针需要在函数中修改原始结构体时结构体很大复制成本高时需要共享同一个结构体实例时1.4 结构体字面值Struct Literals创建结构体时可以用字面值语法有两种写法package main import fmt type Vertex struct { X, Y int } var ( // 写法一按字段顺序列出值必须按声明顺序 v1 Vertex{1, 2} // 类型是 Vertex // 写法二用字段名指定值推荐更清晰顺序可以打乱 v2 Vertex{X: 1} // Y 默认为零值 0 // 写法三完全省略字段名按顺序不推荐用于字段多的情况 v3 Vertex{} // X:0, Y:0全是零值 // 写法四指针类型 p Vertex{1, 2} // 类型是 *Vertex ) func main() { fmt.Println(v1, v2, v3, p) }关键规则如果用了字段名: 值的语法字段顺序可以随意未指定的字段自动为零值如果不用字段名必须严格按照声明顺序填写所有字段Vertex{1, 2}返回的是指向结构体的指针零值规则int→0, float→0.0, string→, bool→false, 指针→nil第二部分切片Slice2.1 什么是切片切片是对数组的一个连续片段的引用它是 Go 语言中最常用的数据结构之一。package main import fmt func main() { // 先创建一个数组 primes : [6]int{2, 3, 5, 7, 11, 13} // 从数组创建一个切片取索引 1 到 3不含 4 var s []int primes[1:4] fmt.Println(s) // [3 5 7] }底层逻辑重点切片在底层由三个部分组成slice headerstruct SliceHeader { Data uintptr // 指向底层数组的指针 Len int // 切片的长度当前有多少个元素 Cap int // 切片的容量从起始位置到底层数组末尾的元素个数 }切片本身不存储数据它只是对底层数组的窗口或视图primes[1:4]表示从索引 1 开始到索引 4 结束不含所以取的是primes[1]、primes[2]、primes[3]切片的下界可以省略默认为 0上界也可以省略默认为数组长度2.2 切片就像数组的引用切片不拥有数据它引用的是底层数组。修改切片的元素会修改底层数组反之亦然。package main import fmt func main() { names : [4]string{ John, Paul, George, Ringo, } fmt.Println(names) a : names[0:2] // [John Paul] b : names[1:3] // [Paul George] fmt.Println(a, b) // 修改 b[0]实际上修改的是底层数组的 names[1] b[0] XXX fmt.Println(a, b) fmt.Println(names) // [John XXX George Ringo] —— a 也受到了影响 }底层逻辑a和b共享同一个底层数组a的 Data 指针指向names[0]b的 Data 指针指向names[1]修改b[0]就是修改底层数组的names[1]a也能看到这个变化这就是为什么切片叫引用——多个切片可以指向同一段内存2.3 切片字面值切片字面值的语法和数组类似但不需要指定长度。package main import fmt func main() { // 切片字面值不需要指定长度 q : []int{2, 3, 5, 7, 11, 13} fmt.Println(q) // 修改元素 q[0] 100 fmt.Println(q) // [100 3 5 7 11 13] }与数组的区别数组[6]int{...}—— 方括号里有数字长度是类型的一部分切片[]int{...}—— 方括号里没有数字长度是动态的2.4 切片的默认行为切片时省略边界Go 会使用合理的默认值。package main import fmt func main() { s : []int{2, 3, 5, 7, 11, 13} printSlice(s) // len6 cap6 [2 3 5 7 11 13] // 省略下界默认为 0 s s[:0] printSlice(s) // len0 cap6 [] // 省略上界默认为切片的长度 s s[:4] printSlice(s) // len4 cap6 [2 3 5 7] // 两个都省略等价于 s[0:len(s)] s s[2:] printSlice(s) // len2 cap4 [5 7] } func printSlice(s []int) { fmt.Printf(len%d cap%d %v\n, len(s), cap(s), s) }默认值规则总结写法等价于说明s[:n]s[0:n]从头取 n 个元素s[n:]s[n:len(s)]从 n 取到末尾s[:]s[0:len(s)]取全部元素2.5 切片的长度与容量长度len切片当前包含的元素个数。容量cap从切片的第一个元素开始到底层数组末尾的元素个数。package main import fmt func main() { s : []int{2, 3, 5, 7, 11, 13} printSlice(s) // len6 cap6 [2 3 5 7 11 13] // 把长度设为 0但容量不变 s s[:0] printSlice(s) // len0 cap6 [] // 扩展长度到 4容量仍然足够 s s[:4] printSlice(s) // len4 cap6 [2 3 5 7] // 丢弃前两个元素长度和容量都减少 s s[2:] printSlice(s) // len2 cap4 [5 7] } func printSlice(s []int) { fmt.Printf(len%d cap%d %v\n, len(s), cap(s), s) }底层逻辑图解初始: s []int{2, 3, 5, 7, 11, 13} 底层数组: [2, 3, 5, 7, 11, 13] ↑ ↑ | | Data指向 数组末尾 len6, cap6 s s[:0]: len0, cap6容量不变只是窗口缩小了 s s[:4]: len4, cap6扩展窗口但不能超过容量 s s[2:]: len2, cap4Data指针右移2位容量从新起点算起关键规则切片不能超过其容量否则会 paniclen(s) cap(s)永远成立重新切片可以延长长度前提是容量足够2.6 零切片Nil Slice切片的零值是nil。nil 切片没有底层数组长度和容量都为 0。package main import fmt func main() { var s []int // 没有初始化s 是 nil fmt.Println(s, len(s), cap(s)) // [] 0 0 if s nil { fmt.Println(nil!) // 会执行到这里 } }注意事项var s []int声明但未初始化 → s 为 nils : []int{}或s : make([]int, 0)→ s 不是 nil只是长度为 0nil 切片和空切片的区别nil 切片没有底层数组JSON 序列化时为null空切片有底层数组长度为 0JSON 序列化时为[]对 nil 切片使用append是安全的会自动分配底层数组2.7 用 make 创建切片make是 Go 的内置函数用来创建动态大小的切片。它会在底层分配一个数组并返回指向它的切片。package main import fmt func main() { // make([]类型, 长度) —— 长度容量 a : make([]int, 5) printSlice(a, a) // a len5 cap5 [0 0 0 0 0] // make([]类型, 长度, 容量) —— 长度和容量不同 b : make([]int, 0, 5) printSlice(b, b) // b len0 cap5 [] // 扩展 b 的长度到容量 b b[:cap(b)] printSlice(b, b) // b len5 cap5 [0 0 0 0 0] // 丢弃第一个元素 b b[1:] printSlice(b, b) // b len4 cap4 [0 0 0 0] } func printSlice(name string, x []int) { fmt.Printf(%s len%d cap%d %v\n, name, len(x), cap(x), x) }关键要点make([]int, 5)→ len5, cap5元素全部为零值make([]int, 0, 5)→ len0, cap5适合后续用 append 添加元素make分配的是归零的数组所有元素初始值为类型的零值第三个参数容量是可选的不传则容量长度2.8 切片的切片多维切片切片可以包含任何类型包括其他切片。这就形成了多维切片。package main import ( fmt strings ) func main() { // 创建一个 3x3 的井字棋盘二维切片 board : [][]string{ []string{_, _, _}, []string{_, _, _}, []string{_, _, _}, } // 玩家轮流下棋 board[0][0] X board[2][2] O board[1][2] X board[1][0] O board[0][2] X // 打印棋盘 for i : 0; i len(board); i { fmt.Printf(%s\n, strings.Join(board[i], )) } }输出X _ X O _ X _ _ O底层逻辑[][]string是一个切片的切片外层切片的每个元素都是一个[]string每一行内层切片可以有不同的长度这就是不规则多维数组访问board[i][j]时先取外层切片的第 i 个元素一个切片再取这个切片的第 j 个元素2.9 追加元素到切片appendGo 提供了内置函数append来向切片添加元素。package main import fmt func main() { var s []int // nil 切片 printSlice(s) // len0 cap0 [] // append 可以作用于 nil 切片 s append(s, 0) printSlice(s) // len1 cap1 [0] // 切片会根据需要自动增长 s append(s, 1) printSlice(s) // len2 cap2 [0 1] // 可以一次添加多个元素 s append(s, 2, 3, 4) printSlice(s) // len5 cap8 [0 1 2 3 4] } func printSlice(s []int) { fmt.Printf(len%d cap%d %v\n, len(s), cap(s), s) }append 的函数签名func append(s []T, vs ...T) []T底层逻辑重点append接收一个切片和若干个值返回一个新的切片如果底层数组还有剩余容量len capappend直接在数组末尾添加元素返回的切片指向同一个底层数组如果容量不够len capappend会分配一个更大的新数组通常是原来容量的 2 倍把旧数据复制到新数组在新数组末尾添加新元素返回指向新数组的切片重要append可能返回一个新的切片指向新的底层数组所以必须用s append(s, v)接收返回值容量增长策略旧容量 1024 时新容量 旧容量 × 2旧容量 1024 时新容量 ≈ 旧容量 × 1.252.10 range 遍历切片for...range循环可以遍历切片和数组、map、字符串等。每次迭代返回两个值索引和元素的副本。pack age main import fmt func main() { var pow []int{1, 2, 4, 8, 16, 32, 64, 128} for i, v : range pow { fmt.Printf(2**%d %d\n, i, v) } }输出2**0 1 2**1 2 2**2 4 2**3 8 ...range 的灵活用法// 只取索引忽略值 for i, _ : range pow { // 使用 i } // 只取值忽略索引用 _ 占位 for _, value : range pow { // 使用 value } // 只想要索引可以省略第二个变量 for i : range pow { // 使用 i }底层逻辑range返回的值是元素的副本不是引用修改v不会影响原切片中的元素如果要修改原切片的元素需要用索引pow[i] newValue2.11 range 进阶用法package main import fmt func main() { // 用 make 创建一个长度为 10 的切片 pow : make([]int, 10) // 只用索引遍历用索引计算值 for i : range pow { pow[i] 1 uint(i) // 1 左移 i 位等于 2**i } // 只取值遍历忽略索引 for _, value : range pow { fmt.Printf(%d\n, value) } }输出1 2 4 8 16 32 64 128 256 512关键技巧1 uint(i)是位运算1 左移 i 位结果是 2 的 i 次方需要把iint 类型转换为uint类型因为移位运算符要求无符号整数range可以省略值变量只保留索引变量2.12 综合练习用切片生成图片这是一个综合练习演示了二维切片、make创建、range遍历、类型转换的综合运用。package main import golang.org/x/tour/pic func Pic(dx, dy int) [][]uint8 { // 创建一个 dy 行的二维切片 img : make([][]uint8, dy) // 遍历每一行 for y : range img { // 每行创建 dx 个元素的切片 img[y] make([]uint8, dx) // 遍历每一列设置像素值 for x : range img[y] { img[y][x] uint8(x * y) // 类型转换int → uint8 } } return img } func main() { pic.Show(Pic) }知识点总结make([][]uint8, dy)创建外层切片dy 行每行需要单独make([]uint8, dx)创建内层切片uint8(x * y)是类型转换把 int 结果转为 uint8这个练习让你理解如何在 Go 中构建和操作多维数据结构第三部分结构体 vs 切片 对比总结特性结构体Struct切片Slice本质值类型内存连续引用类型指向底层数组创建方式Vertex{1, 2}或Vertex{1, 2}make([]T, len, cap)或字面值零值所有字段为零值nil长度/容量固定编译时确定动态len 和 cap扩展不支持append()自动扩展内存分配栈或堆取决于逃逸分析底层数组一定在堆上复制行为赋值时完整复制赋值时只复制 slice header24字节典型用途表示有固定属性的实体表示可变长度的数据集合第四部分常见易错点4.1 切片共享底层数组的陷阱a : []int{1, 2, 3, 4, 5} b : a[:3] // [1, 2, 3] b[0] 999 // a 也变成了 [999, 2, 3, 4, 5]解决如果需要独立副本用copy()函数。4.2 append 可能改变底层数组a : make([]int, 3, 5) // len3, cap5 b : a // b 和 a 共享底层数组 b append(b, 4) // 容量够共享数组 b append(b, 5) // 容量够共享数组 b append(b, 6) // 容量不够分配新数组b 指向新数组a 不变解决永远用s append(s, v)接收返回值。4.3 range 返回的是副本s : []int{1, 2, 3} for _, v : range s { v 100 // 只修改了副本s 不变 } // s 仍然是 [1, 2, 3]解决用索引修改for i : range s { s[i] 100 }4.4 结构体指针 vs 值func modify1(v Vertex) { v.X 100 } // 修改的是副本不影响原值 func modify2(v *Vertex) { v.X 100 } // 修改的是原值 v : Vertex{1, 2} modify1(v) // v 不变 modify2(v) // v.X 变成 100