切片

什么是slice

Go中的切片,是我们经常用到的数据结构。有着比数组更灵活的用法,那么作者就去探究下什么是切片。

我们先来了解下切片的数据结构

type slice struct {
array unsafe.Pointer // 指针
len int // 长度
cap int // 容量
}

切片一共三个属性:指针,指向底层的数组;长度,表示切片可用元素的个数,也就是说使用下标 对元素进行访问的时候,下标不能超过的长度;容量,底层数组的元素个数,容量》=长度。

底层的数组是可以被多个切片同时指向的,因此对一个切片元素的操作可能会影响到其他的切片。

slice的创建使用

序号 方式 代码示例
1 直接声明 var slice []int
2 new slice := *new([]int)
3 字面量 slice := []int{1,2,3,4,5}
4 make slice := make([]int, 5, 10)
5 从切片或数组截取 slice := array[1:5] 或 slice := sourceSlice[1:5]

第一种创建出来的 slice 其实是一个 nil slice。它的长度和容量都为0。和nil比较的结果为true。

这里比较混淆的是empty slice,它的长度和容量也都为0,但是所有的空切片的数据指针都指向同一个地址 0xc42003bda0。空切片和 nil 比较的结果为false。

下面是它的内部结构:

创建方式 nil切片 空切片
方式一 var s1 []int var s2 = []int{}
方式二 var s4 = *new([]int) var s3 = make([]int, 0)
长度 0 0
容量 0 0
和nil比较 true false

nil 切片和空切片很相似,长度和容量都是0,官方建议尽量使用 nil 切片。

  • 字面量

直接初始化表达式进行创建

 s1 := []int{, , , ,,}
  • make
slice := make([]int, , ) // 长度为5,容量为10

slice使用的一点规范

  • 根据 Uber Go代码风格指南

  • nil 是一个有效的 slice

nil 是一个长度为 0 的 slice。意思是,

  • 使用 nil 来替代长度为 0 的 slice 返回

    Bad Good
    if x == "" {
    return []int{}
    }
    if x == "" {
    return nil
    }
  • 检查一个空 slice,应该使用 len(s) == 0,而不是 nil

    Bad Good
    func isEmpty(s []string) bool {
    return s == nil
    }
    func isEmpty(s []string) bool {
    return len(s) == 0
    }
  • The zero value (a slice declared with var) is usable immediately without make().

  • 零值(通过 var 声明的 slice)是立马可用的,并不需要 make() 。

    Bad Good
    nums := []int{}
    // or, nums := make([]int) if add1 {
    nums = append(nums, 1)
    } if add2 {
    nums = append(nums, 2)
    }
    var nums []int
    
    if add1 {
    nums = append(nums, 1)
    } if add2 {
    nums = append(nums, 2)
    }

slice和数组的区别

slice的底层是数组,slice是对数组的封装,它描述一个数组的片段。两者都可以通过下标访问单个元素。

数组是定长的,长度定义好,不能改变。在Go中数组是不常见的,因为长度是类型的一部分,限制了它的表达 能力,比如[3]int 和 [4]int 就是不同的类型。

切片可以动态的扩容,非常灵活。切片的类型和长度没有关系。

slice的append是如何发生的

先看看append函数的原型:

func append(slice []Type, elems ...Type) []Type

append 函数的参数长度可变,因此可以追加多个值到 slice 中,还可以用 ... 传入 slice,直接追加一个切片。

slice = append(slice, elem1, elem2)
slice = append(slice, anotherSlice...)

append函数返回值是一个新的slice,Go编译器不允许调用了append函数后不使用返回值。

append(slice, elem1, elem2)
append(slice, anotherSlice...)

上面是不能编译通过的

使用 append 可以向 slice 追加元素,实际上是往底层数组添加元素。但是底层数组的长度是固定的,如果索引 len-1 所指向的元素已经是底层数组的最后一个元素,就没法再添加了。

这时,slice 会迁移到新的内存位置,新底层数组的长度也会增加,这样就可以放置新增的元素。同时,为了应对未来可能再次发生的 append 操作,新的底层数组的长度,也就是新 slice 的容量是留了一定的 buffer 的。否则,每次添加元素的时候,都会发生迁移,成本太高。

新slice预留buffer大小是有一定规律的。

