关于切片参数传递的问题

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了关于切片参数传递的问题相关的知识,希望对你有一定的参考价值。

前言:在 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) 会导致部分数据失真。

golang中不定参数与数组切片的区别

package main

import "fmt"

func main() {
    myfunc1(88, 42, 12, 56) //传递不定数量的参数
    myfunc2([]int{88, 42, 12, 56}) //传递一个数组切片
}

func myfunc1(args ...int) {    //接受不定数量的参数,这些参数的类型全部是int
    for _, arg := range args {
        fmt.Println(arg)
    }
}

func myfunc2(args []int) {    //传递一个数组切片
    for _, arg := range args {
        fmt.Println(arg)
    }
}

参考资料:

《Go语言编程》 2.5.3 不定参数

以上是关于关于切片参数传递的问题的主要内容,如果未能解决你的问题,请参考以下文章

golang中不定参数与数组切片的区别

Go-常识补充-切片-map(类似字典)-字符串-指针-结构体

golang切片详解

go语言切片作为函数参数的研究

Python定义可变参数与list切片

关于php的参数传递问题