最近经常在参数传递这块出问题,特此记录一下~~
基础
Go 中变量有两类:
- 值类型:string、int、bool、float 等
- 引用类型:slice、map、chan 等
深拷贝和浅拷贝:
- 浅拷贝:把变量里存的内存地址拷贝了,所指向的真实值并没拷贝
- 深拷贝:为数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝
golang默认都是采用值传递,也就是深拷贝。 只有一些特定的类型,如slice、map、channel、function、pointer这些天生就是指针的类型,是通过引用传递的
总结:
- string、int、bool、float 等值类型是值传递,深拷贝,实现真正内容上的拷贝,修改拷贝不会影响原值
- slice、map、chan等引用类型是引用传递,浅拷贝,仅拷贝了内存地址,修改拷贝会影响原值
切片拷贝
- copy复制为值复制,改变原切片的值不会影响新切片
- 等号复制为指针复制,改变原切片或新切片都会对另一个产生影响
1
2
3
4
5
|
// 值复制
target := make([]int, len(source))
copy(target, source)
// 指针复制
target := source
|
递归中的参数传递和切片append问题
为什么代码一的path在递归回溯中会回溯掉数据,而代码二的mp在递归回溯中不会回溯掉数据?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
//代码一:
func inorderTraversal(root *TreeNode) []int {
res := make([]int, 0)
dfs(root, res)
return res
}
func dfs(cur *TreeNode, res []int) {
if cur == nil {
return
}
dfs(cur.Left, res)
res = append(res, cur.Val)
dfs(cur.Right, res)
}
//代码二:
func cloneGraph(node *Node) *Node {
// 记录旧->新节点的对应
mp := make(map[*Node]*Node)
return clone(node, mp)
}
func clone(cur *Node, mp map[*Node]*Node) *Node {
if cur == nil {
return cur
}
// 字典中存在该节点对应的新节点
if v, ok := mp[cur]; ok {
return v
}
// 构建新节点
newNode := &Node{
Val: cur.Val,
Neighbors: make([]*Node, len(cur.Neighbors)),
}
// 更新字典:旧节点->新节点
mp[cur] = newNode
// 遍历当前节点的邻居节点
for i := 0; i < len(cur.Neighbors); i++ {
newNode.Neighbors[i] = clone(cur.Neighbors[i], mp)
}
return newNode
}
|
在这两段代码中,主要区别在于如何处理传递参数和更新数据的方式,这直接影响了递归过程中的数据保存和传递方式。
代码一分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
func inorderTraversal(root *TreeNode) []int {
res := make([]int, 0)
dfs(root, res)
return res
}
func dfs(cur *TreeNode, res []int) {
if cur == nil {
return
}
dfs(cur.Left, res)
res = append(res, cur.Val) // 这里实际上是在当前函数内部操作了一个新的 res 切片,而不是原始的 res
dfs(cur.Right, res)
}
|
在代码一中,dfs
函数的参数 res
是一个切片,切片在 Go 中是引用类型,传递的是指向底层数据结构的指针。但是,关键问题在于 res = append(res, cur.Val)
这一行,这里实际上是将 cur.Val
追加到了 res
的副本上,而不是原始的 res
。因为在 Go 中,切片是引用传递,但如果在函数内部重新分配了切片,会导致传入的切片的底层数组不会被修改。所以,尽管看起来在 dfs
函数内部对 res
进行了操作,但实际上并没有改变 inorderTraversal
函数中的 res
切片。
代码二分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
func cloneGraph(node *Node) *Node {
mp := make(map[*Node]*Node)
return clone(node, mp)
}
func clone(cur *Node, mp map[*Node]*Node) *Node {
if cur == nil {
return cur
}
if v, ok := mp[cur]; ok {
return v
}
newNode := &Node{
Val: cur.Val,
Neighbors: make([]*Node, len(cur.Neighbors)),
}
mp[cur] = newNode
for i := 0; i < len(cur.Neighbors); i++ {
newNode.Neighbors[i] = clone(cur.Neighbors[i], mp)
}
return newNode
}
|
在代码二中,cloneGraph
和 clone
函数的参数 mp
是一个 map[*Node]*Node
类型,而 map
在 Go 中是引用类型,传递的是指向底层数据结构的指针。这意味着,当在 clone
函数内部对 mp
进行操作时,实际上是在操作原始的 map
,而不是 cloneGraph
函数中的副本。
所以,即使在递归的过程中,对 mp
进行了更新(如 mp[cur] = newNode
),这些更新会反映在 cloneGraph
函数中的原始 mp
中,因为它们共享相同的底层数据结构。
总结
关键区别在于切片和 map
在 Go 中的特性:
- 切片是引用类型,但如果在函数内部重新分配切片,会影响的是新分配的切片,而不会影响原始切片。
- map 也是引用类型,对
map
的修改会影响原始的 map
,因为它们共享相同的底层数据结构。
因此,在代码一中,由于 res
切片被重新分配,导致递归回溯时对 res
的修改不会影响到 inorderTraversal
函数中的原始 res
。而在代码二中,mp
是共享的,所以递归回溯时对 mp
的修改会直接影响到 cloneGraph
函数中的原始 mp
。
Go语言中函数参数的传递方式都是值传递
在Go语言中,切片(slice)是一个引用类型,但它包含三个部分:指向底层数组(array)的指针、长度(len)和容量(cap)。将切片作为参数传递给函数时,实际上传递的是指向这个切片的指针及其长度和容量信息的副本,但它们指向的是相同的底层数组。
这意味着,如果在函数内部修改了切片的内容(例如,通过索引访问或修改切片内的元素),那么这些修改会影响到原始切片。但是,如果改变了切片本身的长度或其他元数据,那么这些改变不会影响到原始切片,因为改变的是切片元数据的副本。
示例
1
2
3
4
5
6
7
8
9
10
11
12
13
|
func modifySlice(s []int) {
s[0] = 100 // 修改切片中的元素
s = append(s, 200) // 改变切片本身,原始切片不受影响
}
func main() {
mySlice := []int{1, 2, 3, 4, 5}
fmt.Println("Before modification:", mySlice) // Before modification: [1 2 3 4 5]
modifySlice(mySlice)
fmt.Println("After modification:", mySlice) // After modification: [100 2 3 4 5]
}
|
若想修改成功,则需要通过指针传递切片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// appendToSlice 向切片中追加元素
func appendToSlice(slice *[]int) {
*slice = append(*slice, 200)
fmt.Println("Inside function after append:", *slice)
}
// removeLastElement 从切片中删除最后一个元素
func removeLastElement(slice *[]int) {
if len(*slice) > 0 {
// 注意此处*slice本身就是一个切片,而切片索引操作[:len(*slice)-1]需要作用于一个切片类型
// (*slice)是一个切片类型的表达式,它返回切片的值
*slice = (*slice)[:len(*slice)-1]
}
fmt.Println("Inside function after removal:", *slice)
}
func main() {
mySlice := []int{1, 2, 3, 4, 5}
fmt.Println("Initial slice:", mySlice)
appendToSlice(&mySlice)
fmt.Println("After appending:", mySlice)
removeLastElement(&mySlice)
fmt.Println("After removal:", mySlice)
}
|