前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go数组和切片

Go数组和切片

作者头像
一行舟
发布2022-08-25 13:55:27
3090
发布2022-08-25 13:55:27
举报
文章被收录于专栏:一行舟

引言:本文主要介绍Go语言数组和切片的基本概念,常用方法和使用时的注意事项。

数组

数组声明

代码语言:javascript
复制
package main

import "fmt"

func main()  {
   var a [5]int // 声明长度为5的整形数组
   var b [3]int = [3]int{1,2,3} //声明长度为3的整形数组,并初始化
   c := [3]int{1,2,3} // 声明一个数组,并赋值
   d := [...]int{1,2,3} // 声明一个数组, [...]代表数组的长度,由数组初始化时存放的元素个数决定
   e := [...]int{3:11,12,13} // 声明并初始化一个数组,前三位是默认值,后面三位的值是11,12,13

   fmt.Printf("a = %+d \n", a)
   fmt.Printf("b = %+d \n", b)
   fmt.Printf("c = %+d \n", c)
   fmt.Printf("d = %+d \n", d)
   fmt.Printf("e = %+d \n", e)
}
输出结果:
a = [+0 +0 +0 +0 +0] 
b = [+1 +2 +3] 
c = [+1 +2 +3] 
d = [+1 +2 +3] 
e = [+0 +0 +0 +11 +12 +13]

数组是定长的,长度也是数组属性的一部分,声明之后就不再改变。

数组是否可比较,取决于数组中的元素是否可以比较。如果数组中的元素可以比较我们可以直接通过==操作符来比较两个数组是否相等。

我们可以通过len方法,获取一个数组的长度。

数组是值类型,作为函数的参数传递时会拷贝整个数组。所以我们往往不直接使用大数组作为函数参数,而是使用*取址操作符,把数组的地址作为参数传递,以此来减少数组拷贝带来的性能开销。

Go语言函数的传参方式不存在传值还是传引用的问题,在Go语言看传递的是值还是引用,就看参数自身是数值类型还是引用类型就可以了。

画外音:Go语言中的引用类型有:切片、字典、通道、函数等。

切片(slice)

slice是Go语言中特有的数据结构。一个切片底层必然依赖某一个数组,是这个数组连续片段的引用。但是切片比数组更加灵活,可以随着切片元素增加进行自动扩容

切片声明:

上文中所有初始化数组的方式,把[]中间长度去掉,都可以用来声明切片。除了上文中的方式,切片还有以下声明方式:

代码语言:javascript
复制
package main

import "fmt"

func main()  {

   arr := [5]int{1,2,3,4,5}
   // 通过数组初始化一个切片
   a := arr[1:3] //指定一个数组下标中一个左闭又开的区间作为切片的初始值
   b := arr[:3]  //省略:左侧的数值,左侧的默认是为0
   c := arr[1:]  //省略:右侧的数值,右侧的默认值是数组的长度
   d := arr[:]   // :左右两侧的数值都省略,默认复制整个数组的值给切片

   // 使用make初始化切片
   e := make([]int,5,10)  //初始化一个长度是5,容量是10的切片
   f := make([]int,5)  //初始化一个长度是5的切片,当不指定容量时,容量默认等于切片的长度


   fmt.Printf("a = %+d \n", a)
   fmt.Printf("b = %+d \n", b)
   fmt.Printf("c = %+d \n", c)
   fmt.Printf("d = %+d \n", d)
   fmt.Printf("e = %+d \n", e)
   fmt.Printf("f = %+d \n", f)


}

切片有长度和容量两个属性,长度代表切片当前存放元素的数量,容量代表切片当前最多可存放元素的数量。往切片内插入的元素超过其容量时,切片会发生扩容。

切片相关的方法:len()和cap()分别用来获取切片的长度和容量

代码语言:javascript
复制
package main

import "fmt"

func main()  {

   // 使用make初始化切片
   a := make([]int,5,10)
   fmt.Printf("len = %+d \n", len(a))
   fmt.Printf("cap = %+d \n", cap(a))
}
输出结果:
len = +5 
cap = +10

append():往切片末尾追加元素 copy();把一个切片的值拷贝到另一个切片

代码语言:javascript
复制
package main

import (
   "fmt"
)

