Golang 学习笔记二 数组、切片

一、数组

《快学 Go 语言》第 4 课 —— 低调的数组
Go 语言里面的数组其实很不常用,这是因为数组是定长的静态的,一旦定义好长度就无法更改,而且不同长度的数组属于不同的类型,之间不能相互转换相互赋值,用起来多有不方便之处。

切片是动态的数组,是可以扩充内容增加长度的数组。当长度不变时,它用起来就和普通数组一样。当长度不同时,它们也属于相同的类型,之间可以相互赋值。这就决定了数组的应用领域都广泛地被切片取代了。
1.只声明的话,全部是零值

func main() {
    var a [9]int
    fmt.Println(a)
}

------------
[0 0 0 0 0 0 0 0 0]

三种声明方式,给初值方式是一样的

var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
var b [10]int = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
c := [8]int{1, 2, 3, 4, 5, 6, 7, 8}
//[0,10,20,0,0]
array := [5]int{1:10,2:20}

2.下标访问

func main() {
    var squares [9]int
    for i := 0; i < len(squares); i++ {
        squares[i] = (i + 1) * (i + 1)
    }
    fmt.Println(squares)
}

--------------------
[1 4 9 16 25 36 49 64 81]

3.数组赋值
同样的子元素类型并且是同样长度的数组才可以相互赋值,否则就是不同的数组类型,不能赋值。数组的赋值本质上是一种浅拷贝操作,赋值的两个数组变量的值不会共享。

func main() {
    var a = [9]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    var b [9]int
    b = a
    a[0] = 12345
    fmt.Println(a)
    fmt.Println(b)
}

--------------------------
[12345 2 3 4 5 6 7 8 9]
[1 2 3 4 5 6 7 8 9]

4.range关键字来遍历

func main() {
    var a = [5]int{1,2,3,4,5}
    for index := range a {
        fmt.Println(index, a[index])
    }
    for index, value := range a {
        fmt.Println(index, value)
    }
}

------------
0 1
1 2
2 3
3 4
4 5
0 1
1 2
2 3
3 4
4 5

每次循环迭代, range 产生一对值;索引以及在该索引处的元素值。如果不需要索引怎么办,range 的语法要求, 要处理元素, 必须处理索引。一种思路是把索引赋值给一个临时变量,如 temp , 然后忽略它的值,但Go语言不允许使用无用的局部变量(local variables),因为这会导致编译错误。Go语言中这种情况的解决方法是用 空标识符 (blank identifier),即 _ (也就是下划线)。空标识符可用于任何语法需要变量名但程序逻辑不需要的时候, 例如, 在循环里,丢弃不需要的循环索引, 保留元素值。

    for _,value := range s1{
        fmt.Println(value)
    }

注意:range创建了每个元素的副本,而不是直接返回对该元素的引用。range总是会从切片头部开始迭代。

5.函数间传递数组
在函数间传递变量时,总是以值的方式传递。如果变量是个数组,意味着整个数组,不管有多长,都会完整复制,并传递给函数。在这方面,go语言对待数组的方式和其它很多编程语言不同,其它语言可能会隐式地将数组作为引用或指针对象传入被调用的函数。

有一种更好且更有效的方法来处理这个操作,就是只传入指向数组的指针,只需要复制8个字节的数据,这样危险在于,共享了内存,会改变原始值。

二、切片

《快学 Go 语言》第 5 课 —— 灵活的切片

image.png

上图中一个切片变量包含三个域,分别是底层数组的指针、切片的长度 length 和切片的容量 capacity。切片支持 append 操作可以将新的内容追加到底层数组,也就是填充上面的灰色格子。如果格子满了,切片就需要扩容,底层的数组就会更换。

形象一点说,切片变量是底层数组的视图,底层数组是卧室,切片变量是卧室的窗户。通过窗户我们可以看见底层数组的一部分或全部。一个卧室可以有多个窗户,不同的窗户能看到卧室的不同部分。

1.切片的创建有多种方式,我们先看切片最通用的创建方法,那就是内置的 make 函数

var s1 []int = make([]int, 5, 8)
 var s2 []int = make([]int, 8) // 满容切片

make 函数创建切片,需要提供三个参数,分别是切片的类型、切片的长度和容量。其中第三个参数是可选的,如果不提供第三个参数,那么长度和容量相等,也就是说切片的满容的。

使用 make 函数创建的切片内容是「零值切片」,也就是内部数组的元素都是零值。Go 语言还提供了另一个种创建切片的语法,允许我们给它赋初值。使用这种方式创建的切片是满容的。

func main() {
 var s []int = []int{1,2,3,4,5}  // 满容的
 fmt.Println(s, len(s), cap(s))
}

---------
[1 2 3 4 5] 5 5

