Go語言進階之路(一):變量、類型、數組、切片、字典和結構體

一 類型

Go語言中內置的基礎類型和各類型的初始值爲:

類型名稱 類型 初始值
字節類型 byte 0
整型 int、int8、int16、int32、int64、rune 0
無符號整型 uint、uint8、uint16、uint32、uint64、uintptr 0
布爾類型 bool false
字符串類型 string ""
浮點類型 float32、float64 0
複數類型 complex64、complex128 (0+0i)

啥叫初始值?也就是沒有初始化的值,比如:

var a int
fmt.Println(a)  // 變量a沒有被代碼初始化,a現在的值就是初始值,此處打印出0。

等一下,這裏面混進了兩個比較奇怪的東西,rune和uintptr。rune是Unicode類型,和int32等價,在後續的文章中講string的時候會重點介紹,uintptr是無符號整數,存放的是指針的值,可以理解爲用來保存指針。

 

Go語言中除了這些基礎類型,還有數組、切片、字典、指針、結構體、通道、函數、接口這些類型,後續的文章會詳細講這些。

二 變量

Go語言定義變量使用var關鍵字。定義變量時可以選擇指定類型,或者讓編譯器自動推導出類型,可以指定初始化值,也可以使用編輯器的初始值。如下:

var a int  // a沒有初始化值,會使用編譯器中int的默認值0。
var b int = 2  // 聲明變量時指定類型,同時指定初始化值。
var c = 2  // 聲明變量時指定初始化值,讓編譯器推導出類型。
var d, e int = 3, 4  // 同時聲明多個變量

Go語言中還有一種簡短的聲明變量方式,即使用“:=”。

b := 2  // 簡短的聲明變量b。

注意,“:=”是用於聲明變量的符號,使用“:=”,符號左邊一定要至少有一個新生命的變量。如:

b := 3  // 此行報錯,b在上面已經被聲明過了。

b, c := 4, 5  // 此行正確,因爲符號左邊至少有一個新聲明的變量c。這一行執行完後,b的值就變成4了。

2.1 常量

Go語言聲明常量很簡單,使用const關鍵字就行。

const a = "2"
a = "3"  // 編譯錯誤,a是常量,不能再修改它的值。

2.2 iota計數器

當我們要多個常量來表示計數器的時候,可以使用Go語言內置的常量計數器。當iota出現時,對應的變量值爲0,每往下一行,變量值就加1。

const (
	a = iota  // iota出現的地方,該變量值爲0,即a的值爲0。每往下一行,變量值就加1。
	b         // b的值爲a的值加1,即1。
	c = "str" // c設定了初始化值,破壞了iota往下的賦值規則,因此iota失效。c的值爲str。
	d         // 常量d未指定初始化值,自動使用上一行的初始化值,dd值爲str。
)
fmt.Println(a)  // 輸出0
fmt.Println(b)  // 輸出1
fmt.Println(c)  // 輸出str
fmt.Println(d)  // 輸出e

iota再次出現時,變量值重新爲0。每往下一行,變量值就加1。

const (
	i = iota  // i的值爲0
	j = iota  // iota再次出現,變量值也爲0,即j的值爲0,每往下一行,變量值就加1。
	_         // 跳過一個iota值
	k
)
fmt.Println(i)  // 輸出0
fmt.Println(j)  // 輸出0
fmt.Println(k)  // 輸出2

iota值可以跳過,需要使用Go語言中的跳過操作符(也稱爲垃圾操作符),即“_”。

const (
	i = iota  // iota再次出現,變量值也爲0,每往下一行,變量值就加1。
	j
	_         // 跳過一個iota值
	k
)
fmt.Println(i)  // 輸出0
fmt.Println(j)  // 輸出1
fmt.Println(k)  // 輸出3

三 邏輯判斷、循環

3.1 邏輯判斷

Go語言中使用if進行邏輯判斷,判斷條件不需要用小括號括起來,判斷後執行的內容需要用大括號括起來。

if 7%2 == 0 {
    fmt.Println("7 is even")
} else {
    fmt.Println("7 is odd")
}
if 8%4 == 0 {
    fmt.Println("8 is divisible by 4")
}

