Go語言中複合的數據類型

數組

數組是同一種數據類型元素的集合。在Go語言中,數組從聲明時就確定,可以對其成員進行修改,但是不可以修改數組的大小。

數組的定義

數組的基本定義語法如下:

var 數組名 [數組大小]類型

比如定義一個長度爲3,類型是int的數組:

var a [3]int

注意:長度必須是常量,它是數組類型的一部分,一旦定義,長度不能改變。

數組的初始化

(1)、初始化數組時可以使用初始化列表來設置數組元素的值。

func main() {
    var testArray [3]int                        //數組會初始化爲int類型的零值
    var numArray = [3]int{1, 2}                 //使用指定的初始值完成初始化
    var cityArray = [3]string{"北京", "上海", "深圳"} //使用指定的初始值完成初始化
    fmt.Println(testArray)                      //[0 0 0]
    fmt.Println(numArray)                       //[1 2 0]
    fmt.Println(cityArray)                      //[北京 上海 深圳]
}

(2)、按照上面的方法每次都要確保提供的初始值和數組長度一致,一般情況下我們可以讓編譯器根據初始值的個數自行推斷數組的長度,例如:

func main() {
    var testArray [3]int
    var numArray = [...]int{1, 2}
    var cityArray = [...]string{"北京", "上海", "深圳"}
    fmt.Println(testArray)                          //[0 0 0]
    fmt.Println(numArray)                           //[1 2]
    fmt.Printf("type of numArray:%T\n", numArray)   //type of numArray:[2]int
    fmt.Println(cityArray)                          //[北京 上海 深圳]
    fmt.Printf("type of cityArray:%T\n", cityArray) //type of cityArray:[3]string
}

(3)、我們還可以使用指定索引值的方式來初始化數組,例如:

func main() {
    a := [...]int{1: 1, 3: 5}
    fmt.Println(a)                  // [0 1 0 5]
    fmt.Printf("type of a:%T\n", a) //type of a:[4]int
}

數組的遍歷

數組的遍歷有兩種方法:

  • for range
  • 通過索引遍歷

如下:

package main

import "fmt"

func main() {
    var a = [...]string{"北京", "上海", "廣州"}
    // 通過索引遍歷數組
    for i := 0; i < len(a); i++ {
        fmt.Println(a[i])
    }
    fmt.Println("================")

    // 用for range遍歷數組
    for _, v := range a {
        fmt.Println(v)
    }
}

多維數組

Go語言支持多維數組,這裏以二維數組爲例。

二維數組的定義

二維數組的基本定義如下:

var 數組名 [數組大小][數組大小]類型

如下表示外層數組有三個元素,裏層數組有兩個元素的二維數組:

var a [3][2]int

二維數組的初始化

package main

import "fmt"

func main() {
    var cities = [3][2]string{
        {"北京", "上海"},
        {"廣州", "重慶"},
        {"四川", "貴州"},
    }
    fmt.Println(cities)
}

如果要用自動推導數組長度,只有第一層可以使用,如下:

//支持的寫法
a := [...][2]string{
    {"北京", "上海"},
    {"廣州", "深圳"},
    {"成都", "重慶"},
}
//不支持多維數組的內層使用...
b := [3][...]string{
    {"北京", "上海"},
    {"廣州", "深圳"},
    {"成都", "重慶"},
}

注意:數組是值類型的,其賦值和傳參會複製整個數組,因此改變副本的值,不會改變本身的值。如下:

package main

import "fmt"

func main() {
    var cities = [3][2]string{
        {"北京", "上海"},
        {"廣州", "重慶"},
        {"四川", "貴州"},
    }
    // fmt.Println(cities)
    c2 := cities
    c2[2][0] = "黑龍江"
    fmt.Println(cities)
    fmt.Println(c2)
}

其輸出結果爲:

[[北京 上海] [廣州 重慶] [四川 貴州]]
[[北京 上海] [廣州 重慶] [黑龍江 貴州]]

3.3、切片