这种写法,和数组的定义非常相似,注意区别就在方括号里是否写了长度。

    var i1 [5]int = [5]int{1,2,3,4,5}
    var i2 []int = []int{1,2,3,4,5}
    i1 = append(i1, 6)
    i2 = append(i2,7)

编译不通过,i1用不了append方法,参数必须是slice

2.切片的赋值
切片的赋值是一次浅拷贝操作,拷贝的是切片变量的三个域,你可以将切片变量看成长度为 3 的 int 型数组,数组的赋值就是浅拷贝。拷贝前后两个变量共享底层数组,对一个切片的修改会影响另一个切片的内容,这点需要特别注意。

func main() {
 var s1 = make([]int, 5, 8)
 // 切片的访问和数组差不多
 for i := 0; i < len(s1); i++ {
  s1[i] = i + 1
 }
 var s2 = s1
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))

 // 尝试修改切片内容
 s2[0] = 255
 fmt.Println(s1)
 fmt.Println(s2)
}

--------
[1 2 3 4 5] 5 8
[1 2 3 4 5] 5 8
[255 2 3 4 5]
[255 2 3 4 5]

3.append追加
相对于数组,切片可以增加长度。当append返回时,会返回一个包含修改结果的新切片。函数append总会增加新切片的长度,而容量有可能改变,也有可能不改变,这取决于被操作切片的可用容量。

slice := []int{10,20,30,40,50}
newSlice := slice[1:3]
newSlice = append(newSlice,60);

因为newSlice在底层数组里还有额外容量可用,append操作将可用元素合并到切片的长度,并对其进行赋值。由于和原始的slice共享同一个底层数组,slice中索引为3的元素值也被改动了。

如果切片的底层数组没有足够可用容量,append函数会创建一个新的底层数组,将被引用的现有值复制到新数组里,再追加新值。

slice := []int{10,20,30,40}
newSlice = append(slice,50)

函数append会自动处理底层数组的容量增长,在切片容量小于1000个元素时,总是会成倍地增加容量。超过1000时,每次增加25%。

append可以在一次调用传递多个追加值,如果使用...运算符,可以将一个切片所有元素追加到另一个切片里

s1 := []int{1,2}
s2 := []int{3,4}
append(s1,s2...);

4.切割

func main() {
 var s1 = []int{1,2,3,4,5,6,7}
 // start_index 和 end_index,不包含 end_index
 // [start_index, end_index)
 var s2 = s1[2:5] 
 s2[0] = 0
 fmt.Println(s1, len(s1), cap(s1))
 fmt.Println(s2, len(s2), cap(s2))
}

------------
[1 2 0 4 5 6 7] 7 7
[0 4 5] 3 5

image.png

我们注意到子切片的内部数据指针指向了数组的中间位置,而不再是数组的开头了。子切片容量的大小是从中间的位置开始直到切片末尾的长度,母子切片依旧共享底层数组。

子切片语法上要提供起始和结束位置,这两个位置都可选的,不提供起始位置,默认就是从母切片的初始位置开始(不是底层数组的初始位置),不提供结束位置,默认就结束到母切片尾部(是长度线,不是容量线)。使用过 Python 的同学可能会问,切片支持负数的位置么,答案是不支持,下标不可以是负数。

对数组进行切割可以转换成切片,切片将原数组作为内部底层数组。也就是说修改了原数组会影响到新切片,对切片的修改也会影响到原数组。

func main() {
    var a = [10]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    var b = a[2:6]
    fmt.Println(b)
    a[4] = 100
    fmt.Println(b)
}

-------
[3 4 5 6]
[3 4 100 6]

5.切割的第三个参数

source := []string{"apple","orange","plum","banana","grape"};
slice := source[2:3]//plum1个元素,到结尾,3个容量
slice := source[2:3:4]//也是一个1元素,不过有4-2=2个容量

使用第3个参数将长度和容量保持一致后,再使用append操作就会创建新的底层数组,从而和原底层数组分离,这样就不用担心影响到其他切片中的数据。
6.内置 copy 函数 func copy(dst, src []T) int
copy 函数不会因为原切片和目标切片的长度问题而额外分配底层数组的内存,它只负责拷贝数组的内容,从原切片拷贝到目标切片,拷贝的量是原切片和目标切片长度的较小值 —— min(len(src), len(dst)),函数返回的是拷贝的实际长度。

func main() {
 var s = make([]int, 5, 8)
 for i:=0;i<len(s);i++ {
  s[i] = i+1
 }
 fmt.Println(s)
 var d = make([]int, 2, 6)
 var n = copy(d, s)
 fmt.Println(n, d)
}
-----------
[1 2 3 4 5]
2 [1 2]

7.range遍历
需要强调的是,range创建了每个元素的副本,而不是直接返回该元素的引用

// 创建一个整型切片
// 其长度和容量都是 4 个元素
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
Output:
Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