if語句裏面也可以聲明變量!聲明變量的生命週期隨着if結束就結束了。

if num := 9; num < 0 {       // num的生命週期只在此if...else中
    fmt.Println(num, "is negative")
} else if num < 10 {
    fmt.Println(num, "has 1 digit")
} else {
    fmt.Println(num, "has multiple digits")
}

3.2 循環

Go語言中有for關鍵字,沒有while關鍵字。但可以使用for來實現while。for循環使用有四種方式:

無限循環:

var i = 0
for {
    fmt.Println(i)
    i++
}

只帶條件的循環:

i := 1
for i <= 3 {
    fmt.Println(i)
    i = i + 1
}

帶變量聲明和條件的循環:

for n := 0; n <= 5; n++ {
    if n % 2 == 0 {
        continue
    }
    fmt.Println(n)
}

range循環:

sp := []int{1, 2, 3, 4, 5}
for index, item := range sp {
    fmt.Println("元素位置=", index, "元素值=", item)
}


kvs := map[string]string{"a": "apple", "b": "banana"}
for k, v := range kvs {
    fmt.Printf("%s -> %s\n", k, v)
}

for k := range kvs {
    fmt.Println("key:", k)
}



for i, c := range "go" {
    fmt.Println(i, c)
    //0 103
    //1 111
}

四 數組和切片

Go語言中聲明數組很簡單。

var arr = [3]int{1, 2, 3}
fmt.Println(arr)  // 打印出[1, 2, 3]
fmt.Println(len(arr))  // 打印出3

聲明切片可以使用make,也可以直接聲明。

s1 := make([]string, 3, 5)  // 創建了一個長度爲3,容量爲5的字符串切片
s2 := make([]string, 3)  // 創建了一個長度爲3,容量也爲3的字符串切片
s3 := []string{}  // 創建了一個長度爲0,容量爲0的字符串切片
var s4 []string  // 創建了一個長度爲0,容量爲0的字符串nil切片

切片和數組比較像,可以把切片理解爲會自動增長的數組。切片內部有指向底層數組的指針,同時,切片還具有長度和容量。如下:

var s = make([]int, 3, 5)   // make第一個參數爲切片的類型,第二個參數爲切片的長度,第三個參數爲切片的容量
s[0] = 10
s[1] = 20
s[2] = 30

則切片s的底層實現爲:

當長度大於容量時,往切片中添加元素,會導致切片擴容。

var s = make([]int, 3, 5)
fmt.Println(s)  // 輸出[0, 0, 0],s的長度爲3,s的容量爲5。

s = append(s, 1)  // 往切片末尾添加1個元素,s的元素值現在爲[0, 0, 0, 1],s的容量爲5。

s = append(s, 2, 3)  // 再往s末尾添加2個元素,s底層數組已經無法再容下2個元素,s自動擴容,容量翻倍,然後再往s中添加元素。s的元素值現在爲[0, 0, 0, 1, 2, 3],s的容量爲10。

注意:append函數會返回一個操作後的切片,一般來說,使用append操作切片後,要賦值給原來的那個切片變量,要不然可能會有預期不到的效果。如下:

var s = make([]int, 3, 5)  //
s1 := append(s, 1)  // 往s長度的末尾添加1個元素,添加後長度仍小於容量,因此,直接操作s底層數組添加元素。s長度爲3,因此在底層數組第4個位置添加元素1。

fmt.Println(s1)  // 輸出 [0, 0, 0, 1]
fmt.Println(len(s1))  // 輸出4
fmt.Println(cap(s1))  // 輸出5

// 這個時候,s變成了多少呢?
fmt.Println(s)  // 輸出 [0, 0, 0]
fmt.Println(len(s))  // 輸出3
fmt.Println(cap(s))  // 輸出5

上面這個例子可以看到,往切片中append添加元素後,如果不賦值給原來那個切片變量,那麼在我們看來,就感覺切片沒有任何變化一樣。

 

在Go語言中,append導致切片擴容時,如果切片容量小於1000,總是會成倍的擴容底層數組。大於1000時,則會以1.25倍的數量擴容底層數組。

