golang学习--slice

切片定义

切片(Slice)是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。切片是一个引用类型,它的内部结构包含地址、长度和容量。切片一般用于快速地操作一块数据集合。

数组与切片

切片的数据实际是通过数组来保存的,每个切片都有三个信息:底层数组的指针、切片的长度(len)和切片的容量(cap)。
举个栗子,底层数组a := [8]int{0, 1, 2, 3, 4, 5, 6, 7};

  • 切片s1 := a[:5],切片和数组对应关系:


    Go-slice-2020-05-12-15-23-13
  • 切片s2 := a[3:6],切片和数组对应关系:


    Go-slice-2020-05-12-15-44-43

指向同一个底层数组的切片修改值

切片是指向底层数组的引用类型,指向同一个底层数组的切片底层数据存放都是在同一个位置,修改某个切片会影响到在同一个范围的切片

import (
    "fmt"
    "testing"
)

func TestSliceShareMemory(t *testing.T) {
    year := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
    Q2 := year[3:6]
    t.Log(Q2, len(Q2), cap(Q2))
    summer := year[5:8]
    t.Log(summer, len(summer), cap(summer))
    summer[0] = "Unkonw"
    t.Log(Q2)
    t.Log(year)
}
// === RUN   TestSliceShareMemory
//     TestSliceShareMemory: slice_test.go:36: [Apr May Jun] 3 9
//     TestSliceShareMemory: slice_test.go:38: [Jun Jul Aug] 3 7
//     TestSliceShareMemory: slice_test.go:40: [Apr May Unkonw]
//     TestSliceShareMemory: slice_test.go:41: [Jan Feb Mar Apr May Unkonw Jul Aug Sep Oct Nov Dec]
// --- PASS: TestSliceShareMemory (0.00s)
// PASS

切片表达式

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

  1. 切片len()是可访问长度,容量cap()是总空间大小。通过数组生成的切片, len为首尾索引之差,cap为从切片首索引到数组末尾长度
  2. 切片s[low:high:max],从切片s的low处到high处所获得的切片,len=high-low,cap=max-low
func TestSliceExpression(t *testing.T) {
    a := [5]int{1, 2, 3, 4, 5}
    // b := a[1:3:7]
    b := a[1:3:5]
    fmt.Printf("b:%v len(b):%v cap(b):%v\n", b, len(b), cap(b))
}
// === RUN   TestSliceExpression
// b:[2 3] len(b):2 cap(b):4
// --- PASS: TestSliceExpression (0.00s)
// PASS

切片不能比较

两个切片不能直接比较,会报错:

func TestSliceCompare(t *testing.T) {
    a := []int{1, 2, 3, 4}
    b := []int{1, 2, 3, 4}
    if a == b {
        t.Log("a==b")
    }
}
// invalid operation: a == b (slice can only be compared to nil)
// FAIL go_learn/go_test/slice_test [build failed]
// FAIL

切片的append

切片通过append()添加元素时,未超过newcap时底层数组地址不变,超过的话底层数组会申请新的内存地址。新申请的容量大小计算分成了两步,有关append的源码在$GOROOT/src/runtime/slice.go,可以自己去分析。

计算逻辑newcap

  1. new cap > old * 2直接申请新容量大小;
  2. 小于2倍时,len<1024翻倍,len>1024加上1/4
//$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
    }
  }
}
  
//...略
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
//...略

实际申请内存大小

上面先算了个逻辑上的newcap,实际申请内存的时候,由于内存对齐的关系不会直接就用newcap。上面的代码就是在算好了newcap后会调用roundupsize()得到实际的大小。

//$GOROOT/src/runtime/msize.go
// Returns size of the memory block that mallocgc will allocate if you ask for the size.
func roundupsize(size uintptr) uintptr {
    if size < _MaxSmallSize {
        if size <= smallSizeMax-8 {
            return uintptr(class_to_size[size_to_class8[(size+smallSizeDiv-1)/smallSizeDiv]])
        } else {
            return uintptr(class_to_size[size_to_class128[(size-smallSizeMax+largeSizeDiv-1)/largeSizeDiv]])
        }
    }
    if size+_PageSize < size {
        return size
    }
    return alignUp(size, _PageSize)
}

roundupsize()中的class_to_size、size_to_class8是存了具体大小的数组,根据传入的newcap来算出下标,拿到对应的大小值。这些数组在//$GOROOT/src/runtime/sizeclasses.go,这个文件又是//$GOROOT/src/runtime/mksizeclasses.go.go生成的,生成规则就先不去看了。

//$GOROOT/src/runtime/sizeclasses.go
// Code generated by mksizeclasses.go; DO NOT EDIT.
//go:generate go run mksizeclasses.go
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128 ...}

var size_to_class8 = [smallSizeMax/smallSizeDiv + 1]uint8{0, 1, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 18, 18, 19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 22, 22, 22, 22, 23, 23, 23, 23, 24, 24, 24, 24, 25 ...}

了解了上面的内容之后就可以理解下面的几个例子了:

func TestSliceAppend(t *testing.T) {
    var a = make([]int, 5, 10)
    for i := 0; i < 10; i++ {
        a = append(a, i)
        fmt.Printf("a ptr: %p\n", a)
    }
    fmt.Println(a)
}
// === RUN   TestSliceAppend
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc00000c320
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// a ptr: 0xc0000100a0
// [0 0 0 0 0 0 1 2 3 4 5 6 7 8 9]
// --- PASS: TestSliceAppend (0.00s)
// PASS

func TestSliceAppend2(t *testing.T) {
    s := []int{1, 2, 3, 4}
    a := make([]int, 3, 6)
    b := append(a, 10)
    a[0] = 50
    fmt.Printf("a: %v\tptr: %p\tfirst: %v\n", a, a, a[0])
    fmt.Printf("b: %v\tptr: %p\tfirst: %v\n", b, b, b[0])

    b = append(a, s...)
    a[0] = 100
    fmt.Printf("a: %v\tptr: %p\tfirst: %v\n", a, a, a[0])
    fmt.Printf("b: %v\tptr: %p\tfirst: %v\n", b, b, b[0])
}
// === RUN   TestSliceAppend2
// a: [50 0 0]  ptr: 0xc00000a330   first: 50
// b: [50 0 0 10]   ptr: 0xc00000a330   first: 50
// a: [100 0 0] ptr: 0xc00000a330   first: 100
// b: [50 0 0 1 2 3 4]  ptr: 0xc00001a4e0   first: 50
// --- PASS: TestSliceAppend2 (0.00s)
// PASS

func TestSliceAppend3(t *testing.T) {
    a1 := make([]int, 20)
    b1 := make([]int, 40)
    a1 = append(a1, b1...)
    fmt.Println(len(a1), cap(a1))

    a2 := make([]int, 20)
    b2 := make([]int, 42)
    a2 = append(a2, b2...)
    fmt.Println(len(a2), cap(a2))
}
// === RUN   TestSliceAppend3
// 60 60
// 62 64
// --- PASS: TestSliceAppend3 (0.00s)
// PASS

切片元素删除

在要删除的元素左右切两下:a1 = append(a1[:1], a1[2:]...),删除其实是将所删元素后面的往前挪。

func TestSliceDelete(t *testing.T) {
    a := []int{30, 31, 32, 33, 34, 35, 36, 37}
    // 要删除索引为2的元素
    a = append(a[:2], a[3:]...)
    t.Log(a)
}
// === RUN   TestSliceDelete
//     TestSliceDelete: slice_test.go:98: [30 31 33 34 35 36 37]
// --- PASS: TestSliceDelete (0.00s)
// PASS

参考内容

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335