关于切片参数传递的问题

打印 上一主题 下一主题

主题 911|帖子 911|积分 2733

前言:在 Golang 中函数之间传递变量时总是以值的方式传递的,无论是 int,string,bool,array 这样的内置类型(或者说原始的类型),还是 slice,channel,map 这样的引用类型,在函数间传递变量时,都是以值的方式传递,也就是说传递的都是值的副本。
在使用ioutil的ReadAll方法时查看了其内部实现如下,这让我很痛苦,不明白为什么要这样写。下面我们就来一探究竟。
  1. func ReadAll(r Reader) ([]byte, error) {
  2.   b := make([]byte, 0, 512)
  3.   for {
  4.     if len(b) == cap(b) {
  5.       // Add more capacity (let append pick how much).
  6.       b = append(b, 0)[:len(b)]
  7.     }
  8.     n, err := r.Read(b[len(b):cap(b)])
  9.     b = b[:len(b)+n]
  10.     if err != nil {
  11.       if err == EOF {
  12.         err = nil
  13.       }
  14.       return b, err
  15.     }
  16.   }
  17. }
复制代码
讨论这个问题之前先看一下标准库中切片的内部结构
  1. type SliceHeader struct {
  2.     Data uintptr
  3.     Len int
  4.     Cap int
  5. }
复制代码
由切片的结构定义可知,切片的结构由三个信息组成:

  • 指针Data,指向底层数组中切片指定的开始位置
  • 长度Len,即切片的长度
  • 容量Cap,也就是最大长度,即切片开始位置到数组的最后位置的长度
最开始我想将一个文件内容读取到内存,我想到的操作是这样的
  1. func f1() {
  2.   f, _ := os.Open("F:\\hello.txt")
  3.   b := make([]byte, 0, 512)
  4.   read, err := f.Read(b)
  5.   if err != nil {
  6.     return
  7.   }
  8.   fmt.Println(read)//打印0
  9.   fmt.Println(b)//打印[]
  10. }
复制代码
为什么会出现这种情况呢?我们点开f.Read方法看到 Read reads up to len(b) bytes from the File. 读取len(b) 长度byte的数据到b,那现在len(b)=0就一个字节都不会读取了。这时候你就会明白为什么上面标准库中ReadAll参数为什么要用b[len(b):cap(b)] (对切片的任何操作都会复制一个切片b[len(b):cap(b)] 操作对b切片结构体进行了复制,产生了新的切片并且新切片的len=cap=512,这也就解释了为什么数据能读入b[len(b):cap(b)] 了)。观察下面代码:
  1. func f2() {
  2.   f, _ := os.Open("F:\\hello.txt")
  3.   b := make([]byte, 0, 512)
  4.   //
  5.   c := b[len(b):cap(b)]
  6.   
  7.   fmt.Println(len(c))//512
  8.   fmt.Println(cap(c))//512
  9.   
  10.   read, err := f.Read(c)
  11.   if err != nil {
  12.     return
  13.   }
  14.   
  15.   fmt.Println(read)//512
  16.   fmt.Println(b)//[]
  17.   fmt.Println(b[:cap(b)])//[...] 打印出了数据
  18.   fmt.Println(c)//[...]打印出了和上面相同的数据
  19. }
复制代码
这就奇怪了不是说是引用传递吗,为什么现在c作为参数传进Read方法后值被改变了。这就需要看切片的内部结构了,切片本身并不承载数据。它只是一个有三个属性的结构体,传递时,就会把这个结构体的三个属性复制一份进行传递,而且复制后头指针指向相同的地址。另外还有一个重要的概念:对切片的任何操作都会复制一个切片(并不是复制切片数据,二十切片的结构体,他们指向的内存区域还是一样的),也就是复制上面说的三个属性。读取切片类型数据的另一个重要属性就是len,len是多少那就会读多少数据,虽然由b衍生出的其他结构体他们的头指针的地址是一样的,后面的数据也是一样的,但是如果你的len是0那头指针后面的数据一个byte也不属于你,也就读不出来,你有多少的len那么头指针后就有多少数据属于你。
这也就解释了为什么b始终是空的了,虽然你的头指针后面有数据被填充了,但是你的len始终是0那么数据都与你无关也是就是空了。c切片的头指针与b相同但是len和cap不同都是512。所以就能读取出头指针后512bytes的数据了。
另外还要讨论切片的扩容机制,当切片的len=cap时使用append方法会触发内置的扩容机制cap会扩大。我就有些疑问为什么是b = append(b, 0)[:len(b)] ,因为使用append函数仅仅是为了触发扩容,添加进去的0是无意义的,原来len=512现在就变成了513,再往后填充数据就会导致与原数据不一致的问题,因此要把添加的byte去除。
  1. func f3() {
  2.   f, _ := os.Open("F:\\hello.txt")
  3.   b := make([]byte, 0, 512)
  4.   for {
  5.     if len(b) == cap(b) {
  6.       // Add more capacity (let append pick how much).
  7.       //b = append(b, 0)[:len(b)]
  8.       b = append(b, 0)
  9.     }
  10.     n, err := f.Read(b[len(b):cap(b)])
  11.     b = b[:len(b)+n]
  12.     if err != nil {
  13.       if err == io.EOF {
  14.         err = nil
  15.       }
  16.       break
  17.     }
  18.   }
  19.   fmt.Println(string(b))
  20. }
复制代码
可以看到使用b = append(b, 0) 会导致部分数据失真。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

盛世宏图

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表