数组以及切片

[TOC]

1. Arrays

golang数组与基本静态语言的数组一致,由于golang是强类型的语言,所以数组元素必须类型相同。数组的基本声明方式如下:

1.1 数组声明

  • 定长数组

    使用var arr [n]T 进行声明长度为n, 类型为T的数组对象,采用这种方式进行声明,每个元素根据数组类型赋予相应的初始值,例如int初值为0

    也可以在声明时向数组赋予初始值,a := [3]int{1, 3, 6} ,可以不全赋值,其余位置会被默认值填充

    // 初始值测试
    package main
    
    import (
        "fmt"
        "reflect"
    )
    
    func main() {
        var a [3]int //int array with length 3
    
        var (
            b [3]float32
            c [3]float64
            d [3]string
            e [3]bool
            f [3]byte
            g [3]rune
            h [3]complex64
            i [3]int8
        )
        fmt.Println(reflect.ValueOf(a[0]).Kind(), a,
        reflect.ValueOf(b[0]).Kind(), b, 
        reflect.ValueOf(c[0]).Kind(), c, 
        reflect.ValueOf(d[0]).Kind(), d, 
        reflect.ValueOf(e[0]).Kind(), e, 
        reflect.ValueOf(f[0]).Kind(), f, 
        reflect.ValueOf(g[0]).Kind(), g, 
        reflect.ValueOf(h[0]).Kind(), h,
        reflect.ValueOf(i[0]).Kind(), i)
    }
    int [0 0 0] float32 [0 0 0] float64 [0 0 0] string [  ] bool [false false false] uint8 [0 0 0] int32 [0 0 0] complex64 [(0+0i) (0+0i) (0+0i)] int8 [0 0 0]
  • 自定长度数组

    可以使用[...]T{.., .., } 进行数组的声明,编译器会自动根据给定元素给数组确定长度

    但是后面必须跟上数组元素的声明,不可以不指定元素

    func main() {
        a := [...]int{1, 2, 1e3, 2e4} // ... makes the compiler determine the length
        fmt.Println(a)
    }

    注意数组长度也是数组类型的一部分,对于强类型语言来说,不同长度的数组不属于同一类型,所以将不同长度的数组进行赋值是不被允许的

1.2. 数组以值传递

golang中的数组是value type而不是reference type ,也就是说,进行数组间的相互赋值时,使用的不是同一个内存空间,开辟了另一个内存空间用以存放新的数组,所以改变新的数组中元素并不会改变原数组的值,也就是说,使用的是深复制。

所以数组作为参数进行传递时,也是按值传递,不会改变原数组的元素。

func changeLocal(num [5]int) {  
    num[0] = 55
    fmt.Println("inside function ", num)

}
func main() {  
    num := [...]int{5, 6, 7, 8, 8}
    fmt.Println("before passing to function ", num)
    changeLocal(num) //num is passed by value
    fmt.Println("after passing to function ", num)
}

关于数组的长度可以使用len()函数求取

2. 可迭代对象的遍历

对于数组等可迭代对象,可以使用基本的for循环进行,也可以使用range 函数,进行迭代,每次迭代包含index, value两个值进行使用,如果需要忽略index可以使用空白操作符,忽略value可以只进行index的赋值

for i, v := range list {
    fmt.Println(i, v)
}

3. 切片

数组虽然已经足够灵活,但是必须是确定长度,不支持变长操作。切片类型是基于数组的一个顶层wrapper ,切片本身并不存放任何数据,他们只是某些已经存在的数组的引用

3.1 切片声明

a := [5]int{1, 2, 67, 76, 88}
var b []int = a[1:4]
// 1, 2, 67

a := [5]int{76, 77, 78, 79, 80}
var b  = a[1:4] //creates a slice from a[1] to a[3]
fmt.Println(b[1:4], b)  // 注意此处slice的len为3,cap为4,不可以用[]访问,但是可以使用[:]格式进行访问,

切片的类型用不需要长度的[]int表示,对于数组的[:] 操作,与python用法一致,但是不允许负值操作,而且不允许倒序操作,而且最多只接受两个参数,同样,包含下界不包含上界。

使用make() 函数声明切片

func make([]T, len, cap int) []T

用于生成一个切片,传入的是类型,容量以及长度,值采用默认值填充。

3.2 切片机制

