Go语言学习笔记(五)------数组与切片

一、声明和初始化

1.数组是具有相同唯一类型的一组已编号且长度固定的数据项序列(这是一种同构的数据结构);这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。数组长度必须是一个常量表达式,并且必须是一个非负整数。数组长度也是数组类型的一部分,所以[5]int和[10]int是属于不同类型的,数组的编译时值初始化是按照数组顺序完成的。如果我们想让数组元素类型为任意类型的话可以使用空接口作为类型。,当使用值时我们必须先做一个类型判断。

2.数组元素可以通过 索引(位置)来读取(或者修改),索引从 0 开始,第一个元素索引为 0,第二个索引为 1,以此类推。元素的数目,也称为长度或者数组大小必须是固定的并且在声明该数组时就给出(编译时需要知道数组长度以便分配内存);数组长度最大为 2Gb。声明的格式是:var identifier [len]type。例如:var arr1 [5]int,每个元素是一个整型值,当声明数组时所有的元素都会被自动初始化为默认值 0,arr1 的长度是 5,索引范围从 0 到 len(arr1)-1 ,对索引项为 i 的数组元素赋值可以这么操作: arr[i] = value ,所以数组是 可变的。

3.遍历数组的方法自然就是使用 for 结构:通过 for 初始化数组项、通过 for 打印数组元素、通过 for 依次处理元素。for 循环中的条件非常重要: i < len(arr1) ,如果写成 i <= len(arr1) 的话会产生越界错误。

//IDIOM:(风格)
for i:=0; i < len(arr1); i++{
arr1[i] = ...
}

也可以使用 for-range 的生成方式,在这里i也是数组的索引。当然这两种 for 结构对于切片(slices)来说也同样适用:

//IDIOM:
for i,_:= range arr1 {
...
}

4.Go 语言中的数组是一种 值类型,所以可以通过 new() 来创建: var arr1 = new([5]int) 。new(T) 分配类型 T 的零值并返回其地址,也就是指向类型 T 的指针 ,而 arr2的类型是 [5]int 。这样的结果就是当把一个数组赋值给另一个时,需要在做一次数组内存的拷贝操作。例如:
arr2 := *arr1
arr2[2] = 100
这样两个数组就有了不同的值,在赋值后修改 arr2 不会对 arr1 生效。所以在函数中数组作为参数传入时,如 func1(arr2) ,会产生一次数组拷贝,func1 方法不会修改原始的数组 arr2。如果想修改原数组,那么 arr2 必须通过&操作符以引用方式传过来,例如 func1(&arr2)。另一种方法就是生成数组切片并将其传递给函数。

package main
import "fmt"
func f(a [3]int) { fmt.Println(a) }
func fp(a *[3]int) { fmt.Println(a) }
func main() {
	var ar[3]int
	f(ar) // passes a copy of ar
	fp(&ar) // passes a pointer to ar
	var arr2 [3]int
	var arr1 = new([3]int) //生成指针类型的数组
	arr3 := arr1 //直接地址指针拷贝
	arr2 = *arr1 //内容拷贝
	arr2[1] = 100 //内容拷贝不影响原数组
	arr3[2] =200  //指针地址拷贝影响原数组
	f(arr2)
	fp(arr1)
	fp(arr3)
}

5.如果数组值已经提前知道了,那么可以通过数组常量的方法来初始化数组,例如:第一章变化:var arrAge = [6]int{18, 20, 15, 22, 16},第六个为默认值0。第二种变化:var arrLazy = [...]int{5, 6, 7, 8, 22},... 可同样可以忽略,从技术上说它们其实变化成了切片。第三种变化:: key: value syntax,var arrKeyValue = [5]string{3: "Chris", 4: "Ron"},只有索引 3 和 4 被赋予实际的值,其他元素都被设置为空的字符串,所以输出结果为:

Person at 0 is
Person at 1 is
Person at 2 is
Person at 3 is Chris
Person at 4 is Ron

在这里数组长度同样可以写成 ... 或者直接忽略。

6.取任意数组常量的地址来作为指向新实例的指针。

package main
import "fmt"
func fp(a *[3]int) { fmt.Println(a) }
func main() {
for i := 0; i < 3; i++ {
fp(&[3]int{i, i * i, i * i * i})
}
}

