为什么在函数传参时,数组参数有无固定长度,会产生这样的差距,就是arr5里面的内容不会被改变,arr7却会改变,刚开始学go,求大佬赐教

codeYuanY · 2020-10-07 20:27:50 · 1152 次点击 · 大约8小时之前 开始浏览    置顶
这是一个创建于 2020-10-07 20:27:50 的主题,其中的信息可能已经有所发展或是发生改变。

arr5 := [5]int{1,2,3,4,5}
arr6 := baseStruct.Modify1(arr5)
fmt.Println(arr5)//打印结果:[1 2 3 4 5]
fmt.Println(arr6)//打印结果:[300 2 3 4 5]
/*
    所用函数
    func Modify1(array [5]int) [5]int{
        array[0] = 300
        return array
    }
 */
arr7 := []int{1,2,3,4,5}
arr8 := baseStruct.Modify2(arr7)
fmt.Println(arr7)//打印结果:[300 2 3 4 5]
fmt.Println(arr8)//打印结果:[300 2 3 4 5]
/*
    所用函数
    func Modify2(array []int) []int {
        array[0] = 300
        return array
    }
 */

有疑问加站长微信联系(非本文作者)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1152 次点击  
加入收藏 微博
8 回复  |  直到 2020-10-09 21:58:17
xuanwen
xuanwen · #1 · 5年之前

这是一个关于array与slice的问题

这两个看起来长得差不多,刚开始的确挺容易弄混淆的

package main

import (
    "fmt"
)

// 数组array-值传递
func Modify1(array [5]int) [5]int {
    array[0] = 300
    return array
}

// slice-地址传递
func Modify2(array []int) []int {
    array[0] = 300
    return array
}

func main() {
    // 数组-需要明确指定大小
    arr5 := [5]int{1, 2, 3, 4, 5}
    arr6 := Modify1(arr5)
    fmt.Println("arr5:", arr5) // 打印结果:[1 2 3 4 5]
    fmt.Println("arr6:", arr6) // 打印结果:[300 2 3 4 5]

    // slice-不需要明确指定大小
    arr7 := []int{1, 2, 3, 4, 5}
    arr8 := Modify2(arr7)
    fmt.Println("arr7:", arr7) // 打印结果:[300 2 3 4 5]
    fmt.Println("arr8:", arr8) // 打印结果:[300 2 3 4 5]

    /*
        // 数组-大小不可以变化
        // 此处报错: ./main.go:32:16: first argument to append must be slice; have [5]int
        arr9 := append(arr5, 6)
        fmt.Println("arr9:", arr9) // 此处报错
    */

    // slice-大小可以变化
    arr10 := append(arr7, 6)
    fmt.Println("arr10:", arr10) // 打印结果:[300 2 3 4 5 6]
}

<br/>

明确指定大小即固定长度的为array <br/> 动态改变大小即不固定长度的为slice <br/> 在函数传递中,array是值传递,slice是地址传递 <br/>

<br/> 明确这个就应该很容易理解啦 <br/> arr5 是 array --- 值传递 --- 不改变<br/> arr7 是 slice --- 地址传递 --- 改变<br/> <br/>

传递一个小技巧

在写代码的三个点后面加上golang,代码可以变得高亮

xuanwen
xuanwen · #2 · 5年之前

上面预览的格式有点错乱,重新发布一下总结

明确指定大小即固定长度的为array 
动态改变大小即不固定长度的为slice
在函数传递中,array是值传递,slice是地址传递

明确这个就应该很容易理解啦
arr5 是 array --- 值传递 --- 不改变
arr7 是 slice --- 地址传递 --- 改变

传递一个小技巧

在写代码的三个点后面加上golang,代码可以变得高亮

avtion
avtion · #3 · 5年之前

楼上讲的不够准确,容易陷入坑里面。Slice并不是地址传递,而是一个引用类型

  1. 在Golang里面,所有函数的参数都是值传递,传递过程会将变量进行值拷贝
  2. Slice是引用类型,本质并不是一个单独的类型,而是由三个部分组成,如下所示
    type _slice struct {
     elements unsafe.Pointer // 引用着底层存储在间接部分上的元素
     len      int            // 长度
     cap      int            // 容量
    }
    
  3. 其中elements底层数组的指针,同时也是Slice称之为引用类型的根本
  4. 当函数内部发生扩容,导致底层数组改变,就不会影响外部作用域的底层数组,谨记这一点
