返回

参数传递

最近经常在参数传递这块出问题,特此记录一下~~

基础

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
}

在代码二中,cloneGraphclone 函数的参数 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)
}
最后更新于 Oct 09, 2024 16:40 UTC
Built with Hugo
Theme Stack designed by Jimmy