← 返回文章

数组和切片

两者区别、切片的介绍

参考文章

Go编程模式:切片,接口,时间和性能

数组和切片申明

  1. 数组声明

数组中,没有初始化的数组,默认值为 0。

 // 没有初始化的数组,默认值为 0 
 var arr1 [3]int 
 var arr2 [4]string

初始化数组的方式有四种:

// 初始化的数组 
// 方式一 
var arr1 [3]int 
arr1[0] = 13 
arr1[1] = 4 
arr1[2] = 29 
 
// 方式二 
var arr2 = [4]int{23,12,42} 
 
// 方式三
arr3 := [...]int{1,2} 
 
// 方式四
arr4 := [...]int{1:1,3:5} // key : value 

定义多维数组:

 var arr = [3][2]string{
  {"北京","上海"}, 
  {"广州","深圳"}, 
  {"成都","重庆"}, 
 } 
 fmt.Println(arr[0]) 
 fmt.Println(arr[0][1]) 
  1. 切片声明

切片中,没有初始化的切片, 默认数据是 nil

 // 没有初始化的切片 
 var arr []int 
 // 没有数据, 切片中默认数据是 nil 
 fmt.Println(arr1 == nil) // true 
 
//方式二 make() 声明一个切片 make([]T, size, cap) 
var sliceA = make([]int,4,8) 

初始化切片的方式有二种:

 // 初始化的切片 
 // 方式一 
 var arr2 = []int{1,2,3,4,5} 
 
 // 方式二 
 var arr3 = []int{1:2, 3:5} // key : value 
 
 // 方式三  基于数组来定义切片 
  a := [5]int{1,2,3,4,5} 
  b := a[:] // 获取数组中的所有值 
  // b 就是切片 
  c := a[1:3] //左包右不包 [2,3] 
  1. 总结:

带长度值便是 数组,不带便是 切片

数组和切片的区别

  1. 数组的长度不可变

切片是增长和收缩的动态序列

  1. 数组是 值类型

切片是 引用类型

切片的介绍

切片在 go 语言中并不是数组,而是以结构体的形式存在着,其定义如下:

type slice struct {
    array unsafe.Pointer // 指向存放数据的数组指针
    len   int            // 长度
    cap   int            // 容量
}

熟悉 C/C++ 的同学一定会知道,在结构体里用数组指针的问题——数据会发生共享

接下来看一下slice的一些操作:

foo = make([]int, 5)
foo[3] = 2  
foo[4] = 10 

bar  := foo[1:4] 
bar[1] = 9

通过图示来进一步的理解以下上面的代码

img

通过上图就能很容易理解,为什么修改 bar 会改变原切片的值,为什么 bar 的 cap 是4,是因为它们数据共享。

插一嘴:

cap 介绍

cap 是切片的容量。

切片的容量:指的是从它的第一个元素开始数,到其底层数组元素末尾的个数。

所以,现在就能理解了 bar 的 cap 是 4 了。

cap 的作用

容量 ( cap ) 的作用:在 数据拷贝和内存申请的消耗 与 内存占用 之间提供一个权衡。

例如:在用 append 扩展长度时,若新的长度小于容量,不会更换底层数组,否则,会新申请一个底层数组,拷贝这边的值过去,把原来的数组丢掉。

接下来,使用一个 append 方法来进一步的理解 切片(slice)中 cap 。

a := make([]int, 32)
b := a[1:16]
a = append(a, 1)
a[2] = 42

上述代码的含义:把 a[1:16] 的切片赋给到了 b ,此时,a 和 b 的内存空间是共享的。然后,对 a 做了一个 append() 的操作,这个操作会让 a 重新分享内存,导致 a 和 b 不再共享,如下图所示:

img

从上图可以看以看到 append() 操作让 a 的容量变成了64,而长度是33。这里,需要重点注意一下——append()这个函数在cap****不够用的时候就会重新分配内存以扩大容量,而如果够用的时候不不会重新分享内存。

同时,最好在初始化时就将 容量设置为适当的值,不然,appned 扩容是以 2的n次方扩容的。

再看一个有意思的例子:

func main() {
    path := []byte("AAAA/BBBBBBBBB")
    sepIndex := bytes.IndexByte(path,'/')

    dir1 := path[:sepIndex]
    dir2 := path[sepIndex+1:]

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAA
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => BBBBBBBBB

    dir1 = append(dir1,"suffix"...)

    fmt.Println("dir1 =>",string(dir1)) //prints: dir1 => AAAAsuffix
    fmt.Println("dir2 =>",string(dir2)) //prints: dir2 => uffixBBBB
}