xuanwen
xuanwen · #4 · 5年之前

根据楼上3楼的说法,我写了个例子进行测试

package main

import (
    "fmt"
)

// 先改变slice元素,再增加长度 ---- 原slice元素改变,新slice改变
func Modify3(array []int) []int {
    array[0] = 300
    t1 := append(array, 6)
    return t1
}

// 先改变slice元素,再缩小长度 ---- 原slice元素改变,新slice改变
func Modify4(array []int) []int {
    array[0] = 300
    t1 := array[0:3]
    return t1
}

// 先增加长度,再改变新slice元素值 ---- 原slice元素不改变,新slice改变
func Modify5(array []int) []int {
    t1 := append(array, 6)
    t1[0] = 300
    return t1
}

// 先增加长度,再改变原slice元素值 ---- 原slice元素改变,新slice不改变
func Modify6(array []int) []int {
    t1 := append(array, 6)
    array[0] = 300
    return t1
}

// 先缩小长度,再改变原slice元素值 ---- 原slice元素改变,新slice改变
func Modify7(array []int) []int {
    t1 := array[0:3]
    array[0] = 300
    return t1
}

// 先缩小长度,再改变新slice元素值 ---- 原slice元素改变,新slice改变
func Modify8(array []int) []int {
    t1 := array[0:3]
    t1[0] = 300
    return t1
}

func main() {
    arr1 := []int{1, 2, 3, 4, 5}
    arr2 := Modify3(arr1)
    fmt.Println("arr1:", arr1) // 打印结果:[300 2 3 4 5]
    fmt.Println("arr2:", arr2) // 打印结果:[300 2 3 4 5 6]

    arr3 := []int{1, 2, 3, 4, 5}
    arr4 := Modify4(arr3)
    fmt.Println("arr3:", arr3) // 打印结果: [300 2 3 4 5]
    fmt.Println("arr4:", arr4) // 打印结果: [300 2 3]

    arr5 := []int{1, 2, 3, 4, 5}
    arr6 := Modify5(arr5)
    fmt.Println("arr5:", arr5) // 打印结果: [1 2 3 4 5]
    fmt.Println("arr6:", arr6) // 打印结果: [300 2 3 4 5 6]

    arr7 := []int{1, 2, 3, 4, 5}
    arr8 := Modify6(arr7)
    fmt.Println("arr7:", arr7) // 打印结果: [300 2 3 4 5]
    fmt.Println("arr8:", arr8) // 打印结果: [1 2 3 4 5 6]

    arr9 := []int{1, 2, 3, 4, 5}
    arr10 := Modify7(arr9)
    fmt.Println("arr9:", arr9)   // 打印结果: [300 2 3 4 5]
    fmt.Println("arr10:", arr10) // 打印结果: [300 2 3]

    arr11 := []int{1, 2, 3, 4, 5}
    arr12 := Modify8(arr11)
    fmt.Println("arr11:", arr11) // 打印结果: [300 2 3 4 5]
    fmt.Println("arr12:", arr12) // 打印结果: [300 2 3]
}
输出结果:
$ go run main.go
arr1: [300 2 3 4 5]
arr2: [300 2 3 4 5 6]
arr3: [300 2 3 4 5]
arr4: [300 2 3]
arr5: [1 2 3 4 5]
arr6: [300 2 3 4 5 6]
arr7: [300 2 3 4 5]
arr8: [1 2 3 4 5 6]
arr9: [300 2 3 4 5]
arr10: [300 2 3]
arr11: [300 2 3 4 5]
arr12: [300 2 3]

$ go version
go version go1.13 linux/amd64

实验总结

|----------------------------------------------------------------------------|
|                                                                           |
| Modify3  先改变slice元素,再增加长度    ---- 原slice元素改变,新slice改变        |
| Modify4  先改变slice元素,再缩小长度    ---- 原slice元素改变,新slice改变        |
|                                                                           |
| Modify5  先增加长度,再改变新slice元素值 ---- 原slice元素不改变,新slice改变     |
| Modify6  先增加长度,再改变原slice元素值 ---- 原slice元素改变,新slice不改变     |
|                                                                           |
| Modify7  先缩小长度,再改变原slice元素值 ---- 原slice元素改变,新slice改变       |
| Modify8  先缩小长度,再改变新slice元素值 ---- 原slice元素改变,新slice改变       |
|                                                                           |
|---------------------------------------------------------------------------|

