標籤:array lin def 元素 set 隱式 分配 資訊 list
本文內容是本人對Go語言的變數、常量、數組、切片、映射、結構體的備忘錄,記錄了關鍵的相關知識點,以供翻查。
文中如有錯誤的地方請大家指出,以免誤導!轉摘本文也請註明出處,多謝!
參考書籍《Go語言聖經》、《Go語言實戰》、《Go語言學習筆記》等
一、變數
- 變數是一段或多段用來儲存資料的記憶體;
- 變數總是有固定的資料類型,類型決定了所佔記憶體的長度和儲存格式;
- 編譯後的代碼使用變數的記憶體位址來訪問資料,而不是變數名;
- 簡短變數聲明只能在函數內聲明(局部變數),var聲明方式則無限制(但一般用於聲明未顯式初始化的變數);
- 聲明同一範圍中的同名變數時,將回退為賦值,即重用該變數(必須至少有一個新變數定義);
- 而聲明不同範圍的同名變數則為重新定義(覆蓋);
var q intvar y = 453var ( n,m = 134,"srf" n1,m1 int )func f1() { n,m := 25,"sss" n,m1 := 34,"yyy" fmt.Println(n,m,m1) n = n+5 //賦值運算式中,首先計算右值 //“_”空標識符用來臨時規避編譯器對未使用變數和匯入包的錯誤檢查 if _,ok := add1(n);ok { fmt.Println(n) }}func add1(n int) (int, bool) { return n+1,true}
二、常量、枚舉
- 常量是一個不可改變的值,它可以為字面量,或編譯器能計算出結果的運算式。未使用的常量不會引起編譯錯誤;
- 在常量組中如不指定類型和初始值,則與上一行非空常量右值相同;
- 常量會被編譯器在預先處理階段直接展開,作為指令資料使用,所以無法取常量的地址;
const i = 5const ( x byte = 1 x1 x2 //x1,x2均為1 s = "abs" s1 //s1=“abc”)const ( _,_ int = iota,iota*3 //0,0*3 忽略值,並顯式指定類型為int k1,k2 //1,1*3 l1,l2 //2,2*3 o1,o2 = 5,6 //中斷iota自增 r1,r2 //5,6 同上一行 e1,e2 = iota,iota*3 //5,5*3 恢複iota自增,按行遞增)//枚舉type color byteconst ( blue color = iota red green)func main() { t:= blue fmt.Println(t) //0 //fmt.Println(&i) //錯誤:無法對常量取地址 cannot take the address of i}
三、數組
- 數組是切片和映射的基礎資料結構。數組是實值型別,在賦值和傳遞數組時將拷貝整個數組。
- 數組是一個長度固定的資料類型,儲存著一段具有相同資料類型元素的連續記憶體塊。
- 因為數組佔用的記憶體是連續分配的,所以對數組元素的查詢、修改等操作速度很快。
- 聲明數組的方式:
- var array1 [5]int
- array1 := [5]int{3,5,6,3,2}
- array1 := [...]int{3,4,7,8,1} //根據數組字面量中元素的個數來確定數組的長度
- array1 := [5]int{0:3,3:5,4:8} //只初始化指定索引的元素,其餘元素保持零值
- array1 := [...]int{1,2,9:32}
- 數組元素的類型可以為任何內建類型,也可以是某種結構類型,也可以是指標類型。
- 陣列變數的類型包括數組長度和元素的類型,只有兩部分都相同的數組才可相互賦值。
- 多維陣列:數組本身只有一個維度,只能通過組合多個數組來建立多維陣列;內建函數len、cap均返回第一維度長度
- var array [4][2]int
- array := [4][2]int{2:{20,21},3:{41,25}}
- array := [4][2]int{2:{1:21},3:{0:41}}
- array := [...][4]int{{2,3},{4,5}} //僅第一維度允許使用“...”
- array[2][1] = 10
- 在函數間傳遞數組:由於在函數間傳遞變數時,傳遞的總是變數的值的副本,因為數組是實值型別,所以在賦值和傳遞陣列變數時將拷貝整個數組!在定義函數時,對於較大的資料類型應該把參數設計為指標類型,這樣在調用函數時,只需在棧上分配給每個指標8位元組的記憶體,但這意味著會改變指標指向的值(共用的記憶體),其實大部分情況下應該使用切片類型,而不是數組。
- 注意:因為切片的底層數組可能會在堆上分配記憶體,對於小數組在棧上拷貝的消耗可能比make代價小;
四、切片slice
- 切片slice是參考型別,它內部通過指標引用一個底層數組,並設定相關屬性將資料的讀寫操作限定在指定地區。
//切片本身是個唯讀對象,工作機制類似數組指標的一種封裝type slice struct{ array unsafe.Pointer len int //可讀寫的元素數量 cap int //所引用數組片段的真實長度}
- 建立和初始化:
- slice1 := make( []string, 5 ) //建立一個長度、容量都為5的string類型的切片
- slice1 := make( []string, 3, 5 ) //建立一個長度為3,容量為5的string類型的切片
- slice2 := []string{ "red","blue","green" } //長度和容量均為3的切片
- slice2 := []int{ 99:1 } //長度和容量均為100,並初始化第100個元素為1
- 再次切片reslice:以開始和結束原切片的索引位置確定所引用的數組片段,不支援反向索引,實際範圍是一個右半開區間
假設原切片slice容量為k,新切片newSlice為原切片的索引 i 元素位置開始,在原切片的容量範圍內取值
- newSlice := slice[ i : j ] //長度為j-i,容量為k-i
- newSlice := slice[ i : j : n ] //限制新切片的容量為n-i(第三個參數n-1表示新切片可擴充到的最後一個可見的底層數組部分的元素索引,這樣就達到了限制容量的目的,注意:n必須>=j)
- 新切片無法訪問它所指向的底層數組的第一個元素之前的部分(第一個索引之前的部分)
- 例子:ss:=[]int{10,20,30,40,50} newss:=ss[2:4:5] //newss為[30,40],容量為3
- 新切片和舊切片指向同一個底層數組;
//利用reslice實現一個棧式結構(也可將stack定義為一個類型)var stack = make([]int,0,5)func push(x int) error {n:=len(stack)if n == cap(stack) {return errors.New("stack is full")}stack = stack[:n+1] //新的stack增加了一個可訪問元素stack[n]stack[n]=xreturn nil}func pop() (int, error) {n:=len(stack)if n == 0 {return 0,errors.New("stack is empty")}x:=stack[n-1]stack = stack[:n-1] //新的stack減少了一個可訪問元素stack[n-1]return x,nil}func main() {for i := 0; i < 7; i++ {fmt.Printf("push %d: %v,%v\n",i,push(i),stack)}for i := 0; i < 7; i++ {x,err:=pop()fmt.Printf("push %d: %v,%v\n",x,err,stack)}}
- 切片的長度可以按需自動成長或縮小:
- 動態增長是通過append()函數實現的
- 縮小則是通過對它再次切片來實現,通過再次切片獲得的新切片將和原切片共用底層數組,它們的指標指向同一個底層數組。
- 由於切片只是引用了底層數組,底層數組的資料並不屬於切片本身,所以一個切片只需要24位元組的記憶體(在64位機器上):指標欄位8位元組、長度欄位8位元組、容量欄位8位元組。所以在函數之間直接傳遞切片是高效的,只需分配24位元組的棧記憶體。
- nil切片和空切片:
- nil切片:只聲明,但未初始化的切片,如var slice1 []int,nil切片可用來描述一個不存在的切片
- 空切片:長度和容量均為0的切片,建立空切片時未對底層數組元素分配任何記憶體,可用來表示空集合,如slice1 := make( []int, 0 ),slice2 := []int{}
- 對nil切片和空切片調用內建函數append、len、cap的效果一樣
- append()函數:
slice = append(slice, elem1, elem2) //一次可追加多個值
slice = append(slice, anotherSlice...) //使用“...”將anotherSlice的所有元素追加到slice裡
- 當slice還有可用的容量時,append()操作將可用的元素合并到切片的長度,並對其賦值,最後返回一個全新的切片(和舊切片共用同一個底層數組);
- 如果slice的容量不足時,append()操作會建立一個新的底層數組,並將被引用的舊值複製到新數組裡,然後追加新的值;
- 原切片容量不足時,且小於1000,則新切片的容量為原容量的2倍,若大於1000,則容量的增長因子變為1.25;
- 由於容量不足時,append操作會返回一個具有自己獨立的底層數組的新切片,即與原切片不共用同一底層數組,對新切片元素的修改將不會影響原切片的底層數組,技巧:在建立切片時設定長度等於容量,這樣就可以強制在第一次append操作時建立新的底層數組,達到與原數組分離的目的,如newSlice := oldSlice[2:3:3]
- copy函數:在兩個切片對象之間複製資料,允許指向同一個底層數組,允許目標區間重疊。最終所複製長度以較短的切片長度為準
- 切片的迭代如:for index, value := range slice{ .... },index為當前迭代到的索引位置,value是從slice的副本中取值,index和value變數的記憶體位址是不變的,只是指向的值在不斷更新。
- len函數可返還切片的長度、cap函數可返還切片的容量
- 多維切片(類似交錯數組):切片和數組一樣,本身是一維的,可以組合多個切片來形成多維切片,如:slice := [][]int{ {12},{34,23} },slice[0]為{12},slice[1]為{34,23}
- 注意:如果切片長時間引用大數組中很小的片段,則應該複製出所需資料,建立獨立切片,以便原大數組記憶體可被及時回收;
五、映射map
- 映射map:是一個儲存索引值對的無序集合,它能基於鍵快速檢索資料,鍵就像索引一樣,指向與該鍵關聯的值;
- 映射是無序的,每次迭代它時順序可能不一樣,因為映射內部使用了散列表;
- 映射的散列表包含一組桶,每個桶裡儲存著一部分索引值對;
- 映射內部使用了兩個數組:
- 第一個數組:儲存著用於選擇桶的散列鍵的高八位值,該數組用於區分每個索引值對要存在哪個桶裡;
- 第二個數組:每個桶裡都有一個位元組數組,先依次儲存了該桶裡的所有鍵,之後儲存了該桶的所有值;
- 在儲存、刪除、尋找映射的索引值對時,會把指定的鍵傳給映射的散列函數,該函數把鍵轉換為一個散列值,然後把該散列值與第一個數組比對來選擇哪個桶,再到桶裡的位元組數組裡尋找對應的鍵和值;
- 建立和初始化映射:
- dict1 := make(map[string]int) //空映射,等同於dict1 := map[string]int{}
dict1 := make(map[string]int, 1000) //預先分配足夠記憶體空間,有助於提升效能
dict2 := map[string]int{"srf":143,"robt":342}
- 映射的鍵:只能是能用“==”做比較的類型,但不可以是切片、函數、以及包含切片的類型,因為他們具有引用語義。而映射的值則可以是任意類型;
- nil映射是只聲明而未初始化的映射,無法直接使用,如var dict map[string]int。空映射則可以直接使用;
- map類型的零值是nil,也就是沒有引用任何雜湊表。在向map存資料前必須先建立map,即:引用一個雜湊表。
- 如果要用map儲存大量小對象,應該直接儲存為實值型別,而非指標類型,有助於減少需要掃描的對象數量,大幅縮短記憶體回收時間;
- 從映射中取值:
- value := dict2["srf"] //鍵存在時返回對應的值,不存在時傳回型別的零值
- value, ok := dict2["srf"] //ok為鍵是否存在的布爾標誌
if ok { ...... }
- map中的元素並不是一個變數,我們不能對map的元素進行取址操作(因為map可能會在動態增長時重新分配記憶體),因此無法直接修改value成員,而應該通過臨時變數來修改,或把值定義為指標類型:
m := users[int]user{ 1:{"srf",25}}//m[1].age +=1 //錯誤,無法設定值u := m[1]u.age+=1m[1] = u
- 遍曆映射:
- for key := range dict2 { ..... } //只接收鍵
- for key, value := range dict2 { ...... } //同時接收鍵和值
- 遍曆映射時,可以添加、刪除成員
- 遍曆映射的索引值對時的順序是隨機,若要有序的獲得映射的索引值對,則需要先遍曆出映射的鍵存到一個切片中,然後排序該切片,最後遍曆該切片,按切片中元素的順序去映射中取對應的值
- delete(dict2,"srf") 從映射中刪除指定的索引值對;
- 運行時會對映射的並行作業做出檢測,對映射的操作只能同步進行(同一時刻只能有一個任務在操作映射),否則會導致進程崩潰。可用讀寫鎖sync.RWMutex實現同步,避免讀寫操作同時進行:
func main() {var lock sync.RWMutexm:=make(map[string]int)go func() {for {lock.Lock()m["a"] += 1lock.Unlock() //不能用defertime.Sleep(time.Microsecond)}}()go func() {for {lock.RLock()_ = m["b"]lock.RUnlock()time.Sleep(time.Microsecond)}}()select {} //阻止進程退出}
- 在函數間傳遞映射與傳遞切片一樣(無須再次取地址),傳遞的只是映射本身的副本,而不會複製映射所引用的所有底層資料結構,對該映射副本所做的修改將會反映到所有對這個映射的引用。
- 多維映射:即值為映射類型的映射。使用時應注意,作為值的映射也需要初始化後才能使用,如:
var m1 = make(map[int]map[string]string)
m1[13]= map[string]string{"srf":"yes"}
- 判斷兩個map是否相等的函數:
func equal(x, y map[string]int) bool { if len(x) != len(y) { return false } for k, xv := range x { if yv, ok := y[k]; !ok || yv != xv { return false } } return true}
m:=make(map[string]bool)if !m["srf"] { m["srf"] = true}
六、結構體
- 結構體struct是一種複合類型,由多個不同類型的命名欄位(field)系列打包而成;
- 欄位名必須唯一,可用“_”補位,支援使用自身的指標類型成員(這可以讓我們建立遞迴的資料結構,比如鏈表和樹結構等);
type node struct{ _ int id int `帳號` next *node}
- 結構體類型資訊包括:欄位名、欄位標籤、排列順序,只有三者全部相同才可認為是同一類型;
- 可按順序初始化全部欄位,但建議使用命名方式初始化部分或全部欄位(可忽視欄位的定義順序,便於結構體成員順序的修改、擴充);
- 結構體的比較:只有當結構體的所有成員都是可比較的,那麼該結構體才是可比較的
- 可直接定義一個匿名的結構體類型,並賦值給一個變數,或用作欄位的類型(匿名結構體欄位無法用字面量形式直接初始化,需要“.”文法來初始化其成員)
u := struct{ name string}type file struct{ name string attr struct{ owner int perm int }}f := file{name:"test.dat"}f.attr.owner = 1f.attr.perm = 0755
- 空結構:struct{},長度為0,不分配記憶體,它和其它所有“長度”為0的對象通常都指向runtime.zerobase變數(即它們都指向同一個變數);空結構類型經常作為通道元素的類型,用於事件通知(優點是不佔記憶體);
- 匿名欄位(嵌入類型):即沒有指定顯式的名稱,只有類型的欄位:
- 編譯器將隱式地以類型名作為欄位名稱(不包含包名);
- 外層的結構體不僅獲得了匿名成員類型的所有成員,而且也獲得了該類型全部的匯出的方法;
- 可直接引用嵌入類型欄位的成員,但在以字面量文法初始化時須顯式初始化它的整個結構;
- 匿名欄位的成員的資料類型必須是命名的類型或指向一個命名的類型的指標,不能是介面指標和多級指標;
- 不能將基礎類型和其指標類型同時作為匿名欄位
- 欄位重名處理:優先使用外層欄位(內層欄位被遮蔽了,只能通過完全限定名來訪問),對於多個相同層級的同名欄位也必須通過完全限定名來訪問,否則編譯器無法確定目標;
- 欄位標籤(tag):用來對欄位進行描述的中繼資料,它雖然不屬於資料成員,但卻是類型資訊的組成部分;在運行期,可用反射來擷取欄位的標籤資訊,它常被用作格式檢驗(如JSON)、資料庫關係映射等;標準庫reflect.StructTag提供了分/解析標籤的功能;
type user struct{ name string `暱稱` sex byte `性別`}func main(){ u:=user{"TOM",1} v:=reflect.ValueOf(u) t:=v.Type() for i,n:=0,t.NumField();i<n;i++{ fmt.Printf("%s: %v\n", t.Field(i).Tag, v.Field(i)) }}
- 不管結構體有多少個欄位,它的記憶體總是一次性分配的,各欄位在相鄰的地址空間按定義順序排列(包含嵌入欄位的所有 成員)。對於參考型別、字串、指標,結構記憶體中只包含其基本(頭部)資料。
- 結構體在分配記憶體時,會進行記憶體對齊處理(根據所有欄位中最長的基礎類型寬度為標準),唯一例外是編譯器把空結構類型欄位作為最後一個欄位時的長度視為1來做對齊處理(避免越界)。
- 記憶體對齊與硬體平台、以及訪問效率有關(CPU在訪問自然對齊的資料時需要的讀周期更少,還可避免拼接資料)
Go語言備忘錄:基本資料結構