简介
这是我学习https://draven.co/golang的笔记。
1.数据结构
1.1 数组
问题:
1.请解释 Go 语言中数组的类型是如何定义的?比如 [3]int
和 [4]int
是否属于同一类型?为什么?
2.Go 语言中初始化数组有两种常见方式:[3]int{1,2,3}
和 [...]int{1,2,3}
,这两种方式有什么联系和区别?编译器会如何处理 [...]T
这种声明方式?
3.数组的"长度固定"是其重要特性,请问数组初始化后能否动态修改长度?如果尝试为一个长度为 3 的数组添加第 4 个元素,会发生什么?
4.在访问数组元素时,Go 语言会如何检查索引是否越界?比如 arr[4]
(arr 是长度为 3 的数组)和 arr[i]
(i 是值为 4 的变量)两种情况,检查时机和结果有什么不同?
5.当数组作为函数参数传递时,是值传递还是引用传递?这种传递方式会导致什么结果?如果希望在函数内部修改原数组的内容,应该如何处理?
6.数组初始化时,元素数量不同会影响内存分配位置:当元素数量≤4 和 >4 时,存储位置有什么区别?这对数组的使用有什么实际影响?
7.结合你对数组的理解,简要说明数组和切片的核心区别(从大小特性、类型定义等角度)。
我的回答&ai的纠错:
题目:请解释 Go 语言中数组的类型是如何定义的?比如
[3]int
和[4]int
是否属于同一类型?为什么?你的回答:数组以大小和数据类型定义,例如 [5] int。不属于。只有大小和其中元素属性相同才是同一类型。
修正:Go 语言中数组的类型严格由「元素类型」和「长度」共同定义,例如
[3]int
和[4]int
是完全不同的类型,无法互相赋值或比较。只有当元素类型和长度均相同时,才属于同一类型。题目:Go 语言中初始化数组有两种常见方式:
[3]int{1,2,3}
和[...]int{1,2,3}
,这两种方式有什么联系和区别?编译器会如何处理[...]T
这种声明方式?你的回答:第一种是手动输入长度,第二种是编译器自己算长度。
修正:两种方式在运行时完全等价。
[...]T
是编译器提供的语法糖,编译期间会通过遍历元素自动推导长度,最终转换为[n]T
的形式(如[...]int{1,2,3}
会被转换为[3]int{1,2,3}
)。题目:数组的 "长度固定" 是其重要特性,请问数组初始化后能否动态修改长度?如果尝试为一个长度为 3 的数组添加第 4 个元素,会发生什么?
你的回答:不能。会报错。
修正:不能动态修改长度。如果尝试通过索引添加第 4 个元素(如
arr[3] = 4
,而数组长度为 3),会触发越界错误:若使用常量索引(如arr[3]
),编译时直接报错;若使用变量索引(如i=3; arr[i]
),运行时会 panic。题目:在访问数组元素时,Go 语言会如何检查索引是否越界?比如
arr[4]
(arr 是长度为 3 的数组)和arr[i]
(i 是值为 4 的变量)两种情况,检查时机和结果有什么不同?你的回答:如果是纯数字访问,编译的时候就会检查错误;如果是变量访问,就需要 go 的运行时来检查了。
修正:Go 语言通过「编译期静态检查」和「运行时动态检查」判断索引越界:
- 若使用常量索引(如
arr[4]
,数组长度为 3),编译时直接报错 “invalid array index (out of bounds)”; - 若使用变量索引(如
i=4; arr[i]
),编译期无法判断,运行时会触发panic: runtime error: index out of range
。
- 若使用常量索引(如
题目:当数组作为函数参数传递时,是值传递还是引用传递?这种传递方式会导致什么结果?如果希望在函数内部修改原数组的内容,应该如何处理?
你的回答:值传递。导致函数内部无法修改数组。应该加上 & 变成引用传递。
修正:数组作为函数参数时是值传递(传递数组的副本),因此函数内部修改的是副本,不会影响原数组。若要修改原数组,应传递数组的指针(如
func f(arr *[3]int)
),通过指针间接操作原数组(Go 语言中没有 “引用传递”,只有值传递,指针本身也是值)。题目:数组初始化时,元素数量不同会影响内存分配位置:当元素数量≤4 和 >4 时,存储位置有什么区别?这对数组的使用有什么实际影响?
你的回答:<=4,会直接放在栈里面;>4 会放在静态区,在运行的时候再拿到栈上。
修正:在不考虑逃逸分析的情况下,数组元素数量≤4 时,会直接在栈上初始化;元素数量 > 4 时,会先在静态区初始化,再拷贝到栈上。这一机制是编译器的优化,对开发者透明,但影响数组的内存分配效率(栈上分配更快,静态区分配需额外拷贝)。
题目:结合你对数组的理解,简要说明数组和切片的核心区别(从大小特性、类型定义等角度)。
你的回答:尚未学习到切片。
修正:如实说明未学习即可。若希望提前了解,可简单补充:数组与切片的核心区别是「大小是否固定」—— 数组大小在定义时确定且不可变,类型包含长度(如
[3]int
);切片大小可动态变化,类型不包含长度(如[]int
)。
1.2 切片
问题:
- 题目:Go 语言中切片的类型是如何定义的?与数组的类型定义有什么本质区别?请举例说明。
- 题目:切片在运行时的数据结构包含哪几个部分?各部分分别表示什么含义?
- 题目:Go 语言中初始化切片有哪些常见方式?请分别举例说明。其中,通过
arr[0:3]
(arr 是数组)创建的切片与原数组有什么关系? - 题目:切片的
len()
和cap()
方法分别返回什么?两者之间有什么约束关系(比如谁必须大于等于谁)? - 题目:使用
append
向切片追加元素时,如果切片当前容量足够(len < cap
),会发生什么?如果容量不足(len == cap
),又会触发什么操作? - 题目:切片扩容时,新容量的计算规则是什么?请分情况说明(比如当前长度小于 1024 和大于等于 1024 时的区别)。
- 题目:切片扩容后,新切片与原切片的底层数组一定不同吗?为什么?这对使用切片有什么影响?
- 题目:使用
copy(a, b)
函数拷贝切片时,实际拷贝的元素数量由什么决定?拷贝完成后,修改切片a
的元素会影响切片b
吗? - 题目:请从「长度特性」「内存共享」「类型定义」三个角度,总结切片与数组的核心区别。
ai的反馈:
题目:Go 语言中切片的类型是如何定义的?与数组的类型定义有什么本质区别?请举例说明。
你的回答:[] int {} 类似于这样定义的,[]+ 数据类型。数组需要在 [] 中填写大小或者...,而切片啥都不需要填写。例如 [] int {1,2,3} 和 [3] int {1,2,3},前者属于切片,后者属于数组。
纠正 / 答案:切片类型定义为
[]T
(T 为元素类型),仅由元素类型决定;数组类型定义为[n]T
或[...]T
,由元素类型和长度共同决定(长度不同则类型不同)。例如:[]int
是切片类型,[3]int
和[4]int
是不同的数组类型,而[]int
与元素类型相同的其他切片(如[]int{1,2}
)属于同一类型。题目:切片在运行时的数据结构包含哪几个部分?各部分分别表示什么含义?
你的回答:包含 len,caps,data,分别代表长度,容量和数据区域。
纠正 / 答案:切片在运行时的数据结构包含三部分:
Len
(当前切片的长度,即元素个数)、Cap
(当前切片的容量,即底层数组的大小)、Data
(指向底层数组的指针)。其中 “caps” 表述不准确,应为 “Cap”;“数据区域” 更准确的描述是 “指向底层数组的指针”。题目:Go 语言中初始化切片有哪些常见方式?请分别举例说明。其中,通过
arr[0:3]
(arr 是数组)创建的切片与原数组有什么关系?你的回答:arr [0:3] [] int {} [] interface {} 这三种方式。是原数组的子数组。
纠正 / 答案:初始化切片的三种常见方式为:
- 通过数组 / 切片下标:
arr[0:3]
(arr 为数组或切片); - 字面量:
slice := []int{1, 2, 3}
; make 函数:
slice := make([]int, 3, 5)
(长度 3,容量 5)。其中[]interface{}
是切片类型,并非初始化方式。通过arr[0:3]
创建的切片与原数组共享底层内存,修改切片元素会同时影响原数组。
- 通过数组 / 切片下标:
题目:切片的
len()
和cap()
方法分别返回什么?两者之间有什么约束关系(比如谁必须大于等于谁)?你的回答:len () 返回长度,cap()返回容量。cap>=len。
纠正 / 答案:
len()
返回切片当前的元素个数(长度),cap()
返回切片底层数组的大小(容量)。两者的约束关系是容量必须大于等于长度(cap >= len
),这是切片的基本特性。题目:使用
append
向切片追加元素时,如果切片当前容量足够(len < cap
),会发生什么?如果容量不足(len == cap
),又会触发什么操作?你的回答:会正常添加元素。如果不足,会触发扩容的操作。
纠正 / 答案:若容量足够(
len < cap
),会直接在底层数组的剩余空间添加元素,切片长度增加,不分配新内存;若容量不足(len == cap
),会触发扩容:分配新的底层数组,将原数组元素拷贝到新数组,新切片指向新数组,同时更新长度和容量。题目:切片扩容时,新容量的计算规则是什么?请分情况说明(比如当前长度小于 1024 和大于等于 1024 时的区别)。
你的回答:如果当前长度小于 1024,会直接翻倍;如果大于 1024,会每次增加 25% 的长度,直到新容量大于期望容量。如果期望容量大于当前容量的两倍就会使用期望容量。
纠正 / 答案:扩容时新容量的计算基于当前容量(非长度):
- 若期望容量 > 当前容量的 2 倍,则新容量 = 期望容量;
- 若当前容量 < 1024,则新容量 = 当前容量 × 2;
- 若当前容量 ≥ 1024,则新容量每次增加当前容量的 25%,直到 ≥ 期望容量。
题目:切片扩容后,新切片与原切片的底层数组一定不同吗?为什么?这对使用切片有什么影响?
你的回答:我不清楚。
纠正 / 答案:不一定。若扩容时触发了新内存分配(容量不足),新切片会指向新的底层数组,与原切片数组不同;若未触发扩容(容量足够),则共享原数组。影响:若共享底层数组,修改新切片会影响原切片;若已扩容(数组不同),则两者修改互不影响。
题目:使用
copy(a, b)
函数拷贝切片时,实际拷贝的元素数量由什么决定?拷贝完成后,修改切片a
的元素会影响切片b
吗?你的回答:由 b 的长度决定。不会。
纠正 / 答案:实际拷贝的元素数量是切片 a 和 b 长度的较小值(即
min(len(a), len(b))
)。拷贝完成后,修改切片a
的元素不会影响切片b
,因为copy
会将b
的元素值拷贝到a
的底层内存,两者底层数组独立(除非原本就共享且未发生扩容)。题目:请从「长度特性」「内存共享」「类型定义」三个角度,总结切片与数组的核心区别。
你的回答:长度特性:数组长度固定,切片长度可变。内存共享:数组分配固定内存大小,切片会动态分配新内存。类型定义:数组在定义时就需要声明大小,或者写... 让编译器自己推断,而切片不需要。
纠正 / 答案:
- 长度特性:数组长度固定(初始化后不可变);切片长度可变(可通过
append
动态调整)。 - 内存共享:数组内存独立(每个数组有自己的内存);切片可能与原数组或其他切片共享底层内存(通过下标创建时),动态扩容时会分配新内存。
- 类型定义:数组类型由元素类型和长度共同决定(如
[3]int
与[4]int
不同);切片类型仅由元素类型决定(如[]int
统一为一种类型)。
- 长度特性:数组长度固定(初始化后不可变);切片长度可变(可通过