7.数组通常是一维的,但是可以用来组装成多维数组,例如: [3][5]int , [2][2][2]float64 。内部数组总是长度相同的,Go 语言的多维数组是矩形式的。

8.将数组传递给函数,把一个大数组传递给函数会消耗很多内存。有两种方法可以避免这种现象:传递数组的指针(但这在 Go 中并不常用,通常使用切片)、使用数组的切片。下面介绍传递数组的指针:

package main
import "fmt"
func main() {
array := [3]float64{7.0, 8.5, 9.1}
x := Sum(&array) // Note the explicit address-of operator
// to pass a pointer to the array
fmt.Printf("The sum of the array is: %f", x)
}
func Sum(a *[3]float64) (sum float64) {
for _, v := range a { // derefencing *a to get back to the array is not necessary!
sum += v
}
return
}

二、切片

1.切片(slice)是对数组一个连续片段的引用(该数组我们称之为相关数组,通常是匿名的),所以切片是一个引用类型。这个片段可以是整个数组,或者是由起始和终止索引标识的一些项的子集。需要注意的是,终止索引标识的项不包括在切片内。切片提供了一个相关数组的动态窗口。切片是可索引的,并且可以由 len() 函数获取长度。给定项的切片索引可能比相关数组的相同元素的索引小。和数组不同的是,切片的长度可以在运行时修改,最小为 0 最大为相关数组的长度:切片是一个 长度可变的数组。

2.切片提供了计算容量的函数 cap() 可以测量切片最长可以达到多少::它等于切片的长度 + 数组除切片之外的长度。。如果 s 是一个切片, cap(s) 就是从 s[0] 到数组末尾的数组长度。切片的长度永远不会超过它的容量,所以对于 切片 s来说该不等式永远成立: 0 <= len(s) <= cap(s) 。多个切片如果表示同一个数组的片段,它们可以共享数据;因此一个切片和相关数组的其他切片是共享存储的,相反,不同的数组总是代表不同的存储。优点 因为切片是引用,所以它们不需要使用额外的内存并且比使用数组更有效率,所以在 Go 代码中 切片比数组更常用。

3.声明切片的格式是: var identifier []type (不需要说明长度)。一个切片在未初始化之前默认为 nil,长度为 0。切片的初始化格式是: var slice1 []type = arr1[start:end] 。表示 slice1 是由数组 arr1 从 start 索引到 end-1 索引之间的元素构成的子集(切分数组,start:end 被称为 slice 表达式)。所以 slice1[0] 就等于 arr1[start] 。这可以在 arr1 被填充前就定义好。

4.rr1[:] 那么 slice1 就等于完整的 arr1 数组(所以这种表示方式是arr1[0:len(arr1)] 的一种缩写)。另外一种表述方式是: slice1 = &arr1 。
arr1[2:] 和 arr1[2:len(arr1)] 相同,都包含了数组从第三个到最后的所有元素。arr1[:3] 和 arr1[0:3] 相同,包含了从第一个到第三个元素(不包括第三个,包括第0个)。

5.一个由数字 1、2、3 组成的切片可以这么生成: s := [3]int{1,2,3}[:] (注: 应先用 s := [3]int{1, 2, 3} 生成数组, 再使用 s[:] 转成切片) 甚至更简单的 s := []int{1,2,3} 。s2 := s[:] 是用切片组成的切片,拥有相同的元素,但是仍然指向相同的相关数组。一个切片 s 可以这样扩展到它的大小上限: s = s[:cap(s)]。

6.切片在内存中的组织方式实际上是一个有 3 个域的结构体:指向相关数组的指针,切片长度以及切片容量。

7.切片示例程序:

package main
import "fmt"
func main() {
	var arr1 [6]int
	var slice1 []int = arr1[2:5] // item at index 5 not included!
	// load the array with integers: 0,1,2,3,4,5
	for i := 0; i < len(arr1); i++ {
		arr1[i] = i
	}
	// print the slice
	for i := 0; i < len(slice1); i++ {
		fmt.Printf("Slice at %d is %d\n", i, slice1[i])
	}
	fmt.Printf("The length of arr1 is %d\n", len(arr1))
	fmt.Printf("The length of slice1 is %d\n", len(slice1))
	fmt.Printf("The capacity of slice1 is %d\n", cap(slice1)) //从切片2开始到数组最后一个
	// grow the slice
	slice1 = slice1[0:4]
	for i := 0; i < len(slice1); i++ {
		fmt.Printf("Slice at %d is %d\n", i, slice1[i])
	}
	fmt.Printf("The length of slice1 is %d\n", len(slice1))
	fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
	// grow the slice beyond capacity
	//slice1 = slice1[0:7 ] // panic: runtime error: slice bound out of range
}