數組的長度是不可變的,一旦我們定義了某個數組,其長度就固定了。而切片是對數組的一層封裝,它是擁有相同類型元素的可變長度序列。它非常靈活,支持自動擴容。

切片是一個引用類型,其底層依然是某個數組,如果底層數據改變,切片也會相應的改變。切片的內容包括地址 長度 容量 。它一般用來快速的操作一塊數據集合。

自定義切片

切片的基本語法如下:

var 變量名 []元素類型

注意:在定義切片的時候,不指定其長度。

例如:

package main

import "fmt"

func main() {
    // 定義切片
    var s1 []int
    var s2 []string
    var s3 []bool
    fmt.Println(s1, s2, s3)
}

由於切片是引用類型,它不支持直接比較,只支持和nil 比較,如下:

package main

import "fmt"

func main() {
    // 定義切片
    var s1 []int
    var s2 []string
    var s3 []bool
    fmt.Println(s1, s2, s3)
    fmt.Println(s1 == nil)
    fmt.Println(s2 == nil)
    fmt.Println(s3 == nil)
    // fmt.Println(s1 == s3) // 不能這麼比較
}

自定義切片的初始化

切片的初始化和數組一樣,因爲其底層本身就是數組。比如:

package main

import "fmt"

func main() {
    // 定義切片並初始化
    var s4 = []int{1, 2, 3, 4}
    var s5 = []string{"北京", "上海"}
    var s6 = []bool{true, false}
    fmt.Println(s4, s5, s6)
}

基於數組定義切片

我們知道切片底層是存儲,所以切片也可以基於數組進行定義。如下:

package main

import "fmt"