原slice append 扩容后,原始slice与新slice脱离关系
原slice 缩小后,原始slice与新slice保持关系

实验结论

当函数内部发生slice扩容后,会导致底层数组改变,就不会影响外部作用域的底层数组,经以上代码证明是正确的

但是当函数内部发生slice发生减少的时候,则不会导致底层数组改变,会影响外部作用域的底层数组

感谢楼上指出我的错误

剩余疑问

我全文搜索go源码文件,没有发现3楼的slice结构定义,发现没有找到

// 3楼的slice结构定义
type _slice struct {
 elements unsafe.Pointer // 引用着底层存储在间接部分上的元素
 len      int            // 长度
 cap      int            // 容量
}

// 我的slice结构定义--位于源码位置/**/go/src/go/types/type.go
type Slice struct {
    Elem *Type // element type
}

我不确定我是否找对地方,还是我们两者的go版本不一致

我的go版本为go version go1.13 linux/amd64

希望对楼主有点帮助

avtion
avtion · #5 · 5年之前

当函数内部发生slice发生减少的时候,则不会导致底层数组改变,会影响外部作用域的底层数组

只有发生扩容才会创建新的底层数组,在Golang里面这个扩容过程有两个不同的逻辑,主要的区别是有没有赋值回原变量,详细可以看这篇文章:Go语言设计与实现 - 3.2.4 追加和扩容

附骚操作:

// 深拷贝的第二种方式 - 触发值赋值
a := []int{1,2,3}
b := append(a[:0:0], a...)

没有找到Slice的定义

Golang没有显性告诉开发者Slice的结构,若是开发者有需求,这可以通过reflect.SliceHeader查看或直接修改

a := []int{1, 2, 3}
b := (*reflect.SliceHeader)(unsafe.Pointer(&a))
avtion
avtion · #6 · 5年之前
xuanwenxuanwen #4 回复