截取切片的內容

切片截取可以使用s[index:length:capability],其中第一個參數index表示截取時的起始位置,第二個參數表示長度所到的位置,第三個參數表示容量所到的位置,第三個參數可以不傳。

s := []int{1,2,3,4,5}  // s切片長度爲5,容量爲5。
s1 := s[2:4]  
fmt.Println(s1)  // 輸出[3, 4]

上面這個例子中,取s中座標2(包含)到4(不包含)的元素賦值給s1,賦值後s和s1的底層數組共享了一部分內存。截取後,s1的長度爲2到5,即長度爲3,容量爲從2開始,到s的容量末尾爲止,即容量爲2到5,即s1的容量爲3。

 

帶容量的切片截取:

s := []int{1,2,3,4,5}  // s切片長度爲5,容量爲5。
s2 := s[2:3:3]
fmt.Println(s2)  // 輸出[3]

截取s中座標2(包含)到座標3(不包含)的內容給s2,同時容量爲座標2(包含)到座標3(不包含)的長度。截取後,s2的長度爲1,容量爲1。

五 字典/映射(map)

Go語言中的字典類似於哈希表、Java中的Map,Python中的dict。

創建map可以用make創建,也可以直接創建。

m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13

m1 := map[string]int{
    "k1": 7,
    "k2": 13,
}

添加元素

m := make(map[string]int)
m["k1"] = 7

獲取元素

m := make(map[string]int)
value := m["k1"]

如果元素不存在,m["k1"]會返回類型的默認值。看下面這個例子:

m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
m["k4"] = 0

value := m["k3"]  // m中沒有k3,因此,返回m的value對應的int類型的默認值,即0。

value2 := m["k4"]  // m中有k4,對應的值爲0。

value和value2的值都是0,怎麼判斷m中是否存在某個key呢?其實,m["k3"]不僅會返回元素對應的值,還會返回一個bool值,表明這個元素是否存在於map中。看下面的例子:

m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
m["k4"] = 0

value, exist := m["k3"]
fmt.Println(exist)  // 輸出false
value2, exist := m["k4"]
fmt.Println(exist)  // 輸出true

刪除元素

delete(m1, "k1")

用for...range遍歷map:

m := make(map[string]int)
m["k1"] = 7
m["k2"] = 13
m["k4"] = 0

for key, value := range m {
    fmt.Println("key=", key, "value", value)
}

for key := range m {
    fmt.Println("key=", key, "value", m[key])
}

六 結構體

Go語言中的結構體類似於C語言中的struct。

type person struct {
    name string
    age  int
}

fmt.Println(person{"Bob", 20})    // {Bob 20}
fmt.Println(person{name: "Alice", age: 30})  // {Alice 30}
fmt.Println(person{name: "Fred"}) // {Fred 0}
fmt.Println(&person{name: "Ann", age: 40})  // &{Ann 40}


s := person{name: "Sean", age: 50}
fmt.Println(s.name)               // Sean
sp := &s
fmt.Println(sp.age)               // 50
sp.age = 51
fmt.Println(sp.age)               // 51
fmt.Println(s.age)                // 51

嵌入類型/嵌入字段

Go 語言規範規定,結構體中如果一個字段的聲明中只有字段的類型名而沒有字段的名稱,那麼它就是一個嵌入字段,也可以被稱爲匿名字段。我們可以通過此類型變量的名稱後跟“.”,再後跟嵌入字段類型的方式引用到該字段。也就是說,嵌入字段的類型既是類型也是名稱。匿名的結構體字段將會自動獲得以結構體類型的名字命名的字段名稱。

看下面這個例子,直接把Animal這個結構體的字段嵌入到Cat中:

內嵌的結構體不僅會塞入所有字段,所有該結構體實現的方法也會一起塞入,見下面這個例子:

這裏的c.show() 被轉換成二進制代碼後和 c.Point.show() 是等價的,c.x 和 c.Point.x 也是等價的。

 

我們下期一起聊聊Go語言中的函數和接口。

 

喜歡的可以關注我的WeiXin訂閱號,隨時隨地學習:

 

參考資料

  1. Golang入門:切片(slice)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章