8. 将切片传递给函数:如果你有一个函数需要对数组做操作,你可能总是需要把参数声明为切片。当你调用该函数时,把数组分片,创建为一个切片引用并传递给该函数,这里有一个计算数组元素和的方法:

func sum(a []int) int {
s := 0
for i := 0; i < len(a); i++ {
s += a[i]
}
return s
}
func main() {
var arr = [5]int{0, 1, 2, 3, 4}
sum(arr[:])
}

9.当相关数组还没有定义时,我们可以使用 make() 函数来创建一个切片 同时创建好相关数组: var slice1 []type = make([]type, len) 。
也可以简写为 slice1 := make([]type, len) ,这里 len 是数组的长度并且也是 slice 的初始长度。所以定义 s2 := make([]int, 10) ,那么 cap(s2) == len(s2) == 10 ,make 接受 2 个参数:元素的类型以及切片的元素个数。如果你想创建一个 slice1,它不占用整个数组,而只是占用以 len 为个数个项,那么只要: slice1 := make([]type, len,cap) 。make 的使用方式是: func make([]T, len, cap) ,其中 cap 是可选参数。因为字符串是纯粹不可变的字节数组,它们也可以被切分成 切片。示例:

package main
import "fmt"
func main() {
	var slice1 []int = make([]int, 10)
	// load the array/slice:
	for i := 0; i < len(slice1); i++ {
		slice1[i] = 5 * i
	}
	// print the slice:
	for i := 0; i < len(slice1); i++ {
		fmt.Printf("Slice at %d is %d\n", i, slice1[i])
	}
	fmt.Printf("\nThe length of slice1 is %d\n", len(slice1))
	fmt.Printf("The capacity of slice1 is %d\n", cap(slice1))
}

10.下面两种方法可以生成相同的切片:make([]int, 50, 100)、new([100]int)[0:50]。new(T) 为每个新的类型T分配一片内存,初始化为 0 并且返回类型为*T的内存地址:这种方法 返回一个指向类型为T,值为 0 的地址的指针,它适用于值类型如数组和结构体;它相当于 &T{} 。make(T) 返回一个类型为 T 的初始值,它只适用于3种内建的引用类型:切片、map 和 channel。换言之,new 函数分配内存,make 函数初始化。

11.多维 切片:和数组一样,切片通常也是一维的,但是也可以由一维组合成高维。通过分片的分片(或者切片的数组),长度可以任意动态变化,所以 Go 语言的多维切片可以任意切分。

12.bytes 包:

import "bytes"
type Buffer struct {
...
}

类型 Buffer:这是一个长度可变的 bytes 的 buffer,提供 Read 和 Write 方法,因为读写长度未知的 bytes 最好使用 buffer。Buffer 可以这样定义: var buffer bytes.Buffer 。或者使用 new 获得一个指针: var r *bytes.Buffer = new(bytes.Buffer) 。或者通过函数: func NewBuffer(buf []byte) *Buffer ,创建一个 Buffer 对象并且用 buf 初始化好;NewBuffer 最好用在从 buf 读取的时候使用。在下面的代码段中,我们创建一个 buffer,通过 buffer.WriteString(s) 方法将字符串 s 追加到后面,最后再通过buffer.String() 方法转换为 string:

//这种实现方式比使用 += 要更节省内存和 CPU,尤其是要串联的字符串数目特别多的时候。
var buffer bytes.Buffer
for {
//如果获得数据getNextString没有错误,就一直执行下去
if s, ok := getNextString(); ok { //method getNextString() not shown here
buffer.WriteString(s)
} else {
break
}
}
fmt.Print(buffer.String(), "\n")

三、For-range 结构

1.For-range 结构构建方法可以应用于数组和切片,第一个返回值 ix 是数组或者切片的索引,第二个是在该索引位置的值;他们都是仅在 for 循环内部可见的局部变量。value 只是 slice1 某个索引位置的值的一个拷贝,不能用来修改 slice1 该索引位置的值。

for ix, value := range slice1 {
...
}

2.如果你只需要索引,你可以忽略第二个变量,示例:

