Go語言備忘錄(1):基本數據結構

本文內容是本人對Go語言的變量、常量、數組、切片、映射、結構體的備忘錄,記錄了關鍵的相關知識點,以供翻查。html

文中若有錯誤的地方請你們指出,以避免誤導!轉摘本文也請註明出處:Go語言備忘錄(1):基本數據結構多謝!node

 參考書籍《The Go Programming Language》、《Go In Action》、《Go語言學習筆記》等數據庫

目錄:數組

  1. 變量
  2. 常量
  3. 數組
  4. 切片
  5. 映射
  6. 結構體

1、變量數據結構

  •  變量是一段或多段用來存儲數據的內存;
  • 變量老是有固定的數據類型,類型決定了所佔內存的長度和存儲格式;
  • 編譯後的代碼使用變量的內存地址來訪問數據,而不是變量名;
  • 簡短變量聲明只能在函數內聲明(局部變量),var聲明方式則無限制(但通常用於聲明未顯式初始化的變量);
  • 聲明同一做用域中的同名變量時,將回退爲賦值,即重用該變量(必須至少有一個新變量定義);
  • 而聲明不一樣做用域的同名變量則爲從新定義(覆蓋);
var q int
var y = 453
var (
    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
}

  

2、常量、枚舉
  • 常量是一個不可改變的值,它能夠爲字面量,或編譯器能計算出結果的表達式。未使用的常量不會引發編譯錯誤;
  • 在常量組中如不指定類型和初始值,則與上一行非空常量右值相同;
  • 常量會被編譯器在預處理階段直接展開,做爲指令數據使用,因此沒法取常量的地址;
const i = 5
const (
    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 byte
const (
    blue color = iota
    red
    green
)
func main() {
    t:= blue
    fmt.Println(t) //0
    //fmt.Println(&i) //錯誤:沒法對常量取地址 cannot take the address of i
}

  

3、數組併發

  • 數組是切片和映射的基礎數據結構。數組是值類型,在賦值和傳遞數組時將拷貝整個數組。
  • 數組是一個長度固定的數據類型,存儲着一段具備相同數據類型元素的連續內存塊。
  • 由於數組佔用的內存是連續分配的,因此對數組元素的查詢、修改等操做速度很快。
  • 聲明數組的方式:
    • 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代價小;
4、切片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]=x
	return 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}
  • 注意:若是切片長時間引用大數組中很小的片斷,則應該複製出所需數據,新建獨立切片,以便原大數組內存可被及時回收;
 
5、映射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+=1
m[1] = u
  • 遍歷映射:
    • for key := range dict2 { ..... } //只接收鍵
    • for key, value := range dict2 { ...... } //同時接收鍵和值
    • 遍歷映射時,能夠添加、刪除成員
    • 遍歷映射的鍵值對時的順序是隨機,若要有序的得到映射的鍵值對,則須要先遍歷出映射的鍵存到一個切片中,而後排序該切片,最後遍歷該切片,按切片中元素的順序去映射中取對應的值
  • delete(dict2,"srf") 從映射中刪除指定的鍵值對;
  • 運行時會對映射的併發操做作出檢測,對映射的操做只能同步進行(同一時刻只能有一個任務在操做映射),不然會致使進程崩潰。可用讀寫鎖sync.RWMutex實現同步,避免讀寫操做同時進行:
func main() {
	var lock sync.RWMutex
	m:=make(map[string]int)

	go func() {
		for {
			lock.Lock()
			m["a"] += 1
			lock.Unlock()  //不能用defer

			time.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
}
  • 用map來表示字符串的集合set: 
m:=make(map[string]bool)
if !m["srf"] {
    m["srf"] = true
}

 

6、結構體app

  • 結構體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 = 1
f.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在訪問天然對齊的數據時須要的讀週期更少,還可避免拼接數據)
相關文章
相關標籤/搜索