golang中数组和切片到底有什么区别?

2023-02-24 15:06:14 浏览数 (1)

数组大家都知道是具有「固定长度及类型的序列集合」,但是golang中又引入了「切片」,语法上看起来还和数组差不多,为什么会引入这些呢?切片和数组到底有什么区别呢?接下来咱们来一个一个的看

数组 array

定义数组

代码语言:javascript复制
var arr [5]int = [5]int{1,2,3,4,5}

上述语句表示的意思是,我们来定义一个变量arr 为5个长度的int的数组类型,也就是[5]int,同时给赋值上了初始值 1、2、3、4、5,内存分布如图

紧密挨在一起的数据结构集合

注意

如果定义数组的方法是

代码语言:javascript复制
arr := new([4]int)

那么arr的数据类型为*[4]int,而不是[4]int

不定长数组

当然数组的长度4如果是不固定的,可以用...的方式代替

代码语言:javascript复制
q := [...]int{1, 2, 3}

数组的循环

数组的循环在golang中有一个特有的语法,就是 for range

代码语言:javascript复制
  var arr [4]int = [4]int{1, 2, 3, 4}
 for i, v := range arr {
  fmt.Printf("数组中的第%v项, 值是%vn", i, v)
 }
  //输出结果
  数组中的第0项, 值是1
  数组中的第1项, 值是2
  数组中的第2项, 值是3
  数组中的第3项, 值是4

数组的常用方法

常用方法是「len()」 方法和 「cap()」 方法

  • len()方法的作用是获取数组或者切片的「长度」
  • cap()方法的作用是获取数组或者切片的「容量」

但是「在数组中,这两个值永远相同」,所以在这里咱们不多做考虑,在后面切片中再详细阐述。

切片 slice

为什么会有切片?

切片之所以会诞生,是因为golang中数组存在很大的两个问题

  • 固定的长度,这意味着初始化 array 后,不能再 push 超过 len(array) 长度的元素
  • array 作为参数在函数之间传递时是值传递,相当于把数据copy了一份,具有很大的性能浪费

切片数据类型的底层结构

代码语言:javascript复制
type slice struct {
    array unsafe.Pointer  //指向一个数组的指针
    len int  //当前 slice 的长度
    cap int  //当前 slice 的容量
}

比如我们定义了一个切片

代码语言:javascript复制
  s := make([]int, 3, 5)
 s[0] = 1
 s[1] = 2
 s[2] = 3

那么以上变量在内存中的数据结构如下图所示

所以由上面的分析可以看出来,「切片是依赖于数组的,而且是一个指向数组的指针」,既然切片是指针类型,那么在作为参数传递的时候,肯定是引用类型,不需要重新copy一份而造成空间浪费。

slice 的截取

我们上面说过切片是依赖于数组的,所以切片的截取是基于数组进行截取的,截取这块我们直接看例子就行,看例子记住一个原则即可「左包含,右不包含」

代码语言:javascript复制
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
s4 := a1[2:4] //输出结果[3 4]  
s5 := a1[:4] //输出结果[1 2 3 4]
s6 := a1[2:] //输出结果[3 4 5 6 7 8 9]
s7 := a1[:]  //输出结果[1 2 3 4 5 6 7 8 9]

以上例子都符合上面提到的「左包含,右不包含原则」

  • s4从下标2开始截取,截取到下标4
  • s5省略了第一个参数,表示从下标0开始截取
  • s6省略了第二个参数,表示截取到最后一个元素
  • s7省略了两个参数,只填写了中间的冒号:,表示取全部元素

切片的长度len()和容量cap()

长度很好理解,简单理解就是「元素的个数」,容量相对难理解一些「在切片引用的底层数组中从切片的第一个元素到数组最后一个元素的长度就是切片的容量」

我们还是来直接看例子

代码语言:javascript复制
a1 := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}

s5 := a1[:4] //[1 2 3 4]
s6 := a1[2:] //[3 4 5 6 7 8 9]
s7 := a1[:]  //[1 2 3 4 5 6 7 8 9]

