Go語言之重要數(shù)組類型切片(slice)make,append函數(shù)解讀
切片是一個動態(tài)數(shù)組,因為數(shù)組的長度是固定的,所以操作起來很不方便,比如一個names數(shù)組,我想增加一個學生姓名都沒有辦法,十分不靈活。
所以在開發(fā)中數(shù)組并不常用,切片類型才是大量使用的。
切片基本操作
切片的創(chuàng)建有兩種方式:
- 從數(shù)組或者切片上切取獲得
- 直接聲明切片 : var name []Type // 不同于數(shù)組, []沒有數(shù)字
切片語法:
arr [start : end] 或者 slice [start : end] // start: 開始索引 end:結(jié)束索引
切片特點:
- 左閉右開 [ )
- 取出的元素數(shù)量為:結(jié)束位置 - 開始位置;
- 取出元素不包含結(jié)束位置對應(yīng)的索引,切片最后一個元素使用 slice[len(slice)]獲??;
- 當缺省開始位置時,表示從連續(xù)區(qū)域開頭到結(jié)束位置;當缺省結(jié)束位置時,表示從開始位置到整個連續(xù)區(qū)域末尾;兩者同時缺省時,與切片本身等效;
var arr = [5]int{10, 11, 12, 13, 14} var s1 = arr[1:4] fmt.Println(s1, reflect.TypeOf(s1)) // [11 12 13] []int var s2 = arr[2:5] fmt.Println(s2, reflect.TypeOf(s2)) // [12 13 14] var s3 = s2[0:2] // [12 13]
值類型和引用類型
數(shù)據(jù)類型從存儲方式分為兩類:值類型和引用類型!
(1) 值類型
基本數(shù)據(jù)類型(int,float,bool,string)以及數(shù)組和struct都屬于值類型。
特點:變量直接存儲值,內(nèi)存通常在棧中分配,棧在函數(shù)調(diào)用完會被釋放。值類型變量聲明后,不管是否已經(jīng)賦值,編譯器為其分配內(nèi)存,此時該值存儲于棧上。
var a int //int類型默認值為 0 var b string //string類型默認值為 nil空 var c bool //bool類型默認值為false var d [2]int //數(shù)組默認值為[0 0]
當使用等號=將一個變量的值賦給另一個變量時,如 j = i ,實際上是在內(nèi)存中將 i 的值進行了拷貝,可以通過 &i 獲取變量 i 的內(nèi)存地址。此時如果修改某個變量的值,不會影響另一個。
// 整型賦值 var a =10 b := a b = 101 fmt.Printf("a:%v,a的內(nèi)存地址是%p\n",a,&a) fmt.Printf("b:%v,b的內(nèi)存地址是%p\n",b,&b) //數(shù)組賦值 var c =[3]int{1,2,3} d := c d[1] = 100 fmt.Printf("c:%v,c的內(nèi)存地址是%p\n",c,&c) fmt.Printf("d:%v,d的內(nèi)存地址是%p\n",d,&d)
(2) 引用類型
指針、slice,map,chan,interface等都是引用類型。
特點:變量通過存儲一個地址來存儲最終的值。內(nèi)存通常在堆上分配,通過GC回收。
引用類型必須申請內(nèi)存才可以使用,new()和make()是給引用類型申請內(nèi)存空間。
切片原理
切片的構(gòu)造根本是對一個具體數(shù)組通過切片起始指針,切片長度以及最大容量三個參數(shù)確定下來的
type Slice struct { Data uintptr // 指針,指向底層數(shù)組中切片指定的開始位置 Len int // 長度,即切片的長度 Cap int // 最大長度(容量),也就是切片開始位置到數(shù)組的最后位置的長度 }
var arr = [5]int{10, 11, 12, 13, 14} s1 := arr[0:3] // 對數(shù)組切片 s2 := arr[2:5] s3 := s2[0:2] // 對切片切片 fmt.Println(s1) // [10, 11, 12] fmt.Println(s2) // [12, 13, 14] fmt.Println(s3) // [12, 13] // 地址是連續(xù)的 fmt.Printf("%p\n", &arr) fmt.Printf("%p\n", &arr[0]) // 相差8個字節(jié) fmt.Printf("%p\n", &arr[1]) fmt.Printf("%p\n", &arr[2]) fmt.Printf("%p\n", &arr[3]) fmt.Printf("%p\n", &arr[4]) // 每一個切片都有一塊自己的空間地址,分別存儲了對于數(shù)組的引用地址,長度和容量 fmt.Printf("%p\n", &s1) // s1自己的地址 fmt.Printf("%p\n", &s1[0]) fmt.Println(len(s1), cap(s1)) fmt.Printf("%p\n", &s2) // s2自己的地址 fmt.Printf("%p\n", &s2[0]) fmt.Println(len(s2), cap(s2)) fmt.Printf("%p\n", &s3) // s3自己的地址 fmt.Printf("%p\n", &s3[0]) fmt.Println(len(s3), cap(s3))
var a = [...]int{1, 2, 3, 4, 5, 6} a1 := a[0:3] a2 := a[0:5] a3 := a[1:5] a4 := a[1:] a5 := a[:] a6 := a3[1:2] fmt.Printf("a1的長度%d,容量%d\n", len(a1), cap(a1)) fmt.Printf("a2的長度%d,容量%d\n", len(a2), cap(a2)) fmt.Printf("a3的長度%d,容量%d\n", len(a3), cap(a3)) fmt.Printf("a4的長度%d,容量%d\n", len(a4), cap(a4)) fmt.Printf("a5的長度%d,容量%d\n", len(a5), cap(a5)) fmt.Printf("a6的長度%d,容量%d\n", len(a6), cap(a6))
除了可以從原有的數(shù)組或者切片中生成切片外,也可以聲明一個新的切片,每一種類型都可以擁有其切片類型,表示多個相同類型元素的連續(xù)集合,因此切片類型也可以被聲明,切片類型聲明格式如下:
var name []Type // []Type是切片類型的標識
其中 name 表示切片的變量名,Type 表示切片對應(yīng)的元素類型。
var names = []string{"張三","李四","王五"} fmt.Println(names,reflect.TypeOf(names)) // [張三 李四 王五 趙六 孫七] []string
直接聲明切片,會針對切片構(gòu)建底層數(shù)組,然后切片形成對數(shù)組的引用
練習
s1 := []int{1, 2, 3} s2 := s1[1:] s2[1] = 4 fmt.Println(s1)
var a = []int{1, 2, 3} b := a a[0] = 100 fmt.Println(b)
make函數(shù)
變量的聲明我們可以通過var關(guān)鍵字,然后就可以在程序中使用。當我們不指定變量的默認值時,這些變量的默認值是他們的零值,比如int類型的零值是0,string類型的零值是"",引用類型的零值是nil。
對于例子中的兩種類型的聲明,我們可以直接使用,對其進行賦值輸出。但是如果我們換成引用類型呢?
// arr := []int{} var arr [] int // 如果是 var arr [2] int arr[0] = 1 fmt.Println(arr)
從這個提示中可以看出,對于引用類型的變量,我們不光要聲明它,還要為它分配內(nèi)容空間。
對于值類型的聲明不需要,是因為已經(jīng)默認幫我們分配好了。要分配內(nèi)存,就引出來今天的make函數(shù)。
make也是用于chan、map以及切片的內(nèi)存創(chuàng)建,而且它返回的類型就是這三個類型本身。
如果需要動態(tài)地創(chuàng)建一個切片,可以使用 make() 內(nèi)建函數(shù),格式如下:
make([]Type, size, cap)
其中 Type 是指切片的元素類型,size 指的是為這個類型分配多少個元素,cap 為預(yù)分配的元素數(shù)量,這個值設(shè)定后不影響 size,只是能提前分配空間,降低多次分配空間造成的性能問題。
示例如下:
a := make([]int, 2) b := make([]int, 2, 10) fmt.Println(a, b) fmt.Println(len(a), len(b)) fmt.Println(cap(a), cap(b))
使用 make() 函數(shù)生成的切片一定發(fā)生了內(nèi)存分配操作,但給定開始與結(jié)束位置(包括切片復(fù)位)的切片只是將新的切片結(jié)構(gòu)指向已經(jīng)分配好的內(nèi)存區(qū)域,設(shè)定開始與結(jié)束位置,不會發(fā)生內(nèi)存分配操作。
a := make([]int, 5) b := a[0:3] a[0] = 100 fmt.Println(a) fmt.Println(b)
append(重點)
上面我們已經(jīng)講過,切片作為一個動態(tài)數(shù)組是可以添加元素的,添加方式為內(nèi)建方法append。
(1)append的基本用法
var emps = make([]string, 3, 5) emps[0] = "張三" emps[1] = "李四" emps[2] = "王五" fmt.Println(emps) emps2 := append(emps, "rain") fmt.Println(emps2) emps3 := append(emps2, "eric") fmt.Println(emps3) // 容量不夠時發(fā)生二倍擴容 emps4 := append(emps3, "yuan") fmt.Println(emps4) // 此時底層數(shù)組已經(jīng)發(fā)生變化
擴容機制
1、每次 append 操作都會檢查 slice 是否有足夠的容量,如果足夠會直接在原始數(shù)組上追加元素并返回一個新的 slice,底層數(shù)組不變,但是這種情況非常危險,極度容易產(chǎn)生 bug!而若容量不夠,會創(chuàng)建一個新的容量足夠的底層數(shù)組,先將之前數(shù)組的元素復(fù)制過來,再將新元素追加到后面,然后返回新的 slice,底層數(shù)組改變而這里對新數(shù)組的進行擴容
2、擴容策略:如果切片的容量小于 1024 個元素,于是擴容的時候就翻倍增加容量。上面那個例子也驗證了這一情況,總?cè)萘繌脑瓉淼?個翻倍到現(xiàn)在的8個。一旦元素個數(shù)超過 1024 個元素,那么增長因子就變成 1.25 ,即每次增加原來容量的四分之一。
arr := [4]int{10, 20, 30, 40} s1 := arr[0:2] // [10, 20] s2 := s1 // // [10, 20] s3 := append(append(append(s1, 1), 2), 3) s1[0] = 1000 fmt.Println(s1) fmt.Println(s2) fmt.Println(s3) fmt.Println(arr)
(2)append的擴展用法
var a []int a = append(a, 1) // 追加1個元素 fmt.Println(a) a = append(a, 1, 2, 3) // 追加多個元素, 手寫解包方式 fmt.Println(a) a = append(a, []int{1, 2, 3}...) // 追加一個切片, 切片需要解包 fmt.Println(a)
a = append(a, 1)返回切片又重新賦值a的目的是丟棄老數(shù)組
// 案例1 a := []int{11, 22, 33} fmt.Println(len(a), cap(a)) c := append(a, 44) a[0] = 100 fmt.Println(a) fmt.Println(c) // 案例2 a := make([]int, 3, 10) fmt.Println(a) b := append(a, 11, 22) fmt.Println(a) // 小心a等于多少? fmt.Println(b) a[0] = 100 fmt.Println(a) fmt.Println(b) // 案例3 l := make([]int, 5, 10) v1 := append(l, 1) fmt.Println(v1) fmt.Printf("%p\n", &v1) v2 := append(l, 2) fmt.Println(v2) fmt.Printf("%p\n", &v2) fmt.Println(v1)
切片的插入和刪除
開頭添加元素
var a = []int{1,2,3} a = append([]int{0}, a...) // 在開頭添加1個元素 a = append([]int{-3,-2,-1}, a...) // 在開頭添加1個切片
在切片開頭添加元素一般都會導(dǎo)致內(nèi)存的重新分配,而且會導(dǎo)致已有元素全部被復(fù)制 1 次,因此,從切片的開頭添加元素的性能要比從尾部追加元素的性能差很多。
任意位置插入元素
var a []int a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i個位置插入x a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i個位置插入切片
每個添加操作中的第二個 append 調(diào)用都會創(chuàng)建一個臨時切片,并將 a[i:] 的內(nèi)容復(fù)制到新創(chuàng)建的切片中,然后將臨時創(chuàng)建的切片再追加到 a[:i] 中。
思考這樣寫可以不:
var a = []int{1,2,3,4} s1:=a[:2] s2:=a[2:] fmt.Println(append(append(s1,100,),s2...))
刪除元素
Go語言中并沒有刪除切片元素的專用方法,我們可以使用切片本身的特性來刪除元素。
// 從切片中刪除元素 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]
要從切片a中刪除索引為index的元素,操作方法是a = append(a[:index], a[index+1:]…)
a:=[...]int{1,2,3} b:=a[:] b =append(b[:1],b[2:]...) fmt.Println(a) fmt.Println(b)
切片元素排序
a:=[]int{10,2,3,100} sort.Ints(a) fmt.Println(a) // [2 3 10 100] b:=[]string{"melon","banana","caomei","apple"} sort.Strings(b) fmt.Println(b) // [apple banana caomei melon] c:=[]float64{3.14,5.25,1.12,4,78} sort.Float64s(c) fmt.Println(c) // [1.12 3.14 4 5.25 78] // 注意:如果是一個數(shù)組,需要先轉(zhuǎn)成切片再排序 [:] sort.Sort(sort.Reverse(sort.IntSlice(a))) sort.Sort(sort.Reverse(sort.Float64Slice(c))) fmt.Println(a,c)
切片拷貝
var s1 = []int{1, 2, 3, 4, 5} var s2 = make([]int, len(s1)) copy(s2, s1) fmt.Println(s2) s3 := []int{4, 5} s4 := []int{6, 7, 8, 9} copy(s4, s3) fmt.Println(s4) //[4 5 3]
練習
func 第1個_指針變量() { //取址 var x int x = 10 //x是整型變量 fmt.Println(x) //=============== var p *int p = &x //取址,p是int類型的指針變量 fmt.Println(p) //取值,v=10 fmt.Println(*p) } func 第2個_new函數(shù)() { //new 和 make 是 Go 語言中用于內(nèi)存分配的原語。簡單來說,new 只分配內(nèi)存,make 用于初始化 slice、map 和 channel。 //之前我們學習的基本數(shù)據(jù)類型聲明之后是有一個默認零值的,但是指針類型呢? var p *int //var p *int = new(int) //new函數(shù)的作用是開辟了一塊兒空間,否則*p = 10就報錯 // fmt.Println(p) // <nil> // fmt.Println(*p) // 報錯,并沒有開辟空間地址 *p = 10 // 報錯 fmt.Println(*p) } func 第3個_數(shù)組() { //數(shù)組其實是和字符串一樣的序列類型,不同于字符串在內(nèi)存中連續(xù)存儲字符,數(shù)組用[]的語法將同一類型的多個值存儲在一塊連續(xù)內(nèi)存中。 //數(shù)組是值類型,不存地址都是值類型 var arr [3]int fmt.Println(arr) fmt.Println(&arr[0]) fmt.Printf("%p\n", &arr[0]) fmt.Println(&arr[1]) fmt.Println(&arr[2]) } func 第4個_基于數(shù)組的切片() { //切片有自己的空間 //存放了3個值,起始地址,長度,容量, //為啥說切片是對數(shù)組的引用? //切片本身不存數(shù)據(jù)?。? } func 第5個_make函數(shù)() { //make返回的還是引用類型的本身,而new返回的是指向類型的指針。 //new()返回的是指針 //make()返回的是引用類型本身,切片本身 var s = make([]int, 3, 5) fmt.Print(s) } func 第6個_append函數(shù)1() { var emps = make([]string, 3, 5) emps[0] = "張三" emps[1] = "李四" emps[2] = "王五" fmt.Println(emps) emps2 := append(emps, "rain") fmt.Println(emps2) //["張三","李四","王五","rain"] emps3 := append(emps2, "eric") fmt.Println(emps3) //["張三","李四","王五","rain","eric"] //容量不夠時發(fā)生二倍擴容 emps4 := append(emps3, "yuan") fmt.Println(emps4) //此時底層數(shù)組已經(jīng)發(fā)生變化,[張三 李四 王五 rain eric yuan] } func 第7個_append函數(shù)2() { var emps = make([]string, 3, 5) emps[0] = "張三" emps[1] = "李四" emps[2] = "王五" fmt.Println(emps) emps2 := append(emps, "rain") fmt.Println(emps2) emps3 := append(emps, "eric") fmt.Println(emps3) //[張三 李四 王五 eric] fmt.Println(emps2) //[張三 李四 王五 eric] } func 第8個_append函數(shù)3() { var s1 = []int{1, 2, 3} s1 = append(s1, 4) fmt.Println(s1) //fmt.Println(s2) }
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
解決panic: assignment to entry in nil
這篇文章主要介紹了解決panic: assignment to entry in nil map問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2008-01-01golang實現(xiàn)openssl自簽名雙向認證的詳細步驟
這篇文章主要介紹了golang實現(xiàn)openssl自簽名雙向認證的詳細步驟,本文分步驟給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-03-03Go 并發(fā)控制context實現(xiàn)原理剖析(小結(jié))
Golang context是Golang應(yīng)用開發(fā)常用的并發(fā)控制技術(shù),這篇文章主要介紹了Go 并發(fā)控制context實現(xiàn)原理剖析(小結(jié)),具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-10-10golang?gin框架實現(xiàn)大文件的流式上傳功能
這篇文章主要介紹了golang?gin框架中實現(xiàn)大文件的流式上傳,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2022-07-07golang執(zhí)行命令獲取執(zhí)行結(jié)果狀態(tài)(推薦)
這篇文章主要介紹了golang執(zhí)行命令獲取執(zhí)行結(jié)果狀態(tài)的相關(guān)知識,非常不錯,具有一定的參考借鑒價值,需要的朋友參考下吧2019-11-11