go语言中切片和数组是什么

在go语言中,数组是一个由固定长度的特定类型元素组成的序列,是同一种数据类型元素的集合,一个数组可以由零个或多个元素组成。和数组对应的类型是Slice(切片),切片是对数组的一个连续片段的引用,所以切片是一个引用类型,这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

go语言中切片和数组是什么

本教程操作环境:windows7系统、GO 1.18版本、Dell G3电脑。

一、数组

数组是同一种数据类型元素的集合。 在Go语言中,数组从声明时就确定,使用时可以修改数组成员,但是数组大小不可变化。 基本语法:

// 定义一个长度为3元素类型为int的数组a var a [3]int
登录后复制

数组的长度必须是常量,并且长度是数组类型的一部分。一旦定义,长度不能变

1、数组的初始化

(1)方法一

 var testArray [3]int               // 定义数组时,会初始化int类型为零值  var cityArray = [3]string{"北京", "上海", "深圳"} // 使用指定的初始值完成初始化
登录后复制

(2)方法二

一般情况下我们可以让编译器根据初始值的个数自行推断数组的长度

var cityArray = [...]string{"北京", "上海", "深圳"}
登录后复制

(3)方法三

我们还可以使用指定索引值的方式来初始化数组,例如:

func main() {  a := [...]int{1: 1, 3: 5}  fmt.Println(a)                  // [0 1 0 5]  fmt.Printf("type of a:%Tn", a) //type of a:[4]int }
登录后复制

2、数组的遍历

func main() {  var a = [...]string{"北京", "上海", "深圳"}  // 方法1:for循环遍历  for i := 0; i < len(a); i++ {   fmt.Println(a[i])  }   // 方法2:for range遍历  for index, value := range a {   fmt.Println(index, value)  } }
登录后复制

3、多维数组

Go语言是支持多维数组的,我们这里以二维数组为例(数组中又嵌套数组)。

(1)二维数组的定义

func main() {  a := [3][2]string{   {"北京", "上海"},   {"广州", "深圳"},   {"成都", "重庆"},  }  fmt.Println(a) //[[北京 上海] [广州 深圳] [成都 重庆]]  fmt.Println(a[2][1]) //支持索引取值:重庆 }
登录后复制

(2)二维数组的遍历

func main() {  a := [3][2]string{   {"北京", "上海"},   {"广州", "深圳"},   {"成都", "重庆"},  }  for _, v1 := range a {   for _, v2 := range v1 {    fmt.Printf("%st", v2)   }   fmt.Println()  } }
登录后复制

注意: 多维数组只有第一层可以使用...来让编译器推导数组长度。例如:

a := [...][2]string{  {"北京", "上海"},  {"广州", "深圳"},  {"成都", "重庆"}, }
登录后复制

4、数组是值类型

数组是值类型,赋值和传参会复制整个数组。因此改变副本的值,不会改变本身的值。

func modifyArray(x [3]int) {  x[0] = 100 }  func modifyArray2(x [3][2]int) {  x[2][0] = 100 } func main() {  a := [3]int{10, 20, 30}  modifyArray(a) //在modify中修改的是a的副本x  fmt.Println(a) //[10 20 30]  b := [3][2]int{   {1, 1},   {1, 1},   {1, 1},  }  modifyArray2(b) //在modify中修改的是b的副本x  fmt.Println(b)  //[[1 1] [1 1] [1 1]] }
登录后复制

注意:

  • 数组支持 “==“、”!=” 操作符,因为内存总是被初始化过的。
  • [n]*T表示指针数组(这是一个数组,里面元素是一个个的指针)
  • *[n]T表示数组指针 (这是一个指针,存的是一个数组的内存地址)

二、切片

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容

切片是一个 引用类型,它的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。

切片(slice)是对数组的一个连续片段的引用,所以切片是一个引用类型(因此更类似于 C/C++ 中的数组类型,或者 Python 中的 list 类型),这个片段可以是整个数组,也可以是由起始和终止索引标识的一些项的子集,需要注意的是,终止索引标识的项不包括在切片内。

Go语言中切片的内部结构包含地址、大小和容量,切片一般用于快速地操作一块数据集合,如果将数据集合比作切糕的话,切片就是你要的“那一块”,切的过程包含从哪里开始(切片的起始位置)及切多大(切片的大小),容量可以理解为装切片的口袋大小。

1、切片的定义

声明切片类型的基本语法如下:

var name []T  // name:表示变量名 // T:表示切片中的元素类型
登录后复制

举个栗子:

func main() {  // 声明切片类型  var a []string              //声明一个字符串切片  var b = []int{}             //声明一个整型切片并初始化  var c = []bool{false, true} //声明一个布尔切片并初始化  var d = []bool{false, true} //声明一个布尔切片并初始化  fmt.Println(a == nil)       //true  fmt.Println(b == nil)       //false  fmt.Println(c == nil)       //false  // fmt.Println(c == d)   //切片是引用类型,不支持直接比较,只能和nil比较 }
登录后复制

2、切片的长度和容量

切片拥有自己的长度和容量,我们可以通过使用内置的len()函数求长度,使用内置的cap()函数求切片的容量。

3、切片表达式

切片表达式从字符串、数组、指向数组或切片的指针构造子字符串或切片。它有两种变体:一种指定low和high两个索引界限值的简单的形式,另一种是除了low和high索引界限值外还指定容量的完整的形式

完整切片表达式没啥用,这里只讲简单切片表达式!

// 简单切片表达式 func main() {  a := [5]int{1, 2, 3, 4, 5}  s := a[1:3]  // s := a[low:high]  fmt.Printf("s:%v len(s):%v cap(s):%vn", s, len(s), cap(s)) }
登录后复制

运行结果:

s:[2 3] len(s):2 cap(s):4
登录后复制

(1)使用make()函数构造切片

我们上面都是基于数组来创建的切片,如果需要动态的创建一个切片,我们就需要使用内置的make()函数,格式如下:

make([]T, size, cap)
登录后复制

  • T:切片的元素类型
  • size:切片中元素的数量
  • cap:切片的容量

举个栗子:

func main() {  a := make([]int, 2, 10)  fmt.Println(a)      //[0 0]  fmt.Println(len(a)) //2  fmt.Println(cap(a)) //10 }
登录后复制

上面代码中a的内部存储空间已经分配了10个,但实际上只用了2个。 容量并不会影响当前元素的个数,所以len(a)返回2,cap(a)则返回该切片的容量。

(2)切片的本质

切片自己不拥有任何数据。它只是底层数组的一种表示。对切片所做的任何修改都会反映在底层数组中

切片的本质 就是对底层数组的封装,它包含了三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)

举个例子,现在有一个数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相应示意图如下。

go语言中切片和数组是什么

切片s2 := a[3:6],相应示意图如下:

go语言中切片和数组是什么

如果你懂了切片的本质,那么试试下面这个题吧!

func main() {  a := [5]int{1, 2, 3, 4, 5}  s := a[1:3]  // s := a[low:high]  fmt.Printf("s:%v len(s):%v cap(s):%vn", s, len(s), cap(s))  s2 := s[3:4]  // 索引的上限是cap(s)而不是len(s),可能认为cap是2?切片是从原数组中元素2开始切走的  fmt.Printf("s2:%v len(s2):%v cap(s2):%vn", s2, len(s2), cap(s2)) }
登录后复制

运行结果:

s:[2 3] len(s):2 cap(s):4 s2:[5] len(s2):1 cap(s2):1
登录后复制

s2什么鬼?[2 3][3:4]这个能运行?如果有这样的疑惑,说明你并没有认识到切片的本质,下面我们来看一个图:

注意切片的本质是一个指向底层数组的起点的指针切片len有效长度,以及cap容量

go语言中切片和数组是什么

上面是切片s生成的过程,现在又要切片取[3:4],从s的起点开始数,我们可以很容易看出来[3:4]是5。

(3)切片不能直接比较

切片之间是不能比较的,我们不能使用==操作符来判断两个切片是否含有全部相等元素。 切片唯一合法的比较操作是和nil比较。 一个nil值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil,例如下面的示例:

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil
登录后复制

所以要判断一个切片是否是空的,要是用len(s) == 0来判断,不应该使用s == nil来判断。

注意:nil和空不是一个概念,nil的判断是有无底层数组,s2、s3初始化了的,其实是有底层数组的,s1只是声明,因此没有底层数组为nil。是否为空,则len是否为0为唯一判断条件。

(4)切片的赋值拷贝

下面的代码中演示了拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

func main() {  s1 := make([]int, 3) //[0 0 0]  s2 := s1             //将s1直接赋值给s2,s1和s2共用一个底层数组  s2[0] = 100  fmt.Println(s1) //[100 0 0]  fmt.Println(s2) //[100 0 0] }
登录后复制

(5)切片遍历

切片的遍历方式和数组是一致的,支持索引遍历for range遍历。

func main() {  s := []int{1, 3, 5}   for i := 0; i < len(s); i++ {   fmt.Println(i, s[i])  }   for index, value := range s {   fmt.Println(index, value)  } }
登录后复制

(6)append()方法为切片添加元素

Go语言的内建函数append()可以为切片动态添加元素。 可以一次添加一个元素,可以添加多个元素,也可以添加另一个切片中的元素(后面加…)。

func main(){  var s []int  s = append(s, 1)        // [1]    s = append(s, 2, 3, 4)  // [1 2 3 4]    s2 := []int{5, 6, 7}    s = append(s, s2...)    // [1 2 3 4 5 6 7] } // 这个...类似于python中的*args打散列表
登录后复制

注意: 通过var声明的零值切片可以在append()函数直接使用,无需初始化。

var s []int s = append(s, 1, 2, 3)
登录后复制

没有必要像下面的代码一样初始化一个切片再传入append()函数使用

s := []int{}  // 没有必要初始化 s = append(s, 1, 2, 3)  var s = make([]int)  // 没有必要初始化 s = append(s, 1, 2, 3)
登录后复制

每个切片会指向一个底层数组,这个数组的容量够用就添加新增元素。当底层数组不能容纳新增的元素时,切片就会自动按照一定的策略进行“扩容”,此时该切片指向的底层数组就会更换。“扩容”操作往往发生在append()函数调用时,所以我们通常都需要用原变量接收append函数的返回值

(7)切片的扩容策略

可以通过查看$GOROOT/src/runtime/slice.go源码,其中扩容相关代码如下:

newcap := old.cap doublecap := newcap + newcap if cap > doublecap {  newcap = cap } else {  if old.len < 1024 {   newcap = doublecap  } else {   // Check 0 < newcap to detect overflow   // and prevent an infinite loop.   for 0 < newcap && newcap < cap {    newcap += newcap / 4   }   // Set newcap to the requested cap when   // the newcap calculation overflowed.   if newcap <= 0 {    newcap = cap   }  } }
登录后复制

go语言中切片和数组是什么

(8) 使用copy()函数复制切片

func main() {  a := []int{1, 2, 3, 4, 5}  b := a  fmt.Println(a) //[1 2 3 4 5]  fmt.Println(b) //[1 2 3 4 5]  b[0] = 1000  fmt.Println(a) //[1000 2 3 4 5]  fmt.Println(b) //[1000 2 3 4 5] }
登录后复制

由于切片是引用类型,所以a和b其实都指向了同一块内存地址。修改b的同时a的值也会发生变化

Go语言内建的copy()函数可以迅速地将一个切片的数据复制到另外一个切片空间中,copy()函数的使用方法如下:

func main() {  // copy()复制切片  a := []int{1, 2, 3, 4, 5}  c := make([]int, 5, 5)  copy(c, a)     //使用copy()函数将切片a中的元素复制到切片c  fmt.Println(a) //[1 2 3 4 5]  fmt.Println(c) //[1 2 3 4 5]  c[0] = 1000  fmt.Println(a) //[1 2 3 4 5]  fmt.Println(c) //[1000 2 3 4 5] // 再对切片c操作,就不会影响a了 }
登录后复制

(9)从切片中删除元素

Go语言中并没有删除切片元素的专用方法,我们可以使用切片本身的特性来删除元素。 代码如下:

func main() {  // 从切片中删除元素  a := []int{30, 31, 32, 33, 34, 35, 36, 37}  // 要删除索引为2的元素  a = append(a[:2], a[3:]...) // 把index=2之后的切片和index=2之前的切片拼接在一起  fmt.Println(a) //[30 31 33 34 35 36 37] }
登录后复制

切片a中删除索引为index的元素,操作方法是a = append(a[:index], a[index+1:]...)

(10)内存优化

切片持有对底层数组的引用。只要切片在内存中,数组就不能被垃圾回收。在内存管理方面,这是需要注意的。让我们假设我们有一个非常大的数组,我们只想处理它的一小部分。然后,我们由这个数组创建一个切片,并开始处理切片。这里需要重点注意的是,在切片引用时数组仍然存在内存中。

一种解决方法是使用上面的copy函数,根据切片生成一个一模一样的新切片。这样我们可以使用新的切片,原始数组可以被垃圾回收。

package mainimport (     "fmt")func countries() []string {     a := []string{1, 2, 3, 4, 5}     b := a[:len(a)-2]     c := make([]string, len(b))     copy(c, b) // 将b的内容copy给c     return c}func main() {     d := countries()     fmt.Println(d)  }
登录后复制

b := a[:len(a)-2] 创建一个去掉a的尾部 2 个元素的切片 b,在上述程序的 11 行,将 切片b 复制到 切片c。同时在函数的下一行返回 切片c。现在 a 数组可以被垃圾回收, 因为数组a不再被引用。

三、切片与数组的区别

Go 数组与像 C/C++等语言中数组略有不同:

1. Go 中的数组是值类型,换句话说,如果你将一个数组赋值给另外一个数组,那么,实际上就是将整个数组拷贝一份。因此,在 Go 中如果将数组作为函数的参数传递的话,那效率就肯定没有传递指针高了。

2. 数组的长度也是类型的一部分,这就说明[10]int和[20]int不是同一种数据类型。并且Go 语言中数组的长度是固定的,且不同长度的数组是不同类型,这样的限制带来不少局限性。

3. 而切片则不同,切片(slice)是一个拥有相同类型元素的可变长序列,可以方便地进行扩容和传递,实际使用时比数组更加灵活,这也正是切片存在的意义。而且切片是引用类型,因此在当传递切片时将引用同一指针,修改值将会影响其他的对象。

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发