切片不拥有任何数据,只是作为底层数组的引用,所以基于切片的数据修改,会直接反映在底层数组对象上。所以公用同一个底层数组的切片,如果对其进行更改,会映射到每一个切片上去。

    numa := [3]int{78, 79 ,80}
    nums1 := numa[:] //creates a slice which contains all elements of the array
    nums2 := numa[:]
    fmt.Println("array before change 1",numa)
    nums1[0] = 100
    fmt.Println("array after modification to slice nums1", numa)
    nums2[1] = 101
    fmt.Println("array after modification to slice nums2", numa)

长度与容量:

切片的长度使用len()函数,表示的是切片可表示的元素的数量;切片的容量是从切片开始位置到数组最后一个元素的长度。

一个切片可以被重新切片,并且增长到其容量,如果超出范围会抛出运行时错误。

fruitarray := [...]string{"apple", "orange", "grape", "mango", "water melon", "pine apple", "chikoo"}
fruitslice := fruitarray[1:3]
fmt.Printf("length of slice %d capacity %d\n", len(fruitslice), cap(fruitslice)) //length of is 2 and capacity is 6
fruitslice = fruitslice[:cap(fruitslice)] //re-slicing furitslice till its capacity
fmt.Println("After re-slicing length is",len(fruitslice), "and capacity is",cap(fruitslice))        // 6, 6

结构特征

slices可以被表示为一个结构体

type slice struct {
    Length int
    Capacity int
    ZerothElement *byte    // 头指针
}

一个切片包含了长度,容量,以及指向数组第一个元素的指针,但一个切片传作为参数传递时,虽然是按值传递,但是指针变量仍然会指向同一个底层数组。所以使用切片进行参数传递时,内部的改变会影响到函数外部.

3.3 基本操作

  • Append()

    func append([]T, x ...T) []T
    // 用于动态扩充一个切片,参数x ...T表示接受多个类型为T的值作为参数,可变长参数
    // 当一个切片进行扩充时,如果长度与容量相等,表示空间已经用完
    // 此时建立一个底层新的数组,容量为原来2倍并且把原数组的数据copy到新的数组中去,形成新的底层数组
    // 并且把新数组的引用赋给切片
    cars := []string{"Ferrari", "Honda", "Ford"}
    fmt.Println("cars:", cars, "has old length", len(cars), "and capacity",cap(cars)) //capacity of cars is 3
    cars = append(cars, "Toyota")
    fmt.Println("cars:", cars, "has new length", len(cars), "and capacity",cap(cars)) //capacity of cars is doubled to 6

    append不仅可以追加单个或者多个元素,而且可以追加另外一个slice, 使用...操作符即可

    fruits := []string{"oranges", "apples"}
    veg := []string{"potatoes"}
    food := append(veg, fruits...)
  • copy

    func copy(dst, src []T) int

    将切片的容量扩充2倍,可以使用

    t := make([]byte, len(s), (cap(s)+1)*2)   // cap可能为0
    copy(t, s)
    s = t
    // s = append(s, x ...T)

3.4 空值nil与多维slice

一个slice 的length , capacity均为0,则该切片为空nil ,对于一个控切片,可以使用append() 进行扩充。

使用多维slice只需要在声明是将其指定为多维即可

pls := [][][]string{
    {
      {"My", "programming"},
      {"Language"},
    },
    {
      {"c", "c++"},
      {"Python", "JavaScript"},
      {"Go", "Rust"},
    },
  }

注意使用多维数组时,只允许最高纬度使用...运算符,表示可以不确定其维度,但是其他维度必须标识,如果不标识则内部存放的是slice

var te [...][][]string
var te [...][2][3]int

4. 切片的优化

正如前面所说,切片操作并不会复制底层的数组。整个数组将被保存在内存中,直到它不再被引用。 有时候可能会因为一个小的内存引用导致保存所有的数据。

例如, FindDigits 函数加载整个文件到内存,然后搜索第一个连续的数字,最后结果以切片方式返回。

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

因为切片引用了原始的数组, 导致 GC 不能释放数组的空间;只用到少数几个字节却导致整个文件的内容都一直保存在内存里。

改进方法是将该切片复制到一个新的切片中,释放原内存。

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}
// c := make([]byte, 0)
// c = append(c, b...)

Last updated