fmt.Printf("len(s5):%d cap(s5):%dn", len(s5), cap(s5)) //4 9
fmt.Printf("len(s6):%d cap(s6):%dn", len(s6), cap(s6)) //7 7
fmt.Printf("len(s7):%d cap(s7):%dn", len(s7), cap(s7)) //9 9
  • a1是数组长度为9,容量也为9,值是从1~9
  • s5/s6/s7都是切割数组a1得到的切片。
  • s5的长度为4,因为只有1 2 3 4这4个元素,容量为9,因为s5切片是从数组起始位置开始切割的:第一个元素是1,而s5底层数组a1最后一个元素是9,1~9共9个元素,所以s5的容量为9。
  • s6的长度为7,因为s6的元素是39这7个元素;容量也为7,因为s5的底层数组最后一个元素是9,39共7个元素,所以s6的容量为7。
  • s7更好理解了,长度和容量都是9,大家自己理解一下。

切片的常用方法

make

make方法主要是用于切片的生成,比较简单,比如下面的例子就是我们来定义一个长度为5,容量为10的切片。

代码语言:javascript复制
s1 := make([]int,5,10)
fmt.Printf("s1:%v len(s1):%d cap(s1):%dn", s1, len(s1), cap(s1))
// 输出结果
//s1:[0 0 0 0 0] len(s1):5 cap(s1):10
append

append主要是用于切片的追加。我们还是直接看例子

代码语言:javascript复制
  var s = []int{1, 2, 3, 4}
 fmt.Println(s)
 fmt.Printf("len:%d, cap:%d", len(s), cap(s))
  //输出结果
  [1 2 3 4]
  len:4, cap:4

我们可以看到定义了一个切片,初始化了4个元素,切片此时的长度和容量都为4

代码语言:javascript复制
  var s = []int{1, 2, 3, 4}
 s = append(s, 5)  //给切片s追加一个元素 5
 fmt.Println(s)
 fmt.Printf("len:%d, cap:%dn", len(s), cap(s))
  //输出结果
  [1 2 3 4 5]
  len:5, cap:8

分析:长度由4变成5,我们很好理解;容量为什么会从4变成8呢?「这个是因为go语言对切片的自动扩容机制,append追加,如果cap不够的时候,go底层会把底层数组替换,是go语言的一套扩容策略。」 简单说这个扩容机制就是「如果不够,就在以前的基础上翻倍,如果超过1M,则 1M」,跟redis的bitmap类型的扩容机制是一样的

slice 扩容的"坑"
代码语言:javascript复制
func main() {
 var s = []int{1, 2, 3}
 modifySlice(s)
 fmt.Println(s) // 打印 [1 2 3]
}

func modifySlice(s []int) {
 s = append(s, 4)
 s[0] = 4
}

这个坑在面试中经常会遇到,当 slice 作为函数参数时,「如果在函数内部发生了扩容,这时再修改 slice 中的值是不起作用的」,因为修改发生在新的 array 内存中,对老的 array 内存不起作用。

如何追加多个元素
代码语言:javascript复制
  s1 := []int{1, 2, 3, 4}
 s2 := []int{5, 6}
 s3 := append(s1, s2...)  // ...表示拆开,将切片的值作为追加的元素
 fmt.Println(s3)
  //输出结果
  //[1 2 3 4 5 6]
copy
代码语言:javascript复制
//定义切片s1
s1 := []int{1, 2, 3}

//第一种方式:直接声明变量 用=赋值
//s2切片和s1引用同一个内存地址
var s2 = s1

//第二种方式:copy
var s3 = make([]int, 3)
copy(s3, s1)            //使用copy函数将 参数2的元素复制到参数1

s1[0] = 11
fmt.Printf("s1:%v s2:%v s3:%v",s1, s2, s3) //s1和s2是[11 2 3] s3是[1 2 3]

我们发现s1和s2是[11 2 3] s3是[1 2 3],说明copy方法是复制了一份,开辟了新的内存空间,不再引用s1的内存地址,这就是两者的区别。

0 人点赞