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


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

文中如有錯誤的地方請大家指出,以免誤導!轉摘本文也請注明出處:Go語言備忘錄(1):基本數據結構多謝!

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

目錄:

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

一、變量

  •  變量是一段或多段用來存儲數據的內存;
  • 變量總是有固定的數據類型,類型決定了所占內存的長度和存儲格式;
  • 編譯后的代碼使用變量的內存地址來訪問數據,而不是變量名;
  • 簡短變量聲明只能在函數內聲明(局部變量),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
}

  

二、常量、枚舉
  • 常量是一個不可改變的值,它可以為字面量,或編譯器能計算出結果的表達式。未使用的常量不會引起編譯錯誤;
  • 在常量組中如不指定類型和初始值,則與上一行非空常量右值相同;
  • 常量會被編譯器在預處理階段直接展開,作為指令數據使用,所以無法取常量的地址;
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
}

  

三、數組

  • 數組是切片和映射的基礎數據結構。數組是值類型,在賦值和傳遞數組時將拷貝整個數組。
  • 數組是一個長度固定的數據類型,存儲着一段具有相同數據類型元素的連續內存塊。
  • 因為數組占用的內存是連續分配的,所以對數組元素的查詢、修改等操作速度很快。
  • 聲明數組的方式:
    • 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]=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}
  • 注意:如果切片長時間引用大數組中很小的片段,則應該復制出所需數據,新建獨立切片,以便原大數組內存可被及時回收;
 
五、映射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
}

 

六、結構體

  • 結構體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在訪問自然對齊的數據時需要的讀周期更少,還可避免拼接數據)

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM