18 复合数据类型之切片

复合数据类型之切片Slice

切片是一种操作,也是一种类型!!!

一 切片类型

1.1 切片类型介绍

切片Slice类型是一个元素类型相同且长度可变的序列,切片类型在底层指向的就是一个数组。切片的语法和数组很像,只是没有固定长度而已,如下

​ 1、数组类型:[固定长度]T

​ 2、切片类型:[]T

切片与数组不同的是

​ 1、数组是长度不可变的序列,而切片则是长度可变的序列,
​ 2、数组是一个值类型,而切片是一个引用类型

切片类型由三个部分构成:指针、长度、容量

//1、指针:指针指向的是切片中的第一个元素所对应的底层数组元素的地址。
注意:切片的第一个元素并不一定就是数组的第一个元素

//2、长度:长度就是切片当前包含的元素的个数
注意:长度不能超过切片的容量

//3、容量:长度指的是切片当前容纳的元素的个数,而切片的长度是可以变化的,容量指的则是切片未来可以容纳的元素的个数,也就是说容量指的是切片未来可以达到的长度范围,所以长度肯定不可能超过容量
要注意的是:
    1、随着往切片中append元素,若底层数组不够容纳,切片便会自动扩容(指向新的数组)
    2、在没有扩容的情况下:容量指的是从切片的开始位置到底层数组的结尾位置
ps:内置的len和cap函数分别返回slice的长度和容量

1.2 切片类型的声明与初始化

因为切片类型属于复合类型中的引用类型,所以

(1)直接声明一个切片类型,零值为nil(引用类型的零值都为nil),nil代表空、没有分配内存,即没有底层数组

var a []int
fmt.Println(a == nil) // true

a[0] = 666  // 报错,因为没有开辟内存空间

(2)初始化切片类型的方式有两种

方式一:先调用make()函开辟空间,然后再塞值

=======>先上例子<=======
var a []int = make([]int, 3, 3) // 开辟内存空间,指向一个底层数组
fmt.Printf("%p\n", &a) // 打印变量本身的值,输出地址为0xc00000c060
fmt.Printf("%p\n", a) // 打印首元素的地址,输出地址为0xc000018140,代表不为空、有值了
fmt.Printf("%v\n", a) // [0 0 0]

fmt.Println(a == nil)           // false
a[0] = 666  // 可以赋值

=======>make()介绍<=======
内置的make函数可以创建一个指定`元素类型`、`长度`和`容量`的slice。
其实在底层,make是先创建了一个匿名的数组变量,然后再返回一个slice;并且只有通过这个返回的slice才能引用底层匿名的数组变量
使用make函数为切片事先申请好内存空间,也可以防止运行时申请内存带来的效率问题。

make([]T, len)      // 容量部分可以省略,在这种情况下,容量cap将等于长度len
make([]T, len, cap) // 创建了一个元素个数为cap个的底层数组,然后只引用了底层数组的前len个元素,若len<cap,那额外的元素就是留给未来的增长用的。

方式二:一步到位式,即赋一个完整的值,该方式等同于make完毕之后再塞值,如下所示