根据楼上3楼的说法,我写了个例子进行测试 ```golang package main import ( "fmt" ) // 先改变slice元素,再增加长度 ---- 原slice元素改变,新slice改变 func Modify3(array []int) []int { array[0] = 300 t1 := append(array, 6) return t1 } // 先改变slice元素,再缩小长度 ---- 原slice元素改变,新slice改变 func Modify4(array []int) []int { array[0] = 300 t1 := array[0:3] return t1 } // 先增加长度,再改变新slice元素值 ---- 原slice元素不改变,新slice改变 func Modify5(array []int) []int { t1 := append(array, 6) t1[0] = 300 return t1 } // 先增加长度,再改变原slice元素值 ---- 原slice元素改变,新slice不改变 func Modify6(array []int) []int { t1 := append(array, 6) array[0] = 300 return t1 } // 先缩小长度,再改变原slice元素值 ---- 原slice元素改变,新slice改变 func Modify7(array []int) []int { t1 := array[0:3] array[0] = 300 return t1 } // 先缩小长度,再改变新slice元素值 ---- 原slice元素改变,新slice改变 func Modify8(array []int) []int { t1 := array[0:3] t1[0] = 300 return t1 } func main() { arr1 := []int{1, 2, 3, 4, 5} arr2 := Modify3(arr1) fmt.Println("arr1:", arr1) // 打印结果:[300 2 3 4 5] fmt.Println("arr2:", arr2) // 打印结果:[300 2 3 4 5 6] arr3 := []int{1, 2, 3, 4, 5} arr4 := Modify4(arr3) fmt.Println("arr3:", arr3) // 打印结果: [300 2 3 4 5] fmt.Println("arr4:", arr4) // 打印结果: [300 2 3] arr5 := []int{1, 2, 3, 4, 5} arr6 := Modify5(arr5) fmt.Println("arr5:", arr5) // 打印结果: [1 2 3 4 5] fmt.Println("arr6:", arr6) // 打印结果: [300 2 3 4 5 6] arr7 := []int{1, 2, 3, 4, 5} arr8 := Modify6(arr7) fmt.Println("arr7:", arr7) // 打印结果: [300 2 3 4 5] fmt.Println("arr8:", arr8) // 打印结果: [1 2 3 4 5 6] arr9 := []int{1, 2, 3, 4, 5} arr10 := Modify7(arr9) fmt.Println("arr9:", arr9) // 打印结果: [300 2 3 4 5] fmt.Println("arr10:", arr10) // 打印结果: [300 2 3] arr11 := []int{1, 2, 3, 4, 5} arr12 := Modify8(arr11) fmt.Println("arr11:", arr11) // 打印结果: [300 2 3 4 5] fmt.Println("arr12:", arr12) // 打印结果: [300 2 3] } ``` ```golang 输出结果: $ go run main.go arr1: [300 2 3 4 5] arr2: [300 2 3 4 5 6] arr3: [300 2 3 4 5] arr4: [300 2 3] arr5: [1 2 3 4 5] arr6: [300 2 3 4 5 6] arr7: [300 2 3 4 5] arr8: [1 2 3 4 5 6] arr9: [300 2 3 4 5] arr10: [300 2 3] arr11: [300 2 3 4 5] arr12: [300 2 3] $ go version go version go1.13 linux/amd64 ``` #### 实验总结 ```golang |----------------------------------------------------------------------------| | | | Modify3 先改变slice元素,再增加长度 ---- 原slice元素改变,新slice改变 | | Modify4 先改变slice元素,再缩小长度 ---- 原slice元素改变,新slice改变 | | | | Modify5 先增加长度,再改变新slice元素值 ---- 原slice元素不改变,新slice改变 | | Modify6 先增加长度,再改变原slice元素值 ---- 原slice元素改变,新slice不改变 | | | | Modify7 先缩小长度,再改变原slice元素值 ---- 原slice元素改变,新slice改变 | | Modify8 先缩小长度,再改变新slice元素值 ---- 原slice元素改变,新slice改变 | | | |---------------------------------------------------------------------------| 原slice append 扩容后,原始slice与新slice脱离关系 原slice 缩小后,原始slice与新slice保持关系 ``` #### 实验结论 当函数内部发生slice扩容后,会导致底层数组改变,就不会影响外部作用域的底层数组,经以上代码证明是正确的 但是当函数内部发生slice发生减少的时候,则不会导致底层数组改变,会影响外部作用域的底层数组 感谢楼上指出我的错误 #### 剩余疑问 ```golang 我全文搜索go源码文件,没有发现3楼的slice结构定义,发现没有找到 // 3楼的slice结构定义 type _slice struct { elements unsafe.Pointer // 引用着底层存储在间接部分上的元素 len int // 长度 cap int // 容量 } // 我的slice结构定义--位于源码位置/**/go/src/go/types/type.go type Slice struct { Elem *Type // element type } ``` 我不确定我是否找对地方,还是我们两者的go版本不一致 我的go版本为go version go1.13 linux/amd64 希望对楼主有点帮助

楼上解答一下疑惑

xuanwen
xuanwen · #7 · 5年之前

再次感谢@avtion的解惑 个人平常关注底层理论还是太少、太浅 (厚脸皮不承认自己水平不够)

鉴于楼上的解答,知道了golang的slice的魔幻操作

所以我对这个问题进行了一下拓展,思考除slice外的map,struct,[]struct,[]map的修改,会有什么不同?

先上结论:

array       ----------       传值                  ---- 新的  赋值即可
slice       ----------       传址(append 后切断联系) ---- 新的  使用copy函数
map         ----------       传址                  ---- 新的  for循环深度复制
struct      ----------       传值                  ---- 新的  赋值即可
[]struct    ----------       传址                  ---- 新的  使用copy函数
[]map       ----------       传址                  ---- 新的  使用copy函数后还需要对mapfor循环深度复制

鉴于我的测试代码实在太多,发布在这里排版可能不太好看,所以需要去github看

github地址: https://github.com/GrayMi/goNote/tree/master/0001-go%E8%AF%AD%E8%A8%80%E5%A4%8D%E6%9D%82%E7%B1%BB%E5%9E%8B%E7%9A%84%E4%BC%A0%E5%80%BC%E4%B8%8E%E4%BC%A0%E5%9D%80

如有发现错误或有歧义的地方,请帮忙指出,谢谢!

codeYuanY
codeYuanY · #8 · 5年之前

@xuanwen @avtion谢谢二位的解答,让我感受到初次来到的这个社区的温暖

添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传