package main
import "fmt"
func main() {
seasons := []string{"Spring", "Summer", "Autumn", "Winter"}
for ix, season := range seasons {
fmt.Printf("Season %d is: %s\n", ix, season)
}
var season string
for _, season = range seasons {
fmt.Printf("%s\n", season)
}
}

四、切片重组(reslice)

切片初始长度而 capacity 作为相关数组的长度。我们的切片在达到长度上限后可以扩容。改变切片长度的过程称之为切片重组 reslicing,做法如下: slice1 = slice1[0:end] ,其中 end 是新的末尾索引(即长度)。将切片扩展 1 位可以这么做:sl = sl[0:len(sl)+1],切片可以反复扩展直到占据整个相关数组。示例:

package main
import "fmt"
func main() {
slice1 := make([]int, 0, 10)
// load the slice, cap(slice1) is 10:
for i := 0; i < cap(slice1); i++ {
slice1 = slice1[0:i+1]
slice1[i] = i
fmt.Printf("The length of slice is %d\n", len(slice1))
}
// print the slice:
for i := 0; i < len(slice1); i++ {
fmt.Printf("Slice at %d is %d\n", i, slice1[i])
}
}

五、切片的复制与追加

1.如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来。下面的代码描述了从拷贝切片的 copy 函数和向切片追加新元素的 append 函数。func append(s[]T, x ...T) []T 其中 append 方法将 0 个或多个具有相同类型 s 的元素追加到切片后面并且返回新的切片;追加的元素必须和原切片的元素同类型。如果 s 的容量不足以存储新增元素,append 会分配新的切片来保证已有切片元素和新增元素的存储。因此,返回的切片可能已经指向一个不同的相关数组了。append 方法总是返回成功,除非系统内存耗尽。

package main
import "fmt"
func main() {
	sl_from := []int{1, 2, 3}
	sl_to := make([]int, 10)
	n := copy(sl_to, sl_from)
	fmt.Println(sl_to)
	fmt.Printf("Copied %d elements\n", n) // n == 3
	sl3 := []int{1, 2, 3}
	sl3 = append(sl3, 4, 5, 6)
	fmt.Println(sl3)
}

2.: append 在大多数情况下很好用,但是如果你想完全掌控整个追加过程,你可以实现一个这样的 AppendByte 方法,func copy(dst, src []T) int copy 方法将类型为 T 的切片从源地址 src 拷贝到目标地址 dst,覆盖 dst 的相关元素,并且返回拷贝的元素个数。源地址和目标地址可能会有重叠。拷贝个数是 src 和 dst 的长度最小值。如果 src 是字符串那么元素类型就是 byte。如果你还想继续使用 src,在拷贝结束后执行 src = dst 。

func AppendByte(slice []byte, data ...byte) []byte {
m := len(slice)
n := m + len(data)
if n > cap(slice) { // if necessary, reallocate
// allocate double what's needed, for future growth.
newSlice := make([]byte, (n+1)*2)
copy(newSlice, slice)
slice = newSlice
}
slice = slice[0:n]
copy(slice[m:n], data)
return slice
}

六、字符串、数组和切片的应用

1.s 是一个字符串(本质上是一个字节数组),那么就可以直接通过 c := []byte(s) 来获取一个字节的切片 c。另外,还可以通过 copy 函数来达到相同的目的: copy(dst []byte, src string) 。将一个字符串追加到某一个字符数组的尾部:

var b []byte
var s string
b = append(b, s...)

2..使用 c := []int32(s) 语法,这样切片中的每个 int 都会包含对应的 Unicode 代码,因为字符串中的每次字符都会对应一个整数。类似的,可以将字符串转换为元素类型为 rune 的切片: r := []rune(s) 。可以通过代码 len([]int32(s)) 来获得字符串中字符的数量,但使用 utf8.RuneCountInString(s) 效率会更高一点。

3.获取字符串的某一部分,使用 substr := str[start:end] 可以从字符串 str 获取到从索引 start 开始到 end-1 位置的子字符串。同样
的, str[start:] 则表示获取从 start 开始到 len(str)-1 位置的子字符串。而 str[:end] 表示获取从 0 开始到 end-1的子字符串。

4.Go 语言中的字符串是不可变的,也就是说 str[index] 这样的表达式是不可以被放在等号左侧的。必须先将字符串转换成字节数组,然后再通过修改数组中的元素值来达到修改字符串的目的,最后将字节数组转换回字符串格式.例如,将字符串 "hello" 转换为 "cello":

