1. 數組
數組是一個由固定長度的特定類型元素組成的序列,一個數組可以由零個或多個元素組成。
因為數組的長度是固定的,所以在Go語言中很少直接使用數組。
Go語言數組的聲明:
var 數組變量名 [元素數量]Type
1
- 數組變量名:數組聲明及使用時的變量名。
- 元素數量:數組的元素數量,可以是一個表達式,但最終通過編譯期計算的結果必須是整型數值,元素數量不能含有到運行時才能確認大小的數值。
- Type:可以是任意基本類型,包括數組本身,類型為數組本身時,可以實現多維數組。
例子:
//默認數組中的值是類型的默認值
var arr [3]int
從數組中取值:
- 通過索引下標取值,索引從0開始
fmt.Println(arr[0])fmt.Println(arr[1])fmt.Println(arr[2])
- for range獲取
for index,value := range arr{fmt.Printf("索引:%d,值:%d \n",index,value)
}
給數組賦值:
- 初始化的時候賦值
var arr [3]int = [3]int{1, 2, 4}//如果第三個不使用,默認為0var arr1 [3]int = [3]int{1, 2}//可以使用簡短聲明arr2 := [3]int{1, 2, 3}//如果不寫數據數量,而使用...,表示數組的長度是根據初始化值的個數來計算arr3 := [...]int{1, 2, 4, 1, 2}
通過索引下標賦值
var arr [3]intarr[0]=1arr[1]=2arr[2]=4
一定要注意,數組是定長的,不可更改,在編譯階段就決定了
小技巧: 如果覺的每次寫 [3]int 有點麻煩,你可以為 [3]int 定義一個新的類型。
type arr3 [3]intvar arr arr3arr[2] = 9for _, v := range arr {fmt.Println(v)}
如果想要只初始化第三個值怎么寫?
數組比較
如果兩個數組類型相同(包括數組的長度,數組中元素的類型)的情況下,我們可以直接通過較運算符(==和!=)來判斷兩個數組是否相等,只有當兩個數組的所有元素都是相等的時候數組才是相等的,不能比較兩個類型不同的數組,否則程序將無法完成編譯。
a := [2]int{2, 1}b := [2]int{2, 1}fmt.Println(a == b) //true
2. 多維數組
Go語言中允許使用多維數組,因為數組屬于值類型,所以多維數組的所有維度都會在創建時自動初始化零值,多維數組尤其適合管理具有父子關系或者與坐標系相關聯的數據。
聲明多維數組的語法如下所示:
//array_name 為數組的名字,array_type 為數組的類型,size1、size2 等等為數組每一維度的長度。
var array_name [size1][size2]...[sizen] array_type
二維數組是最簡單的多維數組,二維數組本質上是由多個一維數組組成的。
var arr, arr2 [2][2]int//直接初始化arr = [2][2]int{{1, 2}, {2, 1}}//初始化索引為1的的元素arr2 = [2][2]int{1: {2, 3}}
循環取值
var arr [2][2]int//直接初始化arr = [2][2]int{{1, 2}, {2, 1}}for _, v := range arr {fmt.Println(v) //[1 2] [2 1]}
只要類型一致,就可以將多維數組互相賦值,如下所示,多維數組的類型包括每一維度的長度以及存儲在元素中數據的類型:
// 聲明兩個二維整型數組 [2]int [2]int
var array1 [2][2]int
var array2 [2][2]int
// 為array2的每個元素賦值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
// 將 array2 的值復制給 array1
array1 = array2
因為數組中每個元素都是一個值,所以可以獨立復制某個維度,如下所示:
var arr [2][2]intarr = [2][2]int{{1, 2}, {3, 4}}var arr2 [2]intarr2 = arr[1]fmt.Println(arr2) //[3 4]
3. 切片
切片(Slice)與數組一樣,也是可以容納若干類型相同的元素的容器。
與數組不同的是,無法通過切片類型來確定其值的長度。
每個切片值都會將數組作為其底層數據結構。
我們也把這樣的數組稱為切片的底層數組。
切片(slice)是對數組的一個連續片段的引用,所以切片是一個引用類型。
這個片段可以是整個數組,也可以是由起始和終止索引標識的一些項的子集,需要注意的是,終止索引標識的項不包括在切片內(左閉右開的區間)。
Go語言中切片的內部結構包含地址、大小和容量,切片一般用于快速地操作一塊數據集合。
從連續內存區域生成切片是常見的操作,格式如下:
slice [開始位置 : 結束位置]
1
語法說明如下:
- slice:表示目標切片對象;
- 開始位置:對應目標切片對象的索引;
- 結束位置:對應目標切片的結束索引。
從數組生成切片,代碼如下:
var a = [3]int{1, 2, 3}
//a[1:2] 生成了一個新的切片
fmt.Println(a, a[1:2])
從數組或切片生成新的切片擁有如下特性:
- 取出的元素數量為:結束位置 - 開始位置;
- 取出元素不包含結束位置對應的索引,切片最后一個元素使用 slice[len(slice)] 獲取;
- 當缺省開始位置時,表示從連續區域開頭到結束位置(a[:2]);
- 當缺省結束位置時,表示從開始位置到整個連續區域末尾(a[0:]);
- 兩者同時缺省時,與切片本身等效(a[:]);
- 兩者同時為 0 時,等效于空切片,一般用于切片復位(a[0:0])。
注意:超界會報運行時錯誤,比如數組長度為3,則結束位置最大只能為3
切片在指針的基礎上增加了大小,約束了切片對應的內存區域,切片使用中無法對切片內部的地址和大小進行手動調整,因此切片比指針更安全、強大。
3.1 直接聲明新的切片
除了可以從原有的數組或者切片中生成切片外,也可以聲明一個新的切片,每一種類型都可以擁有其切片類型,表示多個相同類型元素的連續集合。
切片類型聲明格式如下:
//name 表示切片的變量名,Type 表示切片對應的元素類型。
var name []Type
// 聲明字符串切片var strList []string// 聲明整型切片var numList []int// 聲明一個空切片var numListEmpty = []int{}// 輸出3個切片fmt.Println(strList, numList, numListEmpty) //[] [] []// 輸出3個切片大小fmt.Println(len(strList), len(numList), len(numListEmpty))// 切片判定空的結果fmt.Println(strList == nil) //truefmt.Println(numList == nil) //truefmt.Println(numListEmpty == nil) //false
切片是動態結構,只能與 nil 判定相等,不能互相判定相等。聲明新的切片后,可以使用 append() 函數向切片中添加元素。
func append(slice []Type, elems ...Type) []Type
3.2 使用 make() 函數構造切片
如果需要動態地創建一個切片,可以使用 make() 內建函數,格式如下:
make( []Type, size, cap )
Type 是指切片的元素類型,size 指的是為這個類型分配多少個元素,cap 為預分配的元素數量,這個值設定后不影響 size,只是能提前分配空間,降低多次分配空間造成的性能問題。
使用 make() 函數生成的切片一定發生了內存分配操作,但給定開始與結束位置(包括切片復位)的切片只是將新的切片結構指向已經分配好的內存區域,設定開始與結束位置,不會發生內存分配操作
思考題:
var numbers4 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
myslice := numbers4[4:6]
//這打印出來長度為2
fmt.Printf("myslice為 %d, 其長度為: %d\n", myslice, len(myslice))
myslice = myslice[:cap(myslice)]
//為什么 myslice 的長度為2,卻能訪問到第四個元素
fmt.Printf("myslice的第四個元素為: %d", myslice[3])
這里,cap(myslice) 返回 myslice 的容量(capacity)。由于 myslice 是從 numbers4 中提取的,它的容量實際上是從原始數組 numbers4 的起始位置到 numbers4 的結束位置的元素數量,即10。因此,myslice[:cap(myslice)] 實際上等同于 myslice[:10],它將 myslice 擴展到了其最大容量,即包含了從原始數組 numbers4 的開始到原始切片結束的所有元素。
4. 切片復制
Go語言的內置函數 copy() 可以將一個數組切片復制到另一個數組切片中,如果加入的兩個數組切片不一樣大,就會按照其中較小的那個數組切片的元素個數進行復制。
copy() 函數的使用格式如下:
copy( destSlice, srcSlice []T) int
其中 srcSlice 為數據來源切片,destSlice 為復制的目標(也就是將 srcSlice 復制到 destSlice),目標切片必須分配過空間且足夠承載復制的元素個數,并且來源和目標的類型必須一致,copy() 函數的返回值表示實際發生復制的元素個數。
下面的代碼展示了使用 copy() 函數將一個切片復制到另一個切片的過程:
slice1 := []int{1, 2, 3, 4, 5}slice2 := []int{6, 7, 8}copy(slice1, slice2) //復制slice2 的前三個元素到slice1中fmt.Println(slice1) //[6 7 8 4 5]copy(slice2, slice1) //復制slice1的前三個元素到slice2中fmt.Println(slice2) //[6 7 8]
切片的引用和復制操作對切片元素的影響:
srcArr := make([]int, 10)for i := 0; i < 10; i++ {srcArr[i] = i}refArr := srcArrcopyArr := make([]int, 10)copy(copyArr, refArr)fmt.Println(srcArr) //[0 1 2 3 4 5 6 7 8 9]srcArr[0] = 999// 打印引用切片的第一個元素 引用數據的第一個元素將會發生變化fmt.Println(refArr) //[999 1 2 3 4 5 6 7 8 9]//由于數據是復制的,因此不會發生變化。fmt.Println(copyArr) //[0 1 2 3 4 5 6 7 8 9]
5. map
map 是一種無序的鍵值對的集合。
map 最重要的一點是通過 key 來快速檢索數據,key 類似于索引,指向數據的值。
map 是一種集合,所以我們可以像迭代數組和切片那樣迭代它。不過,map 是無序的,我們無法決定它的返回順序,這是因為 map 是使用 hash 表來實現的。
map 是引用類型,可以使用如下方式聲明:
//[keytype] 和 valuetype 之間允許有空格。
var mapname map[keytype]valuetype
其中:
- mapname 為 map 的變量名。
- keytype 為鍵類型。
- valuetype 是鍵對應的值類型。
在聲明的時候不需要知道 map 的長度,因為 map 是可以動態增長的,未初始化的 map 的值是 nil,使用函數 len() 可以獲取 map 中 鍵值對的數目。
var mapLit map[string]intvar mapAssigned map[string]intmapLit = map[string]int{"one": 1, "two": 2}mapAssigned = mapLit//mapAssigned 是 mapList 的引用,對 mapAssigned 的修改也會影響到 mapList 的值。mapAssigned["two"] = 3fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"]) //Map assigned at "two" is: 3fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"]) //Map literal at "ten" is: 0
map的另外一種創建方式:
make(map[keytype]valuetype)
1
切記不要使用new創建map,否則會得到一個空引用的指針
map 可以根據新增的 key-value 動態的伸縮,因此它不存在固定長度或者最大限制,但是也可以選擇標明 map 的初始容量 capacity,格式如下:
make(map[keytype]valuetype, cap)
當 map 增長到容量上限的時候,如果再增加新的 key-value,map 的大小會自動加 1,所以出于性能的考慮,對于大的 map 或者會快速擴張的 map,即使只是大概知道容量,也最好先標明。
既然一個 key 只能對應一個 value,而 value 又是一個原始類型,那么如果一個 key 要對應多個值怎么辦?
答案是:使用切片
例如,當我們要處理 unix 機器上的所有進程,以父進程(pid 為整形)作為 key,所有的子進程(以所有子進程的 pid 組成的切片)作為 value。
通過將 value 定義為 []int 類型或者其他類型的切片,就可以優雅的解決這個問題,示例代碼如下所示:
mp1 := make(map[int][]int)
mp2 := make(map[int]*[]int)
5.1 遍歷map
map 的遍歷過程使用 for range 循環完成,代碼如下:
scene := make(map[string]int)scene["cat"] = 66scene["dog"] = 4scene["pig"] = 960for k, v := range scene {fmt.Printf("key :%s,val:%d\n", k, v)}
注意:map是無序的,不要期望 map 在遍歷時返回某種期望順序的結果
5.2 刪除
使用 delete() 內建函數從 map 中刪除一組鍵值對,delete() 函數的格式如下:
delete(map, 鍵)
map 為要刪除的 map 實例,鍵為要刪除的 map 中鍵值對的鍵。
scene := make(map[string]int)// 準備map數據scene["cat"] = 66scene["dog"] = 4scene["pig"] = 960delete(scene, "dog")for k, v := range scene {fmt.Printf("key :%s,val:%d\n", k, v)}
Go語言中并沒有為 map 提供任何清空所有元素的函數、方法,清空 map 的唯一辦法就是重新 make 一個新的 map,不用擔心垃圾回收的效率,Go語言中的并行垃圾回收效率比寫一個清空函數要高效的多。
注意map 在并發情況下,只讀是線程安全的,同時讀寫是線程不安全的。
func main() {myMap := make(map[int]int)//寫go func() {//不停的寫for {myMap[0] = 10}}()//讀go func() {//不停的讀for {_ = myMap[0]}}()// 無限循環, 讓并發程序在后臺執行for {}
}
程序報錯:fatal error: concurrent map read and map write
需要并發讀寫時,一般的做法是加鎖,但這樣性能并不高,Go語言在 1.9 版本中提供了一種效率較高的并發安全的 sync.Map,sync.Map 和 map 不同,不是以語言原生形態提供,而是在 sync 包下的特殊結構。
sync.Map 有以下特性:
- 無須初始化,直接聲明即可。
- sync.Map 不能使用 map 的方式進行取值和設置等操作,而是使用 sync.Map 的方法進行調用,Store 表示存儲,Load 表示獲取,Delete 表示刪除。
- 使用 Range 配合一個回調函數進行遍歷操作,通過回調函數返回內部遍歷出來的值,Range 參數中回調函數的返回值在需要繼續迭代遍歷時,返回 true,終止迭代遍歷時,返回 false。
var color sync.Mapcolor.Store(1, "red")color.Store(2, "blue")color.Store(3, "yellow")fmt.Println(color) //{{0 0} {[] {} 0xc00008a040} map[1:0xc0000a8018 2:0xc0000a8020 3:0xc0000a8028] 0}fmt.Println(color.Load(1)) //red truecolor.Delete(2)color.Range(func(k, v interface{}) bool {fmt.Println("iter:", k, v)return true})//iter: 1 red//iter: 3 yellow
sync.Map 為了保證并發安全有一些性能損失,因此在非并發情況下,使用 map 相比使用 sync.Map 會有更好的性能
6. nil
在Go語言中,布爾類型的零值(初始值)為 false,數值類型的零值為 0,字符串類型的零值為空字符串"",而指針、切片、映射、通道、函數和接口的零值則是 nil。
nil和其他語言的null是不同的。
nil 標識符是不能比較的
func main() {//invalid operation: nil == nil (operator == not defined on nil)fmt.Println(nil==nil)
}
nil 不是關鍵字或保留字
nil 并不是Go語言的關鍵字或者保留字,也就是說我們可以定義一個名稱為 nil 的變量,比如下面這樣:
//但不提倡這樣做
var nil = errors.New("my god")
nil 沒有默認類型
package main
import ("fmt"
)
func main() {//error :use of untyped nilfmt.Printf("%T", nil)print(nil)
}
不同類型 nil 的指針是一樣的
var arr []intvar point *intfmt.Printf("%p\n", arr) //0x0fmt.Printf("%p", point) //0x0
nil 是 map、slice、pointer、channel、func、interface 的零值
var m map[int]intvar p *intvar f func()var ch chan intvar s []intvar i interface{}fmt.Printf("%##v\n", m) //map[int]int(nil)fmt.Printf("%##v\n", p) //(*int)(nil)fmt.Printf("%##v\n", f) //(func())(nil)fmt.Printf("%##v\n", ch) //(chan int)(nil)fmt.Printf("%##v\n", s) //[]int(nil)fmt.Printf("%##v\n", i) //<nil>
零值是Go語言中變量在聲明之后但是未初始化被賦予的該類型的一個默認值。
不同類型的 nil 值占用的內存大小可能是不一樣的
func main() {var p *struct{}fmt.Println( unsafe.Sizeof( p ) ) // 8var s []intfmt.Println( unsafe.Sizeof( s ) ) // 24var m map[int]boolfmt.Println( unsafe.Sizeof( m ) ) // 8var c chan stringfmt.Println( unsafe.Sizeof( c ) ) // 8var f func()fmt.Println( unsafe.Sizeof( f ) ) // 8var i interface{}fmt.Println( unsafe.Sizeof( i ) ) // 16
}
7. new和make
make 關鍵字的主要作用是創建 slice、map 和 Channel 等內置的數據結構,而 new 的主要作用是為類型申請一片內存空間,并返回指向這片內存的指針。
- make 分配空間后,會進行初始化,new分配的空間被清零
- new 分配返回的是指針,即類型 *Type。make 返回引用,即 Type;
- new 可以分配任意類型的數據;