func main()  {

   s1 := make([]int,5,10)
   s1 = append(s1, 9,10,11) // 往s1中追加 9,10,11 三个元素

   s2 := make([]int,len(s1),10) // 在指定s2的长度时候,默认和s1相同,如果不相同的话会是什么样的呢?
   copy(s2, s1) // 把s1中的值拷贝一份到s2

   fmt.Printf("s1 = %+d \n", s1)
   fmt.Printf("s2 = %+d \n", s2)

   s1 = append(s1, 12,13,14) //继续往s1中添加元素,并超过切片原始的容量10
   fmt.Printf("len(s1) = %+d \n", len(s1))
   fmt.Printf("cap(s1) = %+d \n", cap(s1))

}
输出结果:
s1 = [+0 +0 +0 +0 +0 +9 +10 +11] 
s2 = [+0 +0 +0 +0 +0 +9 +10 +11] 
len(s1) = +11 
cap(s1) = +20

copy(s2,s1)把s1中元素的值复制给s2,如果s2的长度小于s1,则只拷贝s2长度内的元素。连续往s1添加元素,添加元素数量超过s1的容量时,切片发生了扩容。切片扩容的动作是不改变原有的切片,而是生成一个容量更大的切片,把现有的元素和新的元素一起拷贝到新切片中。

切片扩容的策略: 1.如果新申请容量大于当前容量的两倍数,则使用新申请的容量。 2.如果新申请容量小于等于当前容量两倍,当前容量如果小于1024,则新容量变为当前容量的两倍;如果当前容量大于1024,则新增当前容量的1.25倍,和新申请的容量对比,直到大于新的容量。

代码语言:javascript
复制
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
   newcap = cap
} else {
   if old.cap < 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
      }
   }
}

我们验证下切片扩容过程中,申请了新的切片,而且老的切片不变。

代码语言:javascript
复制
package main

import (
   "fmt"
   "unsafe"
)

func main()  {

   s1 := []int{1,2,3,4,5}
   ptr := unsafe.Pointer(&s1[0]) // unsafe.Pointer方法 用来获取变量所在的内存地址
   fmt.Printf("s1第一元素一开始的地址 = %+d \n", ptr)

   s1 = append(s1, 6,7)
   s1[0] = 100
   fmt.Printf("s1扩容后第一个元素的地址 = %+d \n", unsafe.Pointer(&s1[0]))

   s := (*int)(ptr)
   fmt.Printf("s1第一个元素的数值=%+d",*s)

}

输出结果:
s1第一元素一开始的地址 = +824634318848 
s1扩容后第一个元素的地址 = +824634351616 
s1第一个元素的数值=+1

s1扩容前后切片第一个元素的内存地址不相同,可以判定,两者不是同一个对象;扩容之后s1第一个元素的值没有变化可以判定,切片扩容没有修改原始切片的值。如果此时把s1切片,扩容前所有元素都打印出来,值也是没有变化的。

切片扩容有一个容易踩坑的点,请看代码。

代码语言:javascript
复制
package main

import "fmt"

func main() {
   arr := [5]int{1, 2, 3, 4, 5}
   s1 := arr[0:2]
   s2 := append(s1, 100)
   fmt.Printf("第一次打印 s1 = %v, Pointer = %p, len = %d, cap = %d\n", s1, &s1, len(s1), cap(s1))
   fmt.Printf("第一次打印 s2 = %v, Pointer = %p, len = %d, cap = %d\n", s2, &s2, len(s2), cap(s2))
   s2[1] += 50
   fmt.Printf("第二次打印 s1 = %v, Pointer = %p, len = %d, cap = %d\n", s1, &s1, len(s1), cap(s1))
   fmt.Printf("第二次打印 s2 = %v, Pointer = %p, len = %d, cap = %d\n", s2, &s2, len(s2), cap(s2))
   fmt.Printf("arr = %v\n", arr)
}
输出结果:
第一次打印 s1 = [1 2], Pointer = 0xc000114000, len = 2, cap = 5
第一次打印 s2 = [1 2 100], Pointer = 0xc000114018, len = 3, cap = 5
第二次打印 s1 = [1 52], Pointer = 0xc000114000, len = 2, cap = 5
第二次打印 s2 = [1 52 100], Pointer = 0xc000114018, len = 3, cap = 5
arr = [1 52 100 4 5]

通过结果我们可以看到,修改切片s2的元素值,也改变了数组arr的值,因为s1指向arr数组,所有s1的值也发生了变化。此时是因为数组的长度够长,扩容之后并不会新申请数组,切片还是指向老的数组。

Go切片的源码在:runtime包的slice.go文件中。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2021-05-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 一行舟 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言:本文主要介绍Go语言数组和切片的基本概念,常用方法和使用时的注意事项。
  • 数组
    • 数组声明
    • 切片(slice)
      • 切片声明:
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档