s := "hello"
c := []byte(s)
c[0] = 'c'
s2 := string(c) // s2 == "cello"

5.Compare 函数会返回两个字节数组字典顺序的整数对比结果,即 0 if a == b, -1 if a < b, 1 if a > b 。

package main

import "fmt"

func main() {
	i := Compare([]byte("32"),[]byte("12"))
    fmt.Println(i)
	}
func Compare(a, b[]byte) int {
	for i:=0; i < len(a) && i < len(b); i++ {
		switch {
		case a[i] > b[i]:
			return 1
		case a[i] < b[i]:
			return -1
		}
	}
	// 数组的长度可能不同
	switch {
	case len(a) < len(b):
		return -1
	case len(a) > len(b):
		return 1
	}
	return 0 // 数组相等
}

6.搜索及排序切片和数组:标准库提供了 sort 包来实现常见的搜索和排序操作。使用 sort 包中的函数 func Ints(a []int) 来实现对 int类型的切片排序。例如 sort.Ints(arri) ,其中变量 arri 就是需要被升序排序的数组或切片。为了检查某个数组是否已经被排序,可以通过函数 IntsAreSorted(a []int) bool 来检查,如果返回 true 则表示已经被排序。使用函数 func Float64s(a []float64) 来排序 float64 的元素,或使用函数 func Strings(a []string) 排序字符串元素。在数组或切片中搜索一个元素,该数组或切片必须先被排序(因为标准库的搜索算法使用的是二分法)。然后,使用函数 func SearchInts(a []int, n int) int 进行搜索,并返回对应结果的索引值。当然,搜索 float64 和字符串:
func SearchFloat64s(a []float64, x float64) int
func SearchStrings(a []string, x string) int

7. append 函数能够用于各种方面的操作:
(1). 将切片 b 的元素追加到切片 a 之后: a = append(a, b...)
(2). 复制切片 a 的元素到新的切片 b 上:
b = make([]T, len(a))
copy(b, a)
(3). 删除位于索引 i 的元素: a = append(a[:i], a[i+1:]...)
(4). 切除切片 a 中从索引 i 至 j 位置的元素: a = append(a[:i], a[j:]...)
(5). 为切片 a 扩展 j 个元素长度: a = append(a, make([]T, j)...)
(6). 在索引 i 的位置插入元素 x: a = append(a[:i], append([]T{x}, a[i:]...)...)
(7). 在索引 i 的位置插入长度为 j 的新切片: a = append(a[:i], append(make([]T, j), a[i:]...)...)
(8). 在索引 i 的位置插入切片 b 的所有元素: a = append(a[:i], append(b, a[i:]...)...)
(9). 取出位于切片 a 最末尾的元素 x: x, a = a[len(a)-1], a[:len(a)-1]
(10). 将元素 x 追加到切片 a: a = append(a, x)
因此,您可以使用切片和 append 操作来表示任意可变长度的序列。

8.切片和垃圾回收:切片的底层指向一个数组,该数组的实际容量可能要大于切片所定义的容量。只有在没有任何切片指向的时候,底层的
数组内存才会被释放,这种特性有时会导致程序占用多余的内存。示例:函数 FindDigits 将一个文件加载到内存,然后搜索其中所有的数字并返回一个切片。

var digitRegexp = regexp.MustCompile("[0-9]+")
func FindDigits(filename string) []byte {
b, _ := ioutil.ReadFile(filename)
return digitRegexp.Find(b)
}

这段代码可以顺利运行,但返回的 []byte 指向的底层是整个文件的数据。只要该返回的切片不被释放,垃圾回收器就不能释放整个文件所占用的内存。一点点有用的数据却占用整个文件的内存。想要避免这个问题,可以通过拷贝我们需要的部分到一个新的切片中:

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

事实上,上面这段代码只能找到第一个匹配正则表达式的数字串。要想找到所有的数字,可以尝试下面这段代码:

func FindFileDigits(filename string) []byte {
fileBytes, _ := ioutil.ReadFile(filename)
b := digitRegexp.FindAll(fileBytes, len(fileBytes))
c := make([]byte, 0)
for _, bytes := range b {
c = append(c, bytes...)
}
return c
}

 

你可能感兴趣的:(Go)