关于切片参数传递的问题

前言:在 Golang 中函数之间传递变量时总是以值的方式传递的,无论是 int,string,bool,array 这样的内置类型(或者说原始的类型),还是 slice,channel,map 这样的引用类型,在函数间传递变量时,都是以值的方式传递,也就是说传递的都是值的副本。

在使用ioutil的ReadAll方法时查看了其内部实现如下,这让我很痛苦,不明白为什么要这样写。下面我们就来一探究竟。

func ReadAll(r Reader) ([]byte, error) {
  b := make([]byte, 0, 512)
  for {
    if len(b) == cap(b) {
      // Add more capacity (let append pick how much).
      b = append(b, 0)[:len(b)]
    }
    n, err := r.Read(b[len(b):cap(b)])
    b = b[:len(b)+n]
    if err != nil {
      if err == EOF {
        err = nil
      }
      return b, err
    }
  }
}

讨论这个问题之前先看一下标准库中切片的内部结构

type SliceHeader struct {
    Data uintptr
    Len int
    Cap int
}

由切片的结构定义可知,切片的结构由三个信息组成:

  1. 指针Data,指向底层数组中切片指定的开始位置
  2. 长度Len,即切片的长度
  3. 容量Cap,也就是最大长度,即切片开始位置到数组的最后位置的长度

最开始我想将一个文件内容读取到内存,我想到的操作是这样的

func f1() {
  f, _ := os.Open("F:\\hello.txt")
  b := make([]byte, 0, 512)

  read, err := f.Read(b)
  if err != nil {
    return
  }
  fmt.Println(read)//打印0
  fmt.Println(b)//打印[]
}

为什么会出现这种情况呢?我们点开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)] 了)。观察下面代码:

func f2() {
  f, _ := os.Open("F:\\hello.txt")
  b := make([]byte, 0, 512)
  //
  c := b[len(b):cap(b)]
  
  fmt.Println(len(c))//512
  fmt.Println(cap(c))//512
  
  read, err := f.Read(c)

  if err != nil {
    return
  }
  
  fmt.Println(read)//512
  fmt.Println(b)//[]
  fmt.Println(b[:cap(b)])//[...] 打印出了数据
  fmt.Println(c)//[...]打印出了和上面相同的数据
}

这就奇怪了不是说是引用传递吗,为什么现在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去除。

func f3() {
  f, _ := os.Open("F:\\hello.txt")
  b := make([]byte, 0, 512)
  for {
    if len(b) == cap(b) {
      // Add more capacity (let append pick how much).
      //b = append(b, 0)[:len(b)]
      b = append(b, 0)
    }
    n, err := f.Read(b[len(b):cap(b)])

    b = b[:len(b)+n]
    if err != nil {
      if err == io.EOF {
        err = nil
      }
      break
    }
  }
  fmt.Println(string(b))
}

可以看到使用b = append(b, 0) 会导致部分数据失真。

热门相关:超武穿梭   豪门重生盛世闲女   豪门重生盛世闲女   夫人你马甲又掉了   夫人,你马甲又掉了!