dir1 和 dir2 都分别共享 path 的部分内存数据,虽然dir1有一个append()操作,但是因为 cap 足够,于是数据扩展到了dir2的空间。

img

如果想要解决dir1 扩容,同时不影响 dir2 的数据:只需要修改一行代码:

dir1 := path[:sepIndex]
// 改为
dir1 := path[:sepIndex:sepIndex]

新的代码使用了 Full Slice Expression,其最后一个参数叫“Limited Capacity”,于是,后续的append()操作将会导致重新分配内存。

插一嘴

Full Slice Expression 介绍

slice 定义的全部表达式其实有三个变量:[low : high : max] 我们最常用的是前两个,第三个不写默认省略。

切片的长度 len 为:high - low

切片的容量 max 为:max - low

// input[low:high:max]
// Length of the the result is high — low
// Capacity  of the result is max — low

// 例如:
numbers := [10]int{0,1,2,3,4,5,6,7,8,9}
s := numbers[2:4:6]
fmt.Println(s) // [2, 3]
fmt.Println(cap(s)) // 4

切片的方法

  1. 添加数据

使用 append 方法

var sliceA []int 
sliceA = append(sliceA, 12) 
sliceA = append(sliceA, 12, 23, 1) 
 
 
// append 方法还可以合并切片 
// 注意: append 合并切片时,最后一个元素要加 ...
slice1 := []string{"php","java"} 
slice2 := []string{"nodejs","golang"} 
 
slice1 = append(slice1, slice2...)
  1. 复制操作

使用 copy 方法

sliceA := []int{1,2,3,4} 
sliceB := sliceB 
sliceB[0] = 11  // sliceA = [11,2,3,4] 
 
sliceC = make([]int, 4, 4) 
copy(sliceC, sliceA) 
sliceC[0] = 222  // sliceA = [11,2,3,4]
  1. 删除元素

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

a := []int{1, 2, 3, 4, 5, 6, 7} 
// 删除索引为 2 的元素, 注意: append 合并切片时,最后一个元素要加 ... 
a = append(a[:2], a[3:]...)
  1. 修改切片中结构体对象的值
type generalCates struct { 
    Name string 
} 
type category struct { 
    ID string 
    general generalCates 
} 

var cates []category 
 
// 修改 cates 切片中 genreal 中的 Name 值 
for index, cate := range cates { 
    cates[index].General.Name = "另一个值" 
    // 修改 cates 中的值时,必须用 cates + 索引 修改值。 
    // 不能使用 cate 修改值,因为循环结束时候临时变量的作用域消失,并没有真正的修改元素的属性; 
} 
 
  1. 排序

使用 sort 包中的方法对切片排序

  1. 升序排序

对于int、 float64 和 string 数组或是切片的排序,go 分别提供了sort.Ints()、sort.Float64s() 和 sort.Strings() 函数,默认都是从小到大排序。

intList := []int{1, 2, 34, 5, 6, 56, 7, 8} 
float8List := []float64{4.2, 23.4, 13.2, 12.4} 
stringList := []string{"a", "d", "f", "s", "x", "r", "g"} 
 
sort.Ints(intList) 
sort.Float64s(float8List) 
sort.Strings(stringList) 
 
fmt.Println(intList) 
fmt.Println(float8List) 
fmt.Println(stringList)
  1. 降序排序

Golang 的 sort 包可以使用 sort.Reverse(slice) 来调换 sice.Interface.Less ,也就是比较函数,所以,int、 float64 和 string 的逆序排序函数可以这么写。

intList := []int{1, 2, 34, 5, 6, 56, 7, 8} 
float8List := []float64{4.2, 23.4, 13.2, 12.4} 
stringList := []string{"a", "d", "f", "s", "x", "r", "g"} 
 
sort.Sort(sort.Reverse(sort.IntSlice(intList))) 
sort.Sort(sort.Reverse(sort.Float64Slice(float8List))) 
sort.Sort(sort.Reverse(sort.StringSlice(stringList))) 
 
fmt.Println(intList) 
fmt.Println(float8List) 
fmt.Println(stringList)

扩充知识

  1. 什么是值类型,什么是引用类型

值类型:改变变量副本值的时候,不会改变本身的值

引用类型:改变变量副本值的时候,会改变本身的值

切片是引用类型 , 赋值是赋的是地址,一个改变,另一个也改变

Go 中的 基本数据类型 和 数组 都是值类型,赋值是内容,相当于重新拷贝了一份

  1. unsafe.Pointer 是什么

它是 go 语言中的指针。还需要开一章讲一讲