因为迭代返回的变量是一个迭代过程中根据切片依次赋值的新变量,所以 value 的地址总是相同的。要想获取每个元素的地址,可以使用切片变量和索引值。

8.没有push,pop这些功能
参考[译]Go Slice 秘籍

Pop
x, a = a[len(a)-1], a[:len(a)-1]

Push
a = append(a, x)

Shift
x, a := a[0], a[1:]

Unshift
a = append([]T{x}, a...)

要删除slice中间的某个元素并保存原有的元素顺序,可以通过内置的copy函数将后面的子slice向前依次移动一位完成

func remove(slice []int, i int) []int {
copy(slice[i:], slice[i+1:])
return slice[:len(slice)-1]
}

参照上面的copy解释,slice[i+1:]的长度必定是小于slice[i:]的,所以不会出现copy过程中丢失自己想留的数据。然后copy不改变原有底层数组的len和cap,只是把数据往前覆盖了一个元素,所以必须使用return slice[:len(slice)-1]才是想要的结果。
9.可变参数

func sum(vals...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}

sum函数返回任意个int型参数的和。在函数体中,vals被看作是类型为[] int的切片。sum可以接收任意数量的int型参数:

fmt.Println(sum()) // "0"
fmt.Println(sum(3)) // "3"
fmt.Println(sum(1, 2, 3, 4)) // "10"

10.清空
参考 展示不同方式清空 slice 的效果

package main

import (
    "fmt"
)

func dump(letters []string) {
    fmt.Printf("addr = %p\n", letters)
    fmt.Println("letters = ", letters)
    fmt.Println(cap(letters))
    fmt.Println(len(letters))
    for i := range letters {
        fmt.Println(i, letters[i])
    }
}

func main() {
    fmt.Println("=== 基础数据 ==========")
    letters := []string{"a", "b", "c", "d"}
    dump(letters)

    fmt.Println("=== ====== ==========")

    fmt.Println("=== \"原地\"清空 ===")
    fmt.Println("=== 效果:")
    fmt.Println("=== 1.直接在原 slice 上操作,故无 GC 行为")
    fmt.Println("=== 2.清空后 cap 值和之前相同,len 值清零")
    letters = letters[:0]
    dump(letters)

    fmt.Println("=== 添加元素效果:基于原 slice 操作,故再未超 cap 前无需内存分配")
    letters = append(letters, "e")
    dump(letters)


    fmt.Println("=== ====== ==========")
    fmt.Println("=== 基于 nil 清空 ===")
    fmt.Println("=== 效果:")
    fmt.Println("=== 1.类似 C 语言中赋值空指针,原内容会被 GC 处理")
    fmt.Println("=== 2.清空后 cap 值清零,len 值清零")
    letters = nil
    dump(letters)

    fmt.Println("=== 添加元素效果:类似从无到有创建 slice")
    letters = append(letters, "e")
    dump(letters)
}

运行结果

=== 基础数据 ==========
addr = 0xc420070080
letters =  [a b c d]
4
4
0 a
1 b
2 c
3 d
=== ====== ==========
=== "原地"清空 ===
=== 效果:
=== 1.直接在原 slice 上操作,故无 GC 行为
=== 2.清空后 cap 值和之前相同,len 值清零
addr = 0xc420070080
letters =  []
4
0
=== 添加元素效果:基于原 slice 操作,故再未超 cap 前无需内存分配
addr = 0xc420070080
letters =  [e]
4
1
0 e
=== ====== ==========
=== 基于 nil 清空 ===
=== 效果:
=== 1.类似 C 语言中赋值空指针,原内容会被 GC 处理
=== 2.清空后 cap 值清零,len 值清零
addr = 0x0
letters =  []
0
0
=== 添加元素效果:类似从无到有创建 slice
addr = 0xc42007c270
letters =  [e]
1
1
0 e
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 线性结构是计算机最常用的数据结构之一。无论是数组(arrary)还是链表(list),在编程中不可或缺。golan...
    _二少爷阅读 6,596评论 5 13
  • 1.安装 https://studygolang.com/dl 2.使用vscode编辑器安装go插件 3.go语...
    go含羞草阅读 1,538评论 0 6
  • 出处---Go编程语言 欢迎来到 Go 编程语言指南。本指南涵盖了该语言的大部分重要特性 Go 语言的交互式简介,...
    Tuberose阅读 18,398评论 1 46
  • 一入咳嗽坑,学问何其深。越查越多,越多越查。最后,我放弃了,不写了,就简单记录下七妹子这次的积食咳嗽发热好了。 一...
    七嫲嫲阅读 2,474评论 0 0
  • 我现在已经陷于一种境地 一种悲剧的境地 或许不悲剧 我现在已经开始看不懂自己写了啥…… 是不是精分前兆?
    丁一文阅读 110评论 1 2