// 一步到位式,等同于方式一的先make再塞值
var names []string = []string{"egon", "bigegon", "smallegon"

// 也可以通过索引和元素值指定,或者的两种风格的混合语法初始化,如下所示
s1 := []string{"aaa", "bbb",3:"ddd",}
fmt.Println(s1) // [aaa bbb  ddd]
fmt.Println(s1[0]) // aaa
fmt.Println(s1[1]) // bbb
fmt.Println(s1[2]) // 空
fmt.Println(s1[3]) // ddd

小结
slice和数组的字面值语法很类似,它们都是用花括弧包含一系列的初始化元素。
s1 := [5]int{1, 2, 3, 4, 5}
fmt.Printf("%T\n",s1) // 数组类型[5]int

s2 := []int{1, 2, 3, 4, 5}
fmt.Printf("%T\n",s2) // 切片类型[]int

但是对于slice并没有指明序列的长度,这会隐式地创建一个合适大小的数组,然后slice的指针指向底层的数组。

二 切片操作

2.1 切片操作介绍

完整切片操作:s[i:j:max],

i:起始索引
j:结束索引
max:往切片末尾添加值时索引将超过j,max指的是最大可以达到的索引值,max-i+1就是切片的容量

//1、其中0 ≤ i ≤ j ≤ max,引用s的从第i个元素开始到第j-1个元素的子序列,即顾头不顾尾
//2、省略i,默认起始位置为0,即s[:j]等同于s[0:j]
//3、省略j,默认结束位置为len(s),即从起始位置一直切到底,采用被切对象的整个长度
//4、省略和j,例如s[:]代表引用全部
//5、省略max,默认max为底层数组的最大索引

被切的对象可以是?

  • 1、我们可以对数组、切片、指向数组或切片的指针进行切片操作,得到的是一个切片Slice类型。
  • 2、我们也可以对字符串进行切片操作,得到的是一个字符串类型

2.2 对数组进行切片操作得到切片类型

注意:对数组进行切片操作,结束位置不能超过数组的长度

// 先创建一个数组,我们的索引从1开始,第0个元素会被自动初始化为空字符串。
months := [...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"}
fmt.Println(len(months))  // 13

// 例1:对数组进行切片,超出其长度则报错
fmt.Println(months[0:12])
fmt.Println(months[0:13])
fmt.Println(months[0:14])  // 错误,超出数组长度

 // 例2:对同一数组进行切片操作,多个slice之间可以共享底层数组的部分区间可能重叠,如下图所示
Q2 := months[4:7]
fmt.Println(Q2)            // ["April" "May" "June"]
fmt.Printf("%T\n",Q2)      // []string
fmt.Println(Q2[1])         // 访问的是切片中的第2个元素,结果为:May

summer := months[6:9]
fmt.Println(summer)       // ["June" "July" "August"]
fmt.Printf("%T\n",summer) // []string
fmt.Println(summer[1])    // 访问的是切片中的第2个元素,结果为:July

// 例3:切片为引用类型,修改切片的元素,肯定会影响到底层数组及相关切片
summer[0]="JUNE"

fmt.Println(months) // 底层数组改变:[ ...... JUNE ......]
fmt.Println(Q2)     // 相关切片也变:[April May JUNE]

2.3 对切片进行切片操作得到切片类型

注意:

1、对一个切片再进行切片操作,结束位置能达到的最大值是切片的容量,而不是长度

2、对一切片进行切片操作,如果起始位置是0,代表的是切片的第一个元素,而切片的第一个元素并不一定就是底层数组的第一个元素,结束位置同理。

//1、以上述切片类型summer为例
summer := months[6:9]  // summer切片的长度为3,容量为7

// 2、对切片类型再执行切片操作,区间最小值为0,最大值为切片容量7,0指的是切片的第一个元素而非底层数组的
// 2.1 结束位置可以超过原切片的长度3
fmt.Println(summer)         // [June July August]
fmt.Println(summer[:7])     // [June July August September October November December]
fmt.Printf("%T",summer[:7]) // 类型为切片类型:[]string

// 2.2 但是结束位置绝对不能超过原切片的容量7
fmt.Println(summer[:8]) // 异常panic: out of range

2.4 对指向数组或切片的指针进行切片操作来得到切片类型

注意:

数组是值类型,取其指针是有意义的:引用传递

而切片本身就是引用类型,当然也可以取其指针,但用处不大,了解即可

months := [...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"}

x:=&months // x是指向数组的指针

summer := months[6:9]
y:=&summer // y是指向切片的指针

fmt.Println((*x)[0:3]) // [ January February]
fmt.Println((*y)[0:3]) // [June July August]

2.5 对字符串进行切片操作得到子字符串

注意:

1、虽然字符串的底层就是字节切片,但是对于字符串进行切片操作得到的是一个新的字符串

2、对[]byte进行切片操作会生成一个新的[]byte,注意不是字符串哦

msg := "hi吃鸡吗"

// 对字符串切片得到子字符串,结束位置不能超过字符串的长度
res:=msg[0:5] // 同样是以字节单位
fmt.Println(res) // hi吃
fmt.Printf("%T\n",res) // string

res1:=[]byte(msg)
fmt.Println(res1[0:3])        // [104 105 229]
fmt.Printf("%T\n",res1[0:3])  // []uint8,即[]byte

三 数据类型转换

1、字符串转为切片

[]byte(字符串)

[]rune(字符串)

2、切片转切片

[]rune(切片类型)

四 基本操作

4.1 增删改查

同数组一样,涉及到索引操作时,索引必须是非负数

(1)查询:

取最后一个元素
var cityArray = []string{"北京", "上海", "深圳"}
fmt.Println(cityArray[0])        // 北京
fmt.Println(cityArray[len(cityArray)-1]) // 深圳
fmt.Println(cityArray[-1]) // 错误,索引必须是非负的

(2)修改

cityArray[1] = "魔都"
fmt.Println(cityArray)  
cityArray[666] = "魔都"  // 索引不存在则报错

(3)增加

我们无法根据不存在的索引进行赋值操作来为切片添加元素,

s:=[]int{111,222,333}
s[3]=444 // 异常:index out of range [3] with length 3

需要我们使用内置的append函数,append函数的特点是:哪怕操作的切片是零值nil切片,都不需为其初始化值或用make开辟空间,就可以直接执行append功能开辟空间加添加元素一条龙服务

var s []int // // s == nil
// 1、可以一次添加一个元素
s=append(s,111)  // append可能会发生扩容,底层数组会变,所以一定要接收返回值

// 2、也可以添加多个元素
s=append(s,222,333,444)

// 3、也添加另一个切片中的元素,但是需要在后面加三个点...
s2:=[]int{555,666}
s=append(s,s2...)
fmt.Println(s) // [111 222 333 444 555 666]

(4)删除

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

func main() {
    // 从切片中删除元素
    a := []int{30, 31, 32, 33, 34, 35, 36, 37}
    // 要删除索引为2的元素
    a = append(a[:2], a[3:]...) // 注意第二个参数后面要加三个点...
    fmt.Println(a) //[30 31 33 34 35 36 37]
}

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

4.2 二元运算

(1)切片属于复合类型中的引用类型,复合类型中的引用类型不支持直接进行相等性判断,只能与nil直接进行相等性判断,

ps:引用类型所包含的真实值都来自于或者说间接引用于底层数组中的元素,而我们想比较的都是值而不是引用,比较引用也没有意义,以切片为例,其底层数组的元素都是可以随时被修改的,

// 例1
var x = []int{11,22,33}
var y = []int{11,22,33}
fmt.Println(x == y) // 报错切片只能与nil比较:(slice can only be compared to nil

// 例2
var a []string
var b []int
var c = []int{}

fmt.Println(a == nil) //true
fmt.Println(b == nil) //true
fmt.Println(c == nil) //false
fmt.Println(c != nil) //true

// 例3
var x *int
fmt.Println(x == nil) // true

var mmm = 666
var y *int = &mmm
fmt.Println(y == nil) //  false

如果我们就想要比较像切片这种引用类型是否相等,可不可以呢?也可以,思路就是确保长度一样,然后遍历出对应的元素手动进行比较,前提是

​ I.待比较的两个切片中,对应的元素必须是同类型

​ II.待比较的两个切片中,对应的元素必须是可以进行相等性判断的

func equal(x, y []string) bool {
    if len(x) != len(y) {
        return false // 两个切片类型的长度不一样直接返回false,代表不相等
    }
    for i := range x {
        if x[i] != y[i] {
            return false // 两个切片类型但凡有一组对应值不相等就返回false,代表不相等
        }
    }
    return true // 如果能够执行该行代码,返回true,则代表两个切片是相等的,即底层元素一样
}

(2)切片不可以比大小,若想比较,可以编写函数依次取出元素进行比较,就像字符串比大小的底层原理一样

(3)注意:如果你需要测试一个slice是否是空的,使用len(s) == 0来判断,而不应该用s == nil来判断,如下

//1、一个nil值的slice的长度和容量都是0,如下所示
var s []int    // len(s) == 0, s == nil
s = nil        // len(s) == 0, s == nil
s = []int(nil) // len(s) == 0, s == nil // 基于类型转换表达式[]int(nil)生一个值为nil的切片

//2、当然也有非nil值的切片的长度和容量也是0,例如[]int{}或make([]int, 3)[3:]
s = []int{}    // len(s) == 0, s != nil
s = make([]int, 3)[3:] // 同上

其实一个nil值的slice的行为和其它任意0长度的slice一样;例如reverse(nil)也是安全的。除了文档已经明确说明的地方,所有的Go语言函数应该以相同的方式对待nil值的slice和0长度的slice。

4.3 遍历切片

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

s := []string{"aaa", "bbb", "ccc"}

for i := 0; i < len(s); i++ {
    fmt.Println(i, s[i])
}

for index, value := range s {
    fmt.Println(index, value)
}

4.4 使用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()函数的使用格式如下:

copy(destSlice, srcSlice []T)

copy函数有两个参数:

  • srcSlice: 源切片
  • destSlice: 目标切片

举个例子:

func main() {
    // 1、首先初始化好两个切片,
    s1 := []int{1, 2, 3, 4, 5}
    s2 := make([]int, 5, 5) // 必须为s2开辟好内存空间,零值填充,nil切片会拷贝不成功
    fmt.Println(s1) //[1 2 3 4 5]
    fmt.Println(s2) //[0 0 0 0 0]

    // 2、使用copy()函数将源切片s1中的元素复制到目标切片s2
    copy(s2, s1)     // 注意:源切片s1在后,目标切片s2在前
    fmt.Println(s1) //[1 2 3 4 5]
    fmt.Println(s2) //[1 2 3 4 5]

    // 3、修改s2不会影响s1
    s2[0] = 666
    fmt.Println(s2) // [666 2 3 4 5]
    fmt.Println(s1) // [1 2 3 4 5]
}

五 在函数参数中传递

​ 切片在函数参数中的传递属于“引用传递”,为了让大家更加深刻理解切片的灵活之处,我们对比数组以及数组指针来看一下。

​ 因为数组是值类型,传入函数后,是一个新的副本,在函数内修改并不会影响原数组,若想修改,那么必须传递数组的引用,我们可以传递数组指针

// 接收指针类型*[3]int
func reverse(s *[5]int) {
    for i, j := 0, len(*s)-1; i < j; i, j = i+1, j-1 {
        (*s)[i], (*s)[j] = (*s)[j], (*s)[i]
    }
    //fmt.Println(*s)
}

func main() {
    x := [5]int{1, 2, 3, 4, 5}
    fmt.Println(&x) // &[1 2 3 4 5]

    // 1、传入指针
    reverse(&x) 
    fmt.Println(x) // [5 4 3 2 1]
}

​ 但是指针是对数组整体进行引用,如果我们只想修改数组的某一部分,那么传递数组的切片再合适不过了,因为切片可以引用数组的全部也可以引用部分

// 接收切片类型[]int
func reverse(s []int) {
    for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
        s[i], s[j] = s[j], s[i]
    }
}
func main() {
    x := [5]int{1, 2, 3, 4, 5}

    // 1、传入切片,切片引用了数组的全部元素,此时和传入数组指针的效果一样
    reverse(x[:]) 
    fmt.Println(x) // 数组已经被改成了:[5 4 3 2 1]

    // 2、传入切片,切片只引用了前两个元素
    reverse(x[:2]) //
    fmt.Println(x) // 只翻转了前两个元素:[4 5 3 2 1]

    // 3、传入切片,切片只引用了从索引2一直到最后所有的元素
    reverse(x[2:])
    fmt.Println(x) // [4 5 1 2 3]
}

六 切片扩容

先来复习一个重要的知识:

  • %p对应切片名,打印出的地址是其对应底层数组第一个元素的地址
  • %p对应&切片名,打印的才是切片本身的地址
var names [5]string = [5]string{"egon","EGON","EGON666","tom666","TOM777"}
var xxx []string = names[2:]

fmt.Printf("%p\n",xxx)  // 0xc00005c070,此时打印的是切片xxx首元素的指针,
fmt.Printf("%p\n",&xxx)  // 0xc00000c060,此时打印的才是xxx这个变量本身的地址

注意:如果切片xxx底层数组扩容了,因为xxx是指向底层数组的,所以xxx首元素地址肯定变,但是xxx本身的地址不会变,因为始终都是xxx这一个变量,举例如下

// 1、先声明一个数组x,然后对其进行切片操作得到一个切片y指向该数组
var x [9]int = [9]int{11, 22, 33, 44, 55, 66, 77, 88, 99}
var y = x[1:3:5]  // 切片首元素指向数组的第2个元素,切片的容量为4,可以往里增加值,超过4则扩容指向一个新数组
fmt.Println(y)   // [22 33]

// 2、切片y的首元素指向数组x的第2个元素,所以下述打印输出结果一样
fmt.Printf("%p\n", &x[1]) // 打印数组第2个元素的地址:0xc00012e008
fmt.Printf("%p\n", y)     // 打印切片首元素的地址:0xc00012e008

// 3、修改切片,就会修改其指向的数组,如下往切片y里添加元素其实都添加到了底层数组里,但是需要注意的是数组的长度不变,于是都变成了覆盖
y = append(y, 666) // 添加的666覆盖了底层数组的元素44
y = append(y, 777) // 添加的777覆盖了底层数组的元素55
fmt.Println(y) // [22 33 666 777]
fmt.Println(x) // [11 22 33 666 777 66 77 88 99]

fmt.Printf("%p\n", y) // 打印切片首元素的地址,输出还是同上:0xc000136008 ,因为未达到切片容量4,所以切片仍指向原数组

// 4、继续往切片里添加值,则超过了切片的容量4
y = append(y, 888)
fmt.Println(y)  // [11 22 33 666 777 888]
fmt.Printf("%p\n", y) // 输出新地址:0xc000138000 ,超过当初的切片容量4,切片扩容,指向新的底层数组的首地址

// 5、思考:扩容前后y本身的地址是否改变???可以自己测试一下
答案:不变

​ 扩容规则详解如下:

​ 每个切片会指向一个底层数组,在添加新元素时,如果底层数组的容量够用就正常添加,否则,切片就会自动按照一定的策略进行“扩容”,此时go会为切片创建新的底层数组,然后将原数组元素copy到新数组。

​ “扩容”操作往往发生在append()函数调用时,append()执行之后,底层的数组有可能就换了,所以我们通常都需要用原变量接收append函数的返回值,从而保证原变量指向新的底层数组

// 示例
var numSlice []int //注意,numSlice的地址不会变,但是随着扩容、其底层指向的数组会产生新的,所以numSlice指向的第一个元素的地址会发生变化
fmt.Printf("值:%v 长度:%d 容量:%d 地址:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)

for i := 0; i < 10; i++ {
    numSlice = append(numSlice, i)
    fmt.Printf("值:%v 长度:%d 容量:%d 地址:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)
    // fmt.Printf("%p\n",&numSlice) // 注意注意注意:%p对应&numSlice,读取的是numSlice本身的地址,无论如何扩容,是不会发生变化的,但是%p对应numSlice取的则是numSlice指向的数组的第一个元素地址,是会随着扩容而发生变化的
}

// 输出
值:[] 长度:0 容量:0 地址:0x0 // 初始容量为0,以此容量作为开始且看后续扩容
值:[0] 长度:1 容量:1 地址:0xc00010c020 // 此前容量为0,添加新值,容量不够,扩容为1
值:[0 1] 长度:2 容量:2 地址:0xc00010c040 // 此前容量为1,添加新值,容量不够,扩容为2
值:[0 1 2] 长度:3 容量:4 地址:0xc000118020 // 此前容量为2,添加新值,容量不够,扩容为4
值:[0 1 2 3] 长度:4 容量:4 地址:0xc000118020 // 此前容量为4,添加新值,容量够用
值:[0 1 2 3 4] 长度:5 容量:8 地址:0xc00011a040 // 此前容量为4,添加新值,容量不够,扩容为8
值:[0 1 2 3 4 5] 长度:6 容量:8 地址:0xc00011a040 // 此前容量为8,添加新值,容量够用
值:[0 1 2 3 4 5 6] 长度:7 容量:8 地址:0xc00011a040 // 此前容量为8,添加新值,容量够用
值:[0 1 2 3 4 5 6 7] 长度:8 容量:8 地址:0xc00011a040 // 此前容量为8,添加新值,容量够用
值:[0 1 2 3 4 5 6 7 8] 长度:9 容量:16 地址:0xc00011e000 // 此前容量为8,添加新值,容量不够,扩容16
值:[0 1 2 3 4 5 6 7 8 9] 长度:10 容量:16 地址:0xc00011e000 // 此前容量为8,添加新值,容量够用

​ 基于上述输出结果可以看出

1、append()函数将元素追加到切片的最后并返回该切片。
2、切片numSlice的容量按照1,2,4,8,16这样的规则自动进行扩容,每次扩容后都是扩容前的2倍,注意是之前的2倍,如果起始容量为3,那么接下来的扩容就是6、12、24...

​ 扩展阅读:切片的扩容策略

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

func growslice(et *_type, old slice, cap int) slice {
    // 下面代码省略
    ...

    // 关键代码如下
    newcap := old.cap
    doublecap := newcap + newcap  // doublecap为旧容量的两倍
    if cap > doublecap {          // cap为新申请的容量
        newcap = cap
    } else {
        // 小于1024,则*2扩容
        if old.len < 1024 {
            newcap = doublecap
        } else {
            // 否则,那就是大于等同1024,则*1.25扩容
            // 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
            }
        }
    }

    // 下面代码省略
    ...
}
从上面的代码可以看出以下内容:
1、首先判断,如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量(cap)。
2、否则判断,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即(newcap=doublecap),
3、否则判断,如果旧切片长度大于等于1024,则最终容量(newcap)从旧容量(old.cap)开始循环增加原来的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最终容量(newcap)大于等于新申请的容量(cap),即(newcap >= cap)
4、如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。

需要注意的是

  • 1、切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。
  • 2、每次扩容会涉及到数组的copy,然后生成新的数组(slice指向新的数组),这样会给系统带来额外的开销,通常我们在创建slice的时候,建议使用make函数,更具业务场景给定一个合适的cap大小,避免slice因为扩容而发生底层数组的copy。

七 切片练习题

  • 1、请采用两种方式声明与初始化切片
// 方式一:先声明并开辟空间初始化,然后再往空间里塞进值
var names []string = make([]string,3,3)
names[0] = "egon"
names[1] = "bigegon"
names[2] = "smallegon"

// 方式二:一步到位
var names []string = []string{"aaa","bbbb","ccc"}
fmt.Println(names[0])
  • 2、请写出下面代码的输出结果。
func main() {
    var a = make([]string, 5, 10)
    for i := 0; i < 10; i++ {
        a = append(a, fmt.Sprintf("%v", i))
    }
    fmt.Println(a) // [     0 1 2 3 4 5 6 7 8 9] 前面5个空串,因为make已经开辟了5个空串的空间了
}
  • 3、请使用内置的sort包对数组var a = [...]int{3, 7, 8, 9, 1}进行排序

利用内置的包sort,sort接收的是切片类型,切片是对数组的引用,所以对切片排序就是对数组排序啦

func main(){
    var a = [...]int{3, 7, 8, 9, 1}
    sort.Ints(a[:])
    fmt.Println(a) // [1 3 7 8 9]
}
  • 4、请说出下述代码打印结果
切片类型x[start:stop:max] 顾头不顾尾,不指定max,不写max则默认max为底层数组最大索引,max-star即容量,
// 例1:
var xxx = [5]int{111,222,333,444,555}
yyy:=xxx[0:3]
fmt.Println(len(yyy),cap(yyy))  // 3 5

// 例2:如下数组,0号索引用的是零值
mons:=[...]string{1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December"}
fmt.Println(len(mons))  // 13

mon1:=mons[2:5:7] // max为7,I必须比5大,cap为5
mon2:=mons[1:3]   // max默认为13,cap为12
fmt.Println(mon1,len(mon1),cap(mon1))
fmt.Println(mon2,len(mon2),cap(mon2))
  • 5、对切片再进行切片操作
对切片再进行切片操作,结束位置不能超过切片的容量,也就是说对谁切,就以谁为准,这句话适用于切片操作的一切

var xxx = make([]int,3,5)  // 切片xxx的容量为5
xxx[0]=111
xxx[1]=222
xxx[2]=333
//xxx[3]=444 // 报错,如果想添加必须用append
fmt.Println(len(xxx),cap(xxx)) // 3 5

fmt.Println(xxx[0:3])
fmt.Println(xxx[0:4])
fmt.Println(xxx[0:5])
fmt.Println(xxx[0:6])  // 报错,不能超过切片的容量5
  • 6、切片slice的元素为map类型:切记一点,引用类型的零值都是nil,请说出下述代码的结果
// 例1
var mapSlice []map[string]int
fmt.Println(mapSlice == nil) // 输出???答案:true

// 例2
var mapSlice=make([]map[string]int,8,8)
fmt.Println(mapSlice == nil) // 输出结果是???答案:false
mapSlice[0]["数学"] = 100 // 能否赋值成功???答案:不能

// 虽然上述结果为false,但也只是完成了切片mapSlice的初始化,并没有完成对所包含元素的初始话
// 我们应该为其赋予初始化的值
mapSlice[0]= map[string]int{"数学":99,"英语":98,"语文":100}
fmt.Println(mapSlice)
  • 7、选取合适的数据类型存放每个国家的重要城市
// 实现:map的key为string类型,但value为切片类型
var sliceMap=make(map[string][]string)
sliceMap["中国"]=make([]string,3)
sliceMap["中国"][0]="北京"
sliceMap["中国"][1]="上海"
sliceMap["中国"][2]="深圳"

sliceMap["美国"]=make([]string,3)
sliceMap["美国"][0]="纽约"
sliceMap["美国"][1]="洛杉矶"
sliceMap["美国"][2]="芝加哥"

fmt.Println(sliceMap["中国"]) // map[中国:[北京 上海 深圳]]
fmt.Println(sliceMap["美国"]) // 美国 [纽约 洛杉矶 芝加哥]

for k,v:=range sliceMap{
    fmt.Println(k,v)
    // loop1:中国 [100 200 300 0 0 0 0 0]
    // loop2:美国 [纽约 洛杉矶 芝加哥]

}
  • 8、使用slice来模拟stack操作,入栈即向slice中append元素,出栈则通过收缩slice,弹出栈顶的元素:
stack := []int{11, 22, 33}

// 入栈, push
stack = append(stack, 666)
fmt.Println(stack)

// 出栈, pop
stack = stack[:len(stack)-1]
fmt.Println(stack)
  • 9、通过内置的copy函数将后面的子slice向前依次移动一位
// 把i+1到最后的元素从i索引的位置开始拷贝/覆盖
copy(list[i:],list[i+1:])  

例
list := []int{1,2,3,4,5,6}
copy(list[1:],list[1+1:]) 
fmt.Println(list)  // [1 3 4 5 6 6]
  • 10、用最后一个元素覆盖想删除的元素:
func remove(slice []int, i int) []int {
    // 使用最后一个元素覆盖要删除的元素
    slice[i] = slice[len(slice)-1]
    // 返回新的slice
    return slice[:len(slice)-1]
}
  • 11、输入切片和输出切片共用一个底层数组,这可以避免分配另一个数组,不过原来的数据将可能会被覆盖:例如
func clearEmpty(strings []string) []string {
    i := 0
    for _, s := range strings {
        if s != "" {
            strings[i] = s
            i++  // 注意i不是每次都累加1,而是满足s!=""条件才会
        }
    }
    return strings[:i]  // 这里可以用到i,i的值为1不是2不是2不是2
}

srcSlice := []string{"egon1", "", "egon3"}
dstSlice := clearEmpty(srcSlice)

fmt.Printf("%q\n", dstSlice) // ["egon1" "egon3"]
fmt.Printf("%q\n", srcSlice) // ["egon1" "egon3" "egon3"]

同样的,使用append也能实现同样的功能:

func clearEmpty2(strings []string) []string {
    out := strings[:0] // 0长度切片,与strings切片同指向一个底层数组,out的容量为3
    for _, s := range strings {
        if s != "" {
            out = append(out, s)
        }
    }
    return out
}

srcSlice := []string{"egon1", "", "egon3"}
//dstSlice := clearEmpty(srcSlice)
dstSlice := clearEmpty2(srcSlice)

fmt.Printf("%q\n", dstSlice) // ["egon1" "egon3"]
fmt.Printf("%q\n", srcSlice) // ["egon1" "egon3" "egon3"]

无论如何实现,以这种方式重用一个slice一般都要求最多为每个输入值产生一个输出值,事实上很多这类算法都是用来过滤或合并序列中相邻的元素。

这种slice用法是比较复杂的技巧,虽然使用到了slice的一些技巧,但是对于某些场合是比较清晰和有效的。

上一篇
下一篇
Copyright © 2022 Egon的技术星球 egonlin.com 版权所有 帮助IT小伙伴学到真正的技术