func main() {
    // 基於數組定義切片
    // 先定義一個數組
    var a1 = [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 然後對數組進行切割
    var a2 = a1[2:6]
    var a3 = a1[1:]
    var a4 = a1[:4]
    var a5 = a1[:]
    fmt.Println(a1, a2)
}

基於切片定義切片

切片還可以基於切片定義切片。如下:

package main

import "fmt"

func main() {
    // 基於切片定義切片
    // 定義一個切片
    var s = []int{1,2,3,4,5,6,7,8,9}
    // 對切片進行切割
    var s1 = s[2:4]
    fmt.Println(s1)
}

注意:對切片進行再切片的時候,其索引不能超過原數組的長度。

切片的長度和容量

切片擁有自己的長度和容量,我們可以通過使用內置的len()函數求長度,使用內置的cap()函數求切片的容量。

package main

import "fmt"

func main() {
    // 基於切片定義切片
    // 定義一個切片
    var s = []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 對切片進行切割
    var s1 = s[2:4]
    fmt.Println(s1)
    fmt.Printf("len(s1):%d  cap(s1):%d", len(s1), cap(s1))
}

輸出如下:

[3 4]
len(s1):2  cap(s1):7

說明:

  • len函數求出來的長度就是切片的現有長度
  • cap函數求出來的容量是從切片起始位置到數組末尾的長度

用make函數構造切片

上面介紹的都是通過數組來定義切片,不論是通過自定義還是從數組切片來的,其長度和容量都在定義的時候固定了。那麼,如果我們需要動態創建一個切片,就需要使用make 函數,格式如下:

make([]T, size, cap)

說明:

  • T:表示元素的類型
  • size:表示元素的數量
  • cap:表示切片的容量

比如,我們創建一個元素個數是5,容量是10的切片。

package main

import "fmt"

func main() {
    s := make([]int, 5, 10)
    fmt.Println(s)
    fmt.Printf("len(s):%d cap(s):%d\n", len(s), cap(s))
}

輸出如下:

[0 0 0 0 0]
len(s):5 cap(s):10

如果我們不指定容量,其默認和前面的元素個數一致,如下:

package main

import "fmt"

func main() {
    s := make([]int, 5)
    fmt.Println(s)
    fmt.Printf("len(s):%d cap(s):%d", len(s), cap(s))
}

輸出如下:

[0 0 0 0 0]
len(s):5 cap(s):5

切片的本質

切片的本質就是對底層數組的封裝,它包含了三個信息:底層數組的指針、切片的長度(len)和切片的容量(cap)。

舉個例子,現在有一個數組a := [8]int{0, 1, 2, 3, 4, 5, 6, 7},切片s1 := a[:5],相應示意圖如下。

88naCT.png

切片s2 := a[3:6],相應示意圖如下:

88nd8U.png

切片之間是不能比較的,我們不能使用==操作符來判斷兩個切片是否含有全部相等元素。 切片唯一合法的比較操作是和nil比較。 一個nil值的切片並沒有底層數組,一個nil值的切片的長度和容量都是0。但是我們不能說一個長度和容量都是0的切片一定是nil,例如下面的示例:

var s1 []int         //len(s1)=0;cap(s1)=0;s1==nil
s2 := []int{}        //len(s2)=0;cap(s2)=0;s2!=nil
s3 := make([]int, 0) //len(s3)=0;cap(s3)=0;s3!=nil

所以要判斷一個切片是否是空的,要是用len(s) == 0來判斷,不應該使用s == nil來判斷。

注意以下三點:

  • 切片不保存具體的值
  • 切片對應一個底層數組
  • 底層數組都是佔用一塊連續的內存空間

切片的賦值拷貝

我們知道切片的本質是對底層數組的封裝,是一個引用類型,所以如果兩個切片共用底層數組,那麼對一個切片進行修改會影響另一個切片的內容,如下:

package main

import "fmt"

func main() {
    s := make([]int, 5)
    fmt.Println(s)
    fmt.Printf("len(s):%d cap(s):%d\n", len(s), cap(s))
    s1 := s
    s1[0] = 100
    fmt.Println(s)
    fmt.Println(s1)
}

其輸出結果如下:

[0 0 0 0 0]
len(s):5 cap(s):5
[100 0 0 0 0]
[100 0 0 0 0]

用copy函數複製切片

上面介紹的賦值拷貝,兩個切片是共用同一個底層數組,對任意一個進行修改就會影響另一個切片。Go語言中有另一個函數copy(),它是會開闢另外一個內存空間用來保存複製來的切片,通過這個函數複製的切片與原切片沒有任何關係。

其基本語法如下:

copy(destSlice, srcSlice)

其中:

  • destSlice:是目標切片
  • srcSlice:是源切片

例如:

package main

import "fmt"

func main() {
    // 定義一個切片並初始化
    s1 := []int{1, 2, 3, 4}
    // 再定義一個切片
    s2 := make([]int, 4, 4)
    // 用copy()函數進行復制
    copy(s2, s1)
    // 輸出:源切片s1:[1 2 3 4]  目標切片s2:[1 2 3 4]
    fmt.Printf("源切片s1:%v  目標切片s2:%v\n", s1, s2)
    // 對源切片進行修改,觀察目標切片有無變化
    s1[0] = 100
    // 輸出:源切片s1:[100 2 3 4]  目標切片s2:[1 2 3 4]
    fmt.Printf("源切片s1:%v  目標切片s2:%v\n", s1, s2)
    // 對目標切片進行修改,觀察源切片有無變化
    s2[1] = 300
    // 輸出:源切片s1:[100 2 3 4]  目標切片s2:[1 300 3 4]
    fmt.Printf("源切片s1:%v  目標切片s2:%v\n", s1, s2)
}

從上面可以看出,用copy 複製的切片,相互之前並不影響。

切片的遍歷

切片的遍歷和數組一樣,如下:

package main

import "fmt"

func main() {
    s := []int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 第一種:遍歷索引
    for i := 0; i < len(s); i++ {
        fmt.Println(s[i])
    }
    // 第二種:用for range遍歷
    for i, v := range s {
        fmt.Println(i, v)
    }
}

給切片添加元素

上面介紹了切片的長度不是固定,那麼我們就可以往切片裏添加新的內容。在Go語言中,給切片添加內容是用append()方法。

注意:

  1. 如果切片的容量夠用,則直接向切片中添加內容;
  2. 如果切片的容量不夠,則會先一定的策略進行擴容,然後再向裏面添加內容;

比如:

package main

import "fmt"

func main() {
    // 定義一個數組
    a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 然後從數組得到一個切片
    s := a[:4]
    // 然後打印切片的長度和容量,這時候的長度是小於容量的。
    // 輸出:[1 2 3 4] len(s):4 cap(s):9
    fmt.Printf("%v len(s):%d cap(s):%d\n", s, len(s), cap(s))
    // 我們對切片s進行添加元素操作
    s = append(s, 100)
    // 輸出:[1 2 3 4 100] len(s):5 cap(s):9
    fmt.Printf("%v len(s):%d cap(s):%d\n", s, len(s), cap(s))

    // 我們重新從數組得到一個切片,這次用全量的
    s2 := a[:]
    // 輸出:[1 2 3 4 100 6 7 8 9] len(s2):9 cap(s2):9
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
    // 我們對其進行添加元素
    s2 = append(s2, 200)
    // 輸出:[1 2 3 4 100 6 7 8 9 200] len(s2):10 cap(s2):18
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))

}