// growslice handles slice growth during append.
// It is passed the slice element type, the old slice, and the desired new minimum capacity,
// and it returns a new slice with at least that capacity, with the old data
// copied into it.
// The new slice's length is set to the old slice's length,
// NOT to the new requested capacity.
// This is for codegen convenience. The old slice's length is used immediately
// to calculate where to write new values during an append.
// TODO: When the old backend is gone, reconsider this decision.
// The SSA backend might prefer the new length or to return only ptr/cap and save stack space.
func growslice(et *_type, old slice, cap int) slice {
if raceenabled {
callerpc := getcallerpc()
racereadrangepc(old.array, uintptr(old.len*int(et.size)), callerpc, funcPC(growslice))
}
if msanenabled {
msanread(old.array, uintptr(old.len*int(et.size)))
} if et.size == {
if cap < old.cap {
panic(errorString("growslice: cap out of range"))
}
// append should not create a slice with nil pointer but non-zero len.
// We assume that append doesn't need to preserve old.array in this case.
return slice{unsafe.Pointer(&zerobase), old.len, cap}
} newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for < newcap && newcap < cap {
newcap += newcap /
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= {
newcap = cap
}
}
} var overflow bool
var lenmem, newlenmem, capmem uintptr
const ptrSize = unsafe.Sizeof((*byte)(nil))
switch et.size {
case :
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > _MaxMem
newcap = int(capmem)
case ptrSize:
lenmem = uintptr(old.len) * ptrSize
newlenmem = uintptr(cap) * ptrSize
capmem = roundupsize(uintptr(newcap) * ptrSize)
overflow = uintptr(newcap) > _MaxMem/ptrSize
newcap = int(capmem / ptrSize)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
overflow = uintptr(newcap) > maxSliceCap(et.size)
newcap = int(capmem / et.size)
} // The check of overflow (uintptr(newcap) > maxSliceCap(et.size))
// in addition to capmem > _MaxMem is needed to prevent an overflow
// which can be used to trigger a segfault on 32bit architectures
// with this example program:
//
// type T [1<<27 + 1]int64
//
// var d T
// var s []T
//
// func main() {
// s = append(s, d, d, d, d)
// print(len(s), "\n")
// }
if cap < old.cap || overflow || capmem > _MaxMem {
panic(errorString("growslice: cap out of range"))
} var p unsafe.Pointer
if et.kind&kindNoPointers != {
p = mallocgc(capmem, nil, false)
memmove(p, old.array, lenmem)
// The append() that calls growslice is going to overwrite from old.len to cap (which will be the new length).
// Only clear the part that will not be overwritten.
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
// Note: can't use rawmem (which avoids zeroing of memory), because then GC can scan uninitialized memory.
p = mallocgc(capmem, et, true)
if !writeBarrier.enabled {
memmove(p, old.array, lenmem)
} else {
for i := uintptr(); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i))
}
}
} return slice{p, old.len, newcap}
}

其中这一段是重点的代码,我们可以看到

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

复制Slice和Map注意事项

slice 和 map 包含指向底层数据的指针,因此复制的时候需要当心。

接收 Slice 和 Map 作为入参

需要留意的是,如果你保存了作为参数接收的 map 或 slice 的引用,可以通过引用修改它。

Bad Good
func (d *Driver) SetTrips(trips []Trip) {
d.trips = trips
} trips := ...
d1.SetTrips(trips) // Did you mean to modify d1.trips?
trips[0] = ...
func (d *Driver) SetTrips(trips []Trip) {
d.trips = make([]Trip, len(trips))
copy(d.trips, trips)
} trips := ...
d1.SetTrips(trips) // We can now modify trips[0] without affecting d1.trips.
trips[0] = ...

返回 Slice 和 Map

类似的,当心 map 或者 slice 暴露的内部状态是可以被修改的。

Bad Good
type Stats struct {
sync.Mutex counters map[string]int
} // Snapshot 方法返回当前的状态
func (s *Stats) Snapshot() map[string]int {
s.Lock()
defer s.Unlock() return s.counters
} // snapshot 不再被锁保护
snapshot := stats.Snapshot()
type Stats struct {
sync.Mutex counters map[string]int
} func (s *Stats) Snapshot() map[string]int {
s.Lock()
defer s.Unlock() result := make(map[string]int, len(s.counters))
for k, v := range s.counters {
result[k] = v
}
return result
} // 现在 Snapshot 是一个副本
snapshot := stats.Snapshot()

总结

  • 切片是对底层数组的一个抽象,描述了它的一个片段。
  • 切片实际上是一个结构体,它有三个字段:长度,容量,底层数据的地址。
  • 多个切片可能共享同一个底层数组,这种情况下,对其中一个切片或者底层数组的更改,会影响到其他切片。
  • append 函数会在切片容量不够的情况下,调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。
  • 扩容策略并不是简单的扩为原切片容量的 2 倍或 1.25 倍,还有内存对齐的操作。扩容后的容量 >= 原容量的 2 倍或 1.25 倍。
  • 当直接用切片作为函数参数时,可以改变切片的元素,不能改变切片本身;想要改变切片本身,可以将改变后的切片返回,函数调用者接收改变后的切片或者将切片指针作为函数参数。

参考

