Go语言学习 - 复合数据类型:数组、slice

2018-03-19 [编程语言] #Go #Notes
更新日志
2021-06-11 更新分类、标签

复合数据类型

主要分为4种复合数据类型:

本次先将数组和slice的相关知识点及理解记录下来,后续再慢慢看Map和结构体。

数组

数组是具有固定长度且拥有零个或多个相同数据类型元素的序列。由于数组的长度固定,所以在Go里面很少直接使用。

var arr [3]int                  // 3个整数的数组
fmt.Println(arr[len(arr)-1])    // 输出数组最后一个元素

// 遍历数组并输出索引和元素
for i, v := range arr {
    fmt.Println("%d %d\n", i, v)
}
var arr [3]int = [3]{1, 2, 3}
arr := [...]int{1, 2, 3}
// 定义一个长度100的数组,第99个元素(最后一个)的值为 2
r := [...]int{99: 2}
a := [2]int{1, 2}
b := [...]int{1, 2}
c := [3]int{1, 2, 3}
fmt.Println( a == b )   // true
fmt.Println( a == c )   // 编译错误:数组类型不一致    

当调用一个函数时,每个传入的参数都会创建一个副本,然后赋值给对应的函数变量,所以函数接受的是一个副本,而不是原始的参数。使用这种方式传递大的数组会变得很低效,并且在函数内部对数组的任何修改都仅影响副本,而不是原始数组。这种情况下,Go把数组和其他的类型都看成了值传递。在其他语言中,例如Java数组是隐式地使用引用传递。需要传递数组引用可以使用指针方式

slice

理解:在看到slice前面部分时,自动带入了Java中List的概念去看,导致反复看也不是很清楚。最终看到make函数、append函数 让我理解了,的确可以理解为List来使用。

slice:切片,是对数组一段内容的引用。切片的定义与数组基本一致,但不需要在[]中填写数组的长度。

// 数组定义
a := [...]int{1, 2, 3, 4, 5}

// 切片定义
s := []int{1, 2, 3, 4, 5}

slice有3个属性:指针、长度和容量。

可以使用内置的len() 和 cap() 函数来计算切片的长度和容量。

如果slice的引用超过了被引用对象的容量,即cap(被引用对象),那么会导致程序报错。 如果slice的引用超过了被引用都对象的长度,即len(被引用对象),那么最终slice会比原slice长。

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

// 实际夏季的月份
summer := months[6:9]

// 超出cap(summer) ,报错
fmt.Println(summer[:20])

// 扩充长度
summer1 := summer[:5]     //summer1 = [June July August September October]

在看了上面最后一句代码,可以更清晰认为slice只是引用了数组,所有变化也是数组,所以slice可以理解为数组的一个别名(一个区域的别名)

比较

slice和数组基本一致,除初始化方式不需要填写长度之外,slice也不可以直接用 == 或者 !=比较。如果是[]bytes的slice,可以使用bytes.Equal 进行比较,其余类型需要自己写函数实现。

slice不支持直接使用 == 或者 != 比较的原因有以下2点:

  1. 和数组不同,slice的元素是非直接的,有可能slice可以包含它本身
  2. 因为slice的元素不是直接的,所以如果底层数组元素改变,同一个slice在不同的时间会拥有不同的元素。

make 函数

内置的make函数可以创建一个具有指定元素类型、长度和容量的slice。如果省略容量参数,则容量 == 长度。

在make函数的内部实现中,make函数创建了一个无名的数组并返回了它的一个slice,这个数组进可以通过这个slice访问。

// slice引用的无名数组长度 == 传入的len
make([]T, len)

// slice引用的无名数组长度 == cap, 并返回指定len的slice
make([]T, len, cap)

append 函数

内置的append函数用于将元素追加到slice的后面。append函数可以同时给slice添加多个元素,甚至添加另一个slice里的所有元素。

通过slice.go源码中的growslice函数查看slice的扩充策略:当旧的容量小于1024,则新容量直接*2 (doublecap := newcap + newcap ),反之,每次增加1/4。

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
        }
    }
}

在slice每次扩充时,会返回一个新的slice,而旧的slice有可能指向了底层数组,也有可能没有指向底层数组,如果slice的容量完全能够存在新的内容,则指向了旧数组,否则返回的新slice与原slice不一致(底层数组不一致)。所以为了保证在后面能够继续使用变量操作,可以将append的结果赋值给原slice

runes = append(runes, r)
// 思考以下代码的输出
func main() {
	s := []int{3}
	s = append(s, 4)
	s = append(s, 5)
	x := append(s, 6)
	y := append(s, 7)
	fmt.Println(s, x, y)
}

// @TODO 后面学完Go基础后,将slice.go源码仔细阅读一遍。

文章作者:eightpigs
创作时间:2018-03-19
更新时间:2021-06-11
许可协议:CC by-nc-nd 4.0