上面的輸出正好驗證了我們所說,如果切片的容量充足,添加元素則直接再後面追加;如果切片容量不足,添加元素則先對容量進行擴充,再在後面添加元素。

當然,append() 函數還支持一次性擴容多個元素,如下:

package main

import "fmt"

func main() {
    // 定義一個數組
    a := [...]int{1, 2, 3, 4, 5, 6, 7, 8, 9}
    // 我們重新從數組得到一個切片,這次用全量的
    s2 := a[:]
    // 輸出:[1 2 3 4 100 6 7 8 9] len(s2):9 cap(s2):9
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
    // 我們對其進行添加元素
    s2 = append(s2, 200)
    // 輸出:[1 2 3 4 100 6 7 8 9 200] len(s2):10 cap(s2):18
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
    s2 = append(s2, 300, 400, 500)
    fmt.Printf("%v len(s2):%d cap(s2):%d\n", s2, len(s2), cap(s2))
}

切片的擴容策略

通過上面對切片添加元素知道當切片的容量不足的時候會先給切片擴容。擴容也是有一定策略的,其策略如下:

  1. 如果申請的容量大於原來容量的2倍,最終的容量則是申請的容量大小;
  2. 如果舊切片的容量小於1024,則最終容量是舊容量的2倍;
  3. 如果舊切片的容量大於1024,則最終容量是從舊容量開始循環增加原來的1/4,直到最終容量大於等於新申請的容量;
  4. 如果最終容量計算值溢出,則最終容量是新申請容量;

注意:切片擴容還會根據切片中元素的類型不同而做不同的處理,比如intstring類型的處理方式就不一樣。

從切片中刪除元素

Go語言中並沒有刪除切片元素的專用方法,但是我們可以使用切片本身的特性來刪除元素。

比如要刪除切片a中所以爲index的元素,則用以下方法:

a = append(a[:index], a[index+1:]...)

例如:

package main

import "fmt"

func main() {
    // 定義一個切片
    s1 := []int{1, 2, 3, 4, 5}
    // 從切片中刪除值爲3的元素
    s1 = append(s1[:2], s1[3:]...)
    // 輸出:[1 2 4 5]
    fmt.Println(s1)
}

我們從切片中刪除某個元素對原數組有什麼影響呢?我們看下面這個例子:

package main

import "fmt"

func main() {
    // 定義一個數組
    a := [...]int{1, 2, 3, 4, 5, 6}
    s2 := a[:]
    s2 = append(s2[:2], s2[3:]...)
    fmt.Println(s2) // 輸出:[1 2 4 5 6]
    fmt.Println(a)  // 輸出:[1 2 4 5 6 6]
}

我們可以看到從切片中刪除某個元素,對原數據也有影響。如下圖:

88uEzF.png

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