slice使用了解的更多相关文章

  1. Matlab slice方法和包络法绘制三维立体图

    前言:在地球物理勘探,流体空间分布等多种场景中,定位空间点P(x,y,x)的物理属性值Q,并绘制三维空间分布图,对我们洞察空间场景有十分重要的意义. 1. 三维立体图的基本要件: 全空间网格化 网格节 ...

  2. jQuery之常用且重要方法梳理(target,arguments,slice,substring,data,trigger,Attr)-(一)

    1.jquery  data(name) data() 方法向被选元素附加数据,或者从被选元素获取数据. $("#btn1").click(function(){ $(" ...

  3. js url.slice(star,end) url.lastIndexOf('/') + 1, -4

    var url = '"http://60.195.252.25:15518/20151228/XXSX/作三角形的高.mp4")' document.title = url.sl ...

  4. JavaScript中的slice,splice,substr,substring,split的区别

    万恶的输入法,在sublime中会显示出繁体字,各位看官见谅. 1.slice()方法:该方法在数组和string对象中都拥有. var a = [1,2,3,4,5,6]; var s = 'thi ...

  5. Max double slice sum 的解法

    1. 上题目: Task description A non-empty zero-indexed array A consisting of N integers is given. A tripl ...

  6. js中substr,substring,slice。截取字符串的区别

    substr(n1,n2) n1:起始位置(可以为负数) n2:截取长度(不可以为0,不可以为负数,可以为空) 当n1为正数时,从字符串的n1下标处截取字符串(起始位置),长度为n2. 当n1为负数时 ...

  7. JS 中 Array.slice() 和 Array.splice()方法

    slice slice()就是对应String的substring()版本,它截取Array的部分元素,然后返回一个新的Array: var arr = ['A', 'B', 'C', 'D', 'E ...

  8. 【javascript 技巧】Array.prototype.slice的妙用

    Array.prototype.slice的妙用 开门见山,关于Array 的slice的用法可以参考这里 http://www.w3school.com.cn/js/jsref_slice_arra ...

  9. golang中的slice翻转存在以及map中的key判断

    //slice翻转 func stringReverse(src []string){ if src == nil { panic(fmt.Errorf("the src can't be ...

  10. jQuery中slice()用法总结

    <!DOCTYPE html> <html> <head lang="en"> <meta charset="utf-8&quo ...

随机推荐

  1. 【Weiss】【第03章】练习3.20:中缀表达式转后缀表达式

    [练习3.20] a.编写一个程序将中缀表达式转换为后缀表达式,该中缀表达式含括号及四则运算. b.把幂操作符添加到你的指令系统中去. c.编写一个程序将后缀表达式转化为中缀表达式. Answer: ...

  2. vscode回车补全代码

    VsCode设置回车补全代码而不换行 有一部分人不习惯用tab键补全代码,我就是其中之一,习惯了回车补全的我决定设置一波,网上找了很多, 没找到比较详细的,所以自己写一个 有一个叫keybinding ...

  3. Linux篇001——打开vi默认显示行号

    $ vi ~/.vimrc 新增一行命令 :set number 保存退出,source ~/.vimrc

  4. tcp\udp 操作系统发展史

    目录 为什么会出现粘包现象 socket发送大文件示例 UDP协议 基于UDP实现简易版本的qq 总结: SocketServer模块介绍(让tcp也能支持并发) 为什么会出现粘包现象 TCP 三次握 ...

  5. Cesium 源码笔记[1] Viewer模块实例化的大致过程

    我原本想写日记的,但是不太现实. 源码下载 源码可以从源码包和发行包中的Source目录中获取. Cesium的模块化机制从1.63版本开始,由原来的RequireJs变为ES6.但有可能是原先设计耦 ...

  6. 我们是怎么实现Grpc CodeFirst

    前言: Grpc默认是ProtoFirst的,即先写 proto文件,再生成代码,需要人工维护proto,生成的代码也不友好,所以出现了Grpc CodeFirst,下面来说说我们是怎么实现Grpc ...

  7. TensorFlow-Bitcoin-Robot:一个基于 TensorFlow LSTM 模型的 Bitcoin 价格预测机器人。

    简介 TensorFlow-Bitcoin-Robot:一个基于 TensorFlow LSTM 模型的 Bitcoin 价格预测机器人. 文章包括一下几个部分: 1.为什么要尝试做这个项目? 2.为 ...

  8. Consul+upsync+Nginx 动态负载均衡

    1,动态负载均衡 传统的负载均衡,如果修改了nginx.conf 的配置,必须需要重启nginx 服务,效率不高.动态负载均衡,就是可配置化,动态化的去配置负载均衡. 2,实现方案 1. Consul ...

  9. iOS 内置图片瘦身

    一.iOS 内置资源的集中方式 1.1 将图片存放在 bundle 这是一种很常见的方式,项目中各类文件分类放在各个 bundle 下,项目既整洁又能达到隔离资源的目的.采用 bundle 的加载方式 ...

  10. 数字反转 NOIp普及组2011

    当数字位数不确定时,如何反转呢? 本文为博客园ShyButHandsome原创作品,转载请注明出处 使用右侧目录快速浏览文章 题目描述 给定一个整数,请将该数各个位上数字反转得到一个新数. 新数也应满 ...