golang 切片(slice)简单使用

1 golang 切片(slice)简单使用


在流媒体领域,用golang来进行流媒体服务器的开发,会非常频繁的对二进制数据进行处理,这里slice会经常使用,有关slice的简单使用及注意事项,本文做一个说明。

1.1 slice简介

切片(slice)是Golang中数组之上的抽象;可按需自动增长与缩小,其底层是连续的内存块,切片是包含三个成员变量数据结构,如下所示:

type SliceHeader struct {
    Data uintptr
    Len  int
    Cap  int
}
  • Data:指向底层数组的指针
  • Len:切片中元素的个数;len(s)获取
  • Cap:切片的容量(不需重新分配内存前,可容纳的元素数量);cap(s)获取

先看下切片定义和初始化的例子:

var (
    a []int               // nil切片,和nil相等,一般用来表示一个不存在的切片
    b = []int{}           // 空切片,和nil不相等,一般用来表示一个空的集合
    c = []int{1, 2, 3}    // 有3个元素的切片,len=3,cap=3
    d = c[:2]             // 有2个元素的切片,len=2,cap=3
    e = c[0:2:cap(c)]     // 有2个元素的切片,len=2,cap=3
    f = c[:0]             // 有0个元素的切片,len=0,cap=3
    g = make([]int, 3)    // 有3个元素的切片,len=3,cap=3
    h = make([]int, 2, 3) // 有2个元素的切片,len=2,cap=3
    i = make([]int, 0, 3) // 有0个元素的切片,len=0,cap=3
)
j := c[1:2]               //j包含1个元素2,len=2-1,cap=3,其内存还是使用c切片的内存,并未copy

1.2 slice创建和初始化

以建立byte的切片为例,介绍slice创建和初始化过程
var a []byte 定义一个byte的sice,但是未分配任何内存,此时不能直接使用,必须配合make函数进行初始化后才能使用:

var a []byte       //不做任何初始化就会创建一个nil切片
a = make([]byte,0) //空切片,长度和容量都是0,但是不为nil,可以append操作
a = make([]byte,0,100)//切片长度未0,容量未100,空切片
a = make([]byte,3,100) //切片长度为3,初始值为{0,0,0},容量为100

切片长度是切片中实际存入的数据长度,容量是切片初始分配的内存空间,Slice依托数组实现,追加数据通过append(返回值一定要再赋值给原slice),容量不足时会自动扩容,如果容量小于1024,自动扩容为原来2倍,如果容量超过1024,自动扩容为1.25倍,这里一定要注意,如果使用slice进行数据处理时,尽量初始化时分配足够的空间,不要触发切片自动扩容,自动扩容不仅会占用更多的空间,也会存在数据移动操作,消耗资源。

初始化时,也可通过如下几种方式:
a := make([]byte,0,100)
var a []byte = []byte{1,2,3}
a := []int{10: 5} //:=表示变量a类型通过后面表达式推理得到,a第十个元素为5,其他为0,len=cap=11

通过也有切片创建切片,也就是创建切片的子切片,其共享底层内存数组,内存空间使用的就是原始数组的空间,因此在不扩容时修改切片的内容,会影响到其他共享内存的切片:
dst = src[low : high : max]
三个参分别表示:

  • low:为截取的起始下标(含)
  • high:为窃取的结束下标(不含high)
  • max: 为切片保留的原切片的最大下标(不含max)

新切片原始切片的low下标元素为起始,len = high - low, cap = max - low, high 和 max不能超出㡳切片,否则会段错误
如果没有指定max,则max的值为原始切片容量-low。

a := []int{10: 99} //len=cap=11
b := a[1:3] //元素值为{0,0} len=3-1=2,cap=11-1=10
c := a[:5]  //low=0,相当于a[0:5],len=5,cap=11
d := a[8:]  //high=11,len=3,cap=11-8=3
e := a[1:5:5] //len=4 cap=5
f := a[:] //相当于指针赋值,表示同一个切片

1.3 slice操作

主要介绍切片的访问,遍历,插入,删除等操作

1.3.1 slice的访问

可通过下表直接方位,访问不能超过其长度,否则出错:

a := []string{"aaa","bbb","ccc"} //声明并初始化为string的切片,len=cap=3
b := a[3] //b为string类型值为"ccc"
fmt.Println(a[1]) //打印为字符串"bbb"

