Go語言之重要數組類型切片(slice)make,append函數解讀
切片是一個動態(tài)數組,因為數組的長度是固定的,所以操作起來很不方便,比如一個names數組,我想增加一個學生姓名都沒有辦法,十分不靈活。
所以在開發(fā)中數組并不常用,切片類型才是大量使用的。
切片基本操作
切片的創(chuàng)建有兩種方式:
- 從數組或者切片上切取獲得
- 直接聲明切片 : var name []Type // 不同于數組, []沒有數字
切片語法:
arr [start : end] 或者 slice [start : end] // start: 開始索引 end:結束索引
切片特點:
- 左閉右開 [ )
- 取出的元素數量為:結束位置 - 開始位置;
- 取出元素不包含結束位置對應的索引,切片最后一個元素使用 slice[len(slice)]獲??;
- 當缺省開始位置時,表示從連續(xù)區(qū)域開頭到結束位置;當缺省結束位置時,表示從開始位置到整個連續(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]值類型和引用類型
數據類型從存儲方式分為兩類:值類型和引用類型!
(1) 值類型
基本數據類型(int,float,bool,string)以及數組和struct都屬于值類型。
特點:變量直接存儲值,內存通常在棧中分配,棧在函數調用完會被釋放。值類型變量聲明后,不管是否已經賦值,編譯器為其分配內存,此時該值存儲于棧上。
var a int //int類型默認值為 0 var b string //string類型默認值為 nil空 var c bool //bool類型默認值為false var d [2]int //數組默認值為[0 0]
當使用等號=將一個變量的值賦給另一個變量時,如 j = i ,實際上是在內存中將 i 的值進行了拷貝,可以通過 &i 獲取變量 i 的內存地址。此時如果修改某個變量的值,不會影響另一個。
// 整型賦值
var a =10
b := a
b = 101
fmt.Printf("a:%v,a的內存地址是%p\n",a,&a)
fmt.Printf("b:%v,b的內存地址是%p\n",b,&b)
//數組賦值
var c =[3]int{1,2,3}
d := c
d[1] = 100
fmt.Printf("c:%v,c的內存地址是%p\n",c,&c)
fmt.Printf("d:%v,d的內存地址是%p\n",d,&d)(2) 引用類型
指針、slice,map,chan,interface等都是引用類型。
特點:變量通過存儲一個地址來存儲最終的值。內存通常在堆上分配,通過GC回收。
引用類型必須申請內存才可以使用,new()和make()是給引用類型申請內存空間。
切片原理
切片的構造根本是對一個具體數組通過切片起始指針,切片長度以及最大容量三個參數確定下來的
type Slice struct {
Data uintptr // 指針,指向底層數組中切片指定的開始位置
Len int // 長度,即切片的長度
Cap int // 最大長度(容量),也就是切片開始位置到數組的最后位置的長度
}
var arr = [5]int{10, 11, 12, 13, 14}
s1 := arr[0:3] // 對數組切片
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])
// 每一個切片都有一塊自己的空間地址,分別存儲了對于數組的引用地址,長度和容量
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))除了可以從原有的數組或者切片中生成切片外,也可以聲明一個新的切片,每一種類型都可以擁有其切片類型,表示多個相同類型元素的連續(xù)集合,因此切片類型也可以被聲明,切片類型聲明格式如下:
var name []Type // []Type是切片類型的標識
其中 name 表示切片的變量名,Type 表示切片對應的元素類型。
var names = []string{"張三","李四","王五"}
fmt.Println(names,reflect.TypeOf(names)) // [張三 李四 王五 趙六 孫七] []string直接聲明切片,會針對切片構建底層數組,然后切片形成對數組的引用
練習
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函數
變量的聲明我們可以通過var關鍵字,然后就可以在程序中使用。當我們不指定變量的默認值時,這些變量的默認值是他們的零值,比如int類型的零值是0,string類型的零值是"",引用類型的零值是nil。
對于例子中的兩種類型的聲明,我們可以直接使用,對其進行賦值輸出。但是如果我們換成引用類型呢?
// arr := []int{}
var arr [] int // 如果是 var arr [2] int
arr[0] = 1
fmt.Println(arr)從這個提示中可以看出,對于引用類型的變量,我們不光要聲明它,還要為它分配內容空間。
對于值類型的聲明不需要,是因為已經默認幫我們分配好了。要分配內存,就引出來今天的make函數。
make也是用于chan、map以及切片的內存創(chuàng)建,而且它返回的類型就是這三個類型本身。
如果需要動態(tài)地創(chuàng)建一個切片,可以使用 make() 內建函數,格式如下:
make([]Type, size, cap)
其中 Type 是指切片的元素類型,size 指的是為這個類型分配多少個元素,cap 為預分配的元素數量,這個值設定后不影響 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() 函數生成的切片一定發(fā)生了內存分配操作,但給定開始與結束位置(包括切片復位)的切片只是將新的切片結構指向已經分配好的內存區(qū)域,設定開始與結束位置,不會發(fā)生內存分配操作。
a := make([]int, 5) b := a[0:3] a[0] = 100 fmt.Println(a) fmt.Println(b)
append(重點)
上面我們已經講過,切片作為一個動態(tà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) // 此時底層數組已經發(fā)生變化
擴容機制
1、每次 append 操作都會檢查 slice 是否有足夠的容量,如果足夠會直接在原始數組上追加元素并返回一個新的 slice,底層數組不變,但是這種情況非常危險,極度容易產生 bug!而若容量不夠,會創(chuàng)建一個新的容量足夠的底層數組,先將之前數組的元素復制過來,再將新元素追加到后面,然后返回新的 slice,底層數組改變而這里對新數組的進行擴容
2、擴容策略:如果切片的容量小于 1024 個元素,于是擴容的時候就翻倍增加容量。上面那個例子也驗證了這一情況,總容量從原來的4個翻倍到現在的8個。一旦元素個數超過 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的目的是丟棄老數組
// 案例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個切片在切片開頭添加元素一般都會導致內存的重新分配,而且會導致已有元素全部被復制 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 調用都會創(chuàng)建一個臨時切片,并將 a[i:] 的內容復制到新創(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]
// 注意:如果是一個數組,需要先轉成切片再排序 [:]
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函數() {
//new 和 make 是 Go 語言中用于內存分配的原語。簡單來說,new 只分配內存,make 用于初始化 slice、map 和 channel。
//之前我們學習的基本數據類型聲明之后是有一個默認零值的,但是指針類型呢?
var p *int
//var p *int = new(int) //new函數的作用是開辟了一塊兒空間,否則*p = 10就報錯
// fmt.Println(p) // <nil>
// fmt.Println(*p) // 報錯,并沒有開辟空間地址
*p = 10 // 報錯
fmt.Println(*p)
}
func 第3個_數組() {
//數組其實是和字符串一樣的序列類型,不同于字符串在內存中連續(xù)存儲字符,數組用[]的語法將同一類型的多個值存儲在一塊連續(xù)內存中。
//數組是值類型,不存地址都是值類型
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個_基于數組的切片() {
//切片有自己的空間
//存放了3個值,起始地址,長度,容量,
//為啥說切片是對數組的引用?
//切片本身不存數據?。?
}
func 第5個_make函數() {
//make返回的還是引用類型的本身,而new返回的是指向類型的指針。
//new()返回的是指針
//make()返回的是引用類型本身,切片本身
var s = make([]int, 3, 5)
fmt.Print(s)
}
func 第6個_append函數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) //此時底層數組已經發(fā)生變化,[張三 李四 王五 rain eric yuan]
}
func 第7個_append函數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函數3() {
var s1 = []int{1, 2, 3}
s1 = append(s1, 4)
fmt.Println(s1)
//fmt.Println(s2)
}總結
以上為個人經驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關文章
Golang使用Apache PLC4X連接modbus的示例代碼
Modbus是一種串行通信協議,是Modicon公司于1979年為使用可編程邏輯控制器(PLC)通信而發(fā)表,這篇文章主要介紹了Golang使用Apache PLC4X連接modbus的示例代碼,需要的朋友可以參考下2024-07-07

