2.4 Go语言基础之切片
本文主要介紹Go語言中切片(slice)及它的基本使用。
一、引子
因為數(shù)組的長度是固定的并且數(shù)組長度屬于類型的一部分,所以數(shù)組有很多的局限性。 例如:
func arraySum(x [3]int) int{sum := 0for _, v := range x{sum = sum + v}return sum
} 這個求和函數(shù)只能接受[3]int類型,其他的都不支持。 再比如,
a := [3]int{1, 2, 3} 數(shù)組a中已經(jīng)有三個元素了,我們不能再繼續(xù)往數(shù)組a中添加新元素了。
二、切片
2.1 切片知多少
切片(Slice)是一個擁有相同類型元素的可變長度的序列。它是基于數(shù)組類型做的一層封裝。它非常靈活,支持自動擴容。
切片與數(shù)組相比,不需要設(shè)定長度,在[]中不用設(shè)定值,相對來說比較自由。
slice本身沒有數(shù)據(jù)(切片并不存儲任何元素而只是對現(xiàn)有數(shù)組的引用。),是對底層array的一個view對slice所做的任何修改都將反映在底層數(shù)組中。
切片是什么?一種數(shù)據(jù)結(jié)構(gòu),類似數(shù)組,圍繞動態(tài)數(shù)組的概念而設(shè)計,可按需自動改變大小。
切片內(nèi)部實現(xiàn)?切片基于數(shù)組實現(xiàn),底層是數(shù)組(故底層的內(nèi)存是連續(xù)分配,可根據(jù)索引獲取數(shù)據(jù),可迭代以及垃圾回收),自身非常小(切片對象只有3個字段數(shù)據(jù)結(jié)構(gòu)即:指向底層數(shù)組的指針ptr(內(nèi)存地址)、切片長度len、切片容量cap),可看作對底層數(shù)組的抽象。
切片是一個引用類型,它的內(nèi)部結(jié)構(gòu)包含地址、長度和容量。切片一般用于快速地操作一塊數(shù)據(jù)集合。
切片三要素:
- 地址(切片中第一個元素指向的內(nèi)存空間)
- 大小(切片中目前元素的個數(shù))
len() - 容量(底層數(shù)組最大能存放的元素的個數(shù))
cap()
2.2 切片定義
定義聲明切片類型的基本語法如下:
var name []T 其中,
- name:表示變量名
- T:表示切片中的元素類型
2.3 切片初始化
2.3.1 為什么要初始化?
簡單地說,定義聲明只是告訴我的樣子(類型) 初始化就是告訴我在哪里(分配內(nèi)存) 要想找到我 僅僅知道我的樣子是沒用的 得知道我的地址
2.3.2 方法1 定義聲明切片變量時初始化
我們可以直接在聲明定義切片變量時對其直接進行初始化
例1:
func main() {// 聲明切片類型var a []string //聲明一個字符串切片var b = []int{} //聲明一個整型切片并初始化var c = []bool{false, true} //聲明一個布爾切片并初始化//var d = []bool{false, true} //聲明一個布爾切片并初始化fmt.Println(a) //[]fmt.Println(b) //[]fmt.Println(c) //[false true]fmt.Println(a == nil) //truefmt.Println(b == nil) //falsefmt.Println(c == nil) //false// fmt.Println(c == d) //切片是引用類型,不支持直接比較,只能和nil比較
} 2.3.3 方法2 基于數(shù)組定義切片
由于切片的底層就是一個數(shù)組,所以我們可以基于數(shù)組定義切片。
func main() {// 基于數(shù)組定義切片a := [5]int{55, 56, 57, 58, 59}b := a[1:4] //基于數(shù)組a創(chuàng)建切片,包括元素a[1],a[2],a[3]fmt.Println(b) //[56 57 58]fmt.Printf("type of b:%T\n", b) //type of b:[]int
} 還支持如下方式:
c := a[1:] //[56 57 58 59]
d := a[:4] //[55 56 57]
e := a[:] //[55 56 57 58 59] 總結(jié)如下:
a) arr[start:end]:包括start到end-1(包括end-1)之間的所有元素
b) arr[start:]:包括start到arr最后一個元素(包括最后一個元素)之間的所有元素
c) arr[:end]:包括0到end-1(包括end-1)之間的所有元素
d) arr[:]:包括整個數(shù)組的所有元素
2.3.4 方法3 基于切片再切片
除了基于數(shù)組得到切片,我們還可以通過切片來得到切片。
func main() {//切片再切片a := [...]string{"北京", "上海", "廣州", "深圳", "成都", "重慶"}fmt.Printf("a:%v type:%T len:%d cap:%d\n", a, a, len(a), cap(a))b := a[1:3]fmt.Printf("b:%v type:%T len:%d cap:%d\n", b, b, len(b), cap(b))c := b[1:5]fmt.Printf("c:%v type:%T len:%d cap:%d\n", c, c, len(c), cap(c))
} 輸出:
a:[北京 上海 廣州 深圳 成都 重慶] type:[6]string len:6 cap:6
b:[上海 廣州] type:[]string len:2 cap:5
c:[廣州 深圳 成都 重慶] type:[]string len:4 cap:4 注意: 對切片進行再切片時,索引不能超過原數(shù)組的長度,否則會出現(xiàn)索引越界的錯誤。
2.3.5 方法4 使用make函數(shù)構(gòu)造切片
我們上面都是基于數(shù)組來創(chuàng)建的切片,如果需要動態(tài)的創(chuàng)建一個切片,我們就需要使用內(nèi)置的make()函數(shù),格式如下:
make([]T, size, cap) 其中:
- T:切片的元素類型
- size:切片中元素的數(shù)量
- cap:切片的容量
舉個例子:
func main() {a := make([]int, 2, 10)fmt.Println(a) //[0 0]fmt.Println(len(a)) //2fmt.Println(cap(a)) //10
} 上面代碼中a的內(nèi)部存儲空間已經(jīng)分配了10個,但實際上只用了2個。 容量并不會影響當(dāng)前元素的個數(shù),所以len(a)返回2,cap(a)則返回該切片的容量。
補充:
make(make是一個內(nèi)置函數(shù))創(chuàng)建切片(其也是基于數(shù)組,只不過是底層已經(jīng)創(chuàng)建好了,我們看不見)比較推薦這種,應(yīng)用范圍也比較廣。切片元素的默認值為0。
make在這里做的就是在底層做了一個數(shù)組并且分配內(nèi)存空間,切片然后去引用這個數(shù)組。
2.3.6 注意
空切片必須初始化,或者用append才能使用,否則就會panic。
例1:
package mainimport "fmt"func main() {var a []stringif a == nil {fmt.Println("a是空切片")}a[0] = "paul" //不初始化直接賦值操作fmt.Println(a) }執(zhí)行結(jié)果:
例2:
package mainimport "fmt"func main() {var a []stringif a == nil {fmt.Println("a是空切片")}a = append(a, "harden") //不初始化但是可以進行append操作,append會自動對這個空切片做自動擴容。fmt.Println(a) }執(zhí)行結(jié)果:
2.4切片的本質(zhì)
切片的本質(zhì)就是對底層數(shù)組的封裝,它包含了三個信息:底層數(shù)組的指針、切片的長度(len)和切片的容量(cap)。
舉個例子,現(xiàn)在有一個數(shù)組a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相應(yīng)示意圖如下。
切片s2 := a[3:6],相應(yīng)示意圖如下:
2.5 nil切片和空切片區(qū)別
nil切片(比如: var nilSlice []int )指向底層數(shù)組的指針為nil,nil切片表示不存在的切片。
空切片(比如: slice := []int{} )對應(yīng)的指針是個地址,空切片表示一個空集合。
nil切片和空切片區(qū)別:
有無初始化(就是有無分配內(nèi)存地址),nil切片無初始化,空切片初始化了;
打印出來內(nèi)容雖然都是空,但是nil切片打印為空內(nèi)存地址,空切片打印為特殊的內(nèi)存地址(所有類型空切片都存放在這個特殊內(nèi)存地址)
nil切片和空切片都可以進行append操作,因為append操作會對該切片做自動初始化擴容操作;
關(guān)于nil切片和空切片區(qū)別:http://blog.itpub.net/31561269/viewspace-2220962/
例1:
package mainimport "fmt"func main() {//空切片b := []int{}fmt.Printf("b為:%#v\n", b) //%#v表示以go語言語法形式顯示值fmt.Printf("b的內(nèi)存地址:%p\n", b)b = append(b, 2)fmt.Printf("After:b為:%#v\n", b)fmt.Printf("After:b的內(nèi)存地址:%p\n", b)c := []string{}fmt.Printf("c為:%#v\n", c)fmt.Printf("c的內(nèi)存地址:%p\n", c)c = append(c, "MVP")fmt.Printf("After:c為:%#v\n", c)fmt.Printf("After:c的內(nèi)存地址:%p\n", c)//nil切片var d []intfmt.Printf("d為:%#v\n", d)fmt.Printf("d的內(nèi)存地址:%p\n", d)d = append(d, 1)fmt.Printf("After:d為:%#v\n", d)fmt.Printf("After:d的內(nèi)存地址:%p\n", d)} 執(zhí)行結(jié)果:
2.6 切片不能直接比較
切片之間是不能比較的,我們不能使用==操作符來判斷兩個切片是否含有全部相等元素。 切片唯一合法的比較操作是和nil比較。 一個nil值的切片并沒有底層數(shù)組,一個nil值的切片的長度和容量都是0。但是我們不能說一個長度和容量都是0的切片一定是nil,例如下面的示例:
var s1 []int //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{} //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil 2.7 如何使用切片?
類似使用數(shù)組,通過索引獲取切片對應(yīng)元素的值,也可修改對應(yīng)元素的值。
切片只能訪問到長度內(nèi)的元素,長度外的如果訪問會運行時異常。就會panic(報越界(panic: runtime error: index out of range)錯誤)
slice := []int{1,2,3,4,5}
fmt.Println(slice[2]) //3
slice[2] = 10
fmt.Println(slice[2]) //10 2.8 切片的賦值拷貝
下面的代碼中演示了拷貝前后兩個變量共享底層數(shù)組,對一個切片的修改會影響另一個切片的內(nèi)容,這點需要特別注意。
func main() {s1 := make([]int, 3) //[0 0 0]s2 := s1 //將s1直接賦值給s2,s1和s2共用一個底層數(shù)組s2[0] = 100fmt.Println(s1) //[100 0 0]fmt.Println(s2) //[100 0 0]
} 2.9 切片遍歷
切片的遍歷方式和數(shù)組是一致的,支持索引遍歷和for range遍歷。
func main() {s := []int{1, 3, 5}for i := 0; i < len(s); i++ {fmt.Println(i, s[i])}for index, value := range s {fmt.Println(index, value)}
} 2.10 append()方法為切片添加元素
Go語言的內(nèi)建函數(shù)append()可以為切片動態(tài)添加元素。 每個切片會指向一個底層數(shù)組,這個數(shù)組能容納一定數(shù)量的元素。當(dāng)?shù)讓訑?shù)組不能容納新增的元素時,切片就會自動按照一定的策略進行“擴容”,此時該切片指向的底層數(shù)組就會更換。“擴容”操作往往發(fā)生在append()函數(shù)調(diào)用時。 舉個例子:
func main() {//append()添加元素和切片擴容var numSlice []intfor i := 0; i < 10; i++ {numSlice = append(numSlice, i)fmt.Printf("%v len:%d cap:%d ptr:%p\n", numSlice, len(numSlice), cap(numSlice), numSlice)}
} 輸出:
[0] len:1 cap:1 ptr:0xc0000a8000
[0 1] len:2 cap:2 ptr:0xc0000a8040
[0 1 2] len:3 cap:4 ptr:0xc0000b2020
[0 1 2 3] len:4 cap:4 ptr:0xc0000b2020
[0 1 2 3 4] len:5 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5] len:6 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6] len:7 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7] len:8 cap:8 ptr:0xc0000b6000
[0 1 2 3 4 5 6 7 8] len:9 cap:16 ptr:0xc0000b8000
[0 1 2 3 4 5 6 7 8 9] len:10 cap:16 ptr:0xc0000b8000 從上面的結(jié)果可以看出:
append()函數(shù)將元素追加到切片的最后并返回該切片。- 切片numSlice的容量按照1,2,4,8,16這樣的規(guī)則自動進行擴容,每次擴容后都是擴容前的2倍。
append()函數(shù)還支持一次性追加多個元素。 例如:
var citySlice []string
// 追加一個元素
citySlice = append(citySlice, "北京")
// 追加多個元素
citySlice = append(citySlice, "上海", "廣州", "深圳")
// 追加切片
a := []string{"成都", "重慶"}
citySlice = append(citySlice, a...)
fmt.Println(citySlice) //[北京 上海 廣州 深圳 成都 重慶] 2.11 切片的擴容策略
可以通過查看$GOROOT/src/runtime/slice.go源碼,其中擴容相關(guān)代碼如下:
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}}
} 從上面的代碼可以看出以下內(nèi)容:
- 首先判斷,如果新申請容量(cap)大于2倍的舊容量(old.cap),最終容量(newcap)就是新申請的容量(cap)。
- 否則判斷,如果舊切片的長度小于1024,則最終容量(newcap)就是舊容量(old.cap)的兩倍,即(newcap=doublecap),
- 否則判斷,如果舊切片長度大于等于1024,則最終容量(newcap)從舊容量(old.cap)開始循環(huán)增加原來的1/4,即(newcap=old.cap,for {newcap += newcap/4})直到最終容量(newcap)大于等于新申請的容量(cap),即(newcap >= cap)
- 如果最終容量(cap)計算值溢出,則最終容量(cap)就是新申請容量(cap)。
需要注意的是,切片擴容還會根據(jù)切片中元素的類型不同而做不同的處理,比如int和string類型的處理方式就不一樣。
2.12 使用copy()函數(shù)復(fù)制切片
首先我們來看一個問題:
func main() {a := []int{1, 2, 3, 4, 5}b := afmt.Println(a) //[1 2 3 4 5]fmt.Println(b) //[1 2 3 4 5]b[0] = 1000fmt.Println(a) //[1000 2 3 4 5]fmt.Println(b) //[1000 2 3 4 5]
} 由于切片是引用類型,所以a和b其實都指向了同一塊內(nèi)存地址。修改b的同時a的值也會發(fā)生變化。
Go語言內(nèi)建的copy()函數(shù)可以迅速地將一個切片的數(shù)據(jù)復(fù)制到另外一個切片空間中,copy()函數(shù)的使用格式如下:
copy(destSlice, srcSlice []T) 其中:
- srcSlice: 數(shù)據(jù)來源切片
- destSlice: 目標切片
舉個例子:
func main() {// copy()復(fù)制切片a := []int{1, 2, 3, 4, 5}c := make([]int, 5, 5)copy(c, a) //使用copy()函數(shù)將切片a中的元素復(fù)制到切片cfmt.Println(a) //[1 2 3 4 5]fmt.Println(c) //[1 2 3 4 5]c[0] = 1000fmt.Println(a) //[1 2 3 4 5]fmt.Println(c) //[1000 2 3 4 5]
} 2.13 從切片中刪除元素
Go語言中并沒有刪除切片元素的專用方法,我們可以使用切片本身的特性來刪除元素。 代碼如下:
func main() {// 從切片中刪除元素a := []int{30, 31, 32, 33, 34, 35, 36, 37}// 要刪除索引為2的元素a = append(a[:2], a[3:]...)fmt.Println(a) //[30 31 33 34 35 36 37]
} 總結(jié)一下就是:要從切片a中刪除索引為index的元素,操作方法是a = append(a[:index], a[index+1:]...)
轉(zhuǎn)載于:https://www.cnblogs.com/forever521Lee/p/10770349.html
總結(jié)
以上是生活随笔為你收集整理的2.4 Go语言基础之切片的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 鼋头渚门票多少钱一张
- 下一篇: linux下获取系统时间 和 时间偏移