1.3.2 slice的遍历

切片也是一个集合,通过range遍历元素

a := []string{"aaa", "bbb", "ccc"}
for k, v := range a {
  fmt.Println(k, " ", v) //k为下表,v为值
} 
/*其运行结果为:
0   aaa
1   bbb
2   ccc
*/

注意:

  • range返回的切片值是对应元素的copy,修改不会改变原slice,若要修改则需要通过索引,如a[k] = "xxx"
  • slice是非线程安全的,遍历期间不能对slice进行删除和修改操作,注意要有多线程操作,需要加线程锁sync.RWMutex

1.3.2 插入元素

通过函数append可在切片尾部追加元素,如下:

a := []byte{1,2,3}
a = append(a,1) //在a尾部增加一个byte元素1
a = append(a,[]byte{3,4}...)//在a尾部增加一个元素为3,4的byte切片,切片组合,把[]byte{3.4}切片中元素的值逐个增加到a的尾部
b := []byte{7,8}
a = apend(a,b...)//在a的尾部插入b切片中的元素

注意此种追加方式,如果cap重组,则不会重新分配内存,如果cap不足会触发cap扩容

如果要在切片头部增加元素,则必然会引起内存重新分配与赋值操作,插入方式如下:

a := []byte{1,2,3}
a = append([]byte{5}, a...) //头部必须是与a相同类型的slice,就相当于先把a追加到匿名变量[]byte{1}的尾部,
                            //再把此匿名变量的指针赋值给a,所以a已经不是原来分配的内存空间了
a = append([]byte{1,2,3}, a...)

所以尽量避免在slice头部插入元素,特别是长度加大的slice

还有一种情况,可能要在slice非头部和尾部的地方插入元素,此时会设计数据移动,也可能会使得slice扩容,其方式可通过以下代码实现:

a := []byte{1,2,3}
alen = len(a)
b := []byte{4,5} //插入到a的第一个元素和第二个元素之间
a = append(a, b...)        // 先把长度增加到len(a)+len(b)
copy(a[1+len(b):], a[1:alen])  // 后移len(b)个元素
copy(a[1:1+len(b)], b)

1.3.3 删除元素

删除切片指定元素,Go 标准库并未给出相应的函数,需要我们自己实现,删除的元素若是指针,可能还会被底层数组引用中,从而无法被gc回收,为了能及时的释放,在删除前,先把对应元素设置为nil再删除;
在切片头尾删除时,通过子切片生成的方式最简单,如下方式:

a[len(a)-1]=nil
a = a[:len(a)-1]
a[1] = nil
a = a[1:]

若要删除中间的元素,可通过append移动的方式:

// append追加,覆盖被删除元素
a = append(a[:i], a[i+N:]...)
 
// 复制,覆盖被删除元素
a = a[:i+copy(a[i:], a[i+N:])]

也可以通过遍历的方式来实现,这个可自行实现即可

1.4 slice在流媒体数据处理中使用

在流媒体中会需要对码流数据进行解复用/复用/抽帧/组帧/解码等操作,通过切片的方式能够更加灵活的进行byte数据进行处理,经常会用到切片的插入/拷贝/移动截取等操作,为了更高的数据处理性能,slice使用时需要注意以下方面:

  • slice容量分配,尽可能够用,尽量避免弧线扩容,比如rtp包最大不会超过mtu,我们可以初始化RTP包的slice为稍大于mtu
    rtpbuf := make([]byte, 0, 1480)
  • 尽量使用slice特性截取子slice,不要触发数据拷贝和新内存占用
    payload := rtpbuf[headlen:]
    rtppkt.Timestamp = binary.BigEndian.Uint32(rtpbuf[4:8])
    binary.BigEndian.PutUint32(rtpheader[8:12], p.SSRC)
  • 针对视频流封装,分配内存时可在buf头部预留足够长的预留字节,打包时直接把头字节拷贝到头部,而不触发数据部分的移动或拷贝
buf := make([]byte, 0, 1400+14)
buf = append(buf, []byte{13:0})
data := buf[14:]
header := buf[:14]
...//码流封装和分片
data = append(data,payload...)
//填充头部,例如头部为12个RTPheader
header := buf[2:14]
//rtp头部封装,直接使用header切片生成
...
sendbuf := buf[2:]
//发送数据
...
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

壹零仓

感谢您的鼓励

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值