日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問(wèn) 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

第03章 Go语言容器(container)

發(fā)布時(shí)間:2023/12/20 编程问答 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 第03章 Go语言容器(container) 小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

變量在一定程度上能滿(mǎn)足函數(shù)及代碼要求。如果編寫(xiě)一些復(fù)雜算法、結(jié)構(gòu)和邏輯,就需要更復(fù)雜的類(lèi)型來(lái)實(shí)現(xiàn)。這類(lèi)復(fù)雜類(lèi)型一般情況下具有各種形式的存儲(chǔ)和處理數(shù)據(jù)的功能,將它們稱(chēng)為“容器(container)”。

在很多語(yǔ)言里,容器是以標(biāo)準(zhǔn)庫(kù)的方式提供,你可以隨時(shí)查看這些標(biāo)準(zhǔn)庫(kù)的代碼,了解如何創(chuàng)建,刪除,維護(hù)內(nèi)存。

本章將以實(shí)用為目的,詳細(xì)介紹數(shù)組、切片、映射,以及列表的增加、刪除、修改和遍歷的使用方法。本章既可以作為教程,也可以作為字典,以方便開(kāi)發(fā)者日常的查詢(xún)和應(yīng)用。

其它語(yǔ)言中的容器

  • C語(yǔ)言沒(méi)有提供容器封裝,開(kāi)發(fā)者需要自己根據(jù)性能需求進(jìn)行封裝,或者使用第三方提供的容器。
  • C++ 語(yǔ)言的容器通過(guò)標(biāo)準(zhǔn)庫(kù)提供,如 vector 對(duì)應(yīng)數(shù)組,list 對(duì)應(yīng)雙鏈表,map 對(duì)應(yīng)映射等。
  • C# 語(yǔ)言通過(guò) .NET 框架提供,如 List 對(duì)應(yīng)數(shù)組,LinkedList 對(duì)應(yīng)雙鏈表,Dictionary 對(duì)應(yīng)映射。
  • Lua 語(yǔ)言的 table 實(shí)現(xiàn)了數(shù)組和映射的功能,Lua 語(yǔ)言默認(rèn)沒(méi)有雙鏈表支持。

3.1?Go語(yǔ)言數(shù)組詳解

數(shù)組是一個(gè)由固定長(zhǎng)度的特定類(lèi)型元素組成的序列,一個(gè)數(shù)組可以由零個(gè)或多個(gè)元素組成。因?yàn)閿?shù)組的長(zhǎng)度是固定的,所以在Go語(yǔ)言中很少直接使用數(shù)組。

和數(shù)組對(duì)應(yīng)的類(lèi)型是 Slice(切片),Slice 是可以增長(zhǎng)和收縮的動(dòng)態(tài)序列,功能也更靈活,但是想要理解 slice 工作原理的話(huà)需要先理解數(shù)組,所以本節(jié)主要為大家講解數(shù)組的使用,至于 Slice(切片)將在《Go語(yǔ)言切片》一節(jié)中為大家講解。

Go語(yǔ)言數(shù)組的聲明

數(shù)組的聲明語(yǔ)法如下:

var 數(shù)組變量名 [元素?cái)?shù)量]Type

語(yǔ)法說(shuō)明如下所示:

  • 數(shù)組變量名:數(shù)組聲明及使用時(shí)的變量名。
  • 元素?cái)?shù)量:數(shù)組的元素?cái)?shù)量,可以是一個(gè)表達(dá)式,但最終通過(guò)編譯期計(jì)算的結(jié)果必須是整型數(shù)值,元素?cái)?shù)量不能含有到運(yùn)行時(shí)才能確認(rèn)大小的數(shù)值。
  • Type:可以是任意基本類(lèi)型,包括數(shù)組本身,類(lèi)型為數(shù)組本身時(shí),可以實(shí)現(xiàn)多維數(shù)組。


數(shù)組的每個(gè)元素都可以通過(guò)索引下標(biāo)來(lái)訪問(wèn),索引下標(biāo)的范圍是從 0 開(kāi)始到數(shù)組長(zhǎng)度減 1 的位置,內(nèi)置函數(shù)?len() 可以返回?cái)?shù)組中元素的個(gè)數(shù)。

  • var a [3]int // 定義三個(gè)整數(shù)的數(shù)組
  • fmt.Println(a[0]) // 打印第一個(gè)元素
  • fmt.Println(a[len(a)-1]) // 打印最后一個(gè)元素
  • ?
  • // 打印索引和元素
  • for i, v := range a {
  • fmt.Printf("%d %d\n", i, v)
  • }
  • ?
  • // 僅打印元素
  • for _, v := range a {
  • fmt.Printf("%d\n", v)
  • }
  • 默認(rèn)情況下,數(shù)組的每個(gè)元素都會(huì)被初始化為元素類(lèi)型對(duì)應(yīng)的零值,對(duì)于數(shù)字類(lèi)型來(lái)說(shuō)就是 0,同時(shí)也可以使用數(shù)組字面值語(yǔ)法,用一組值來(lái)初始化數(shù)組:

  • var q [3]int = [3]int{1, 2, 3}
  • var r [3]int = [3]int{1, 2}
  • fmt.Println(r[2]) // "0"
  • 在數(shù)組的定義中,如果在數(shù)組長(zhǎng)度的位置出現(xiàn)“...”省略號(hào),則表示數(shù)組的長(zhǎng)度是根據(jù)初始化值的個(gè)數(shù)來(lái)計(jì)算,因此,上面數(shù)組?q 的定義可以簡(jiǎn)化為:

  • q := [...]int{1, 2, 3}
  • fmt.Printf("%T\n", q) // "[3]int"
  • 數(shù)組的長(zhǎng)度是數(shù)組類(lèi)型的一個(gè)組成部分,因此 [3]int 和 [4]int 是兩種不同的數(shù)組類(lèi)型,數(shù)組的長(zhǎng)度必須是常量表達(dá)式,因?yàn)閿?shù)組的長(zhǎng)度需要在編譯階段確定。

  • q := [3]int{1, 2, 3}
  • q = [4]int{1, 2, 3, 4} // 編譯錯(cuò)誤:無(wú)法將 [4]int 賦給 [3]int
  • 比較兩個(gè)數(shù)組是否相等

    如果兩個(gè)數(shù)組類(lèi)型相同(包括數(shù)組的長(zhǎng)度,數(shù)組中元素的類(lèi)型)的情況下,我們可以直接通過(guò)較運(yùn)算符(==和!=)來(lái)判斷兩個(gè)數(shù)組是否相等,只有當(dāng)兩個(gè)數(shù)組的所有元素都是相等的時(shí)候數(shù)組才是相等的,不能比較兩個(gè)類(lèi)型不同的數(shù)組,否則程序?qū)o(wú)法完成編譯。

  • a := [2]int{1, 2}
  • b := [...]int{1, 2}
  • c := [2]int{1, 3}
  • fmt.Println(a == b, a == c, b == c) // "true false false"
  • d := [3]int{1, 2}
  • fmt.Println(a == d) // 編譯錯(cuò)誤:無(wú)法比較 [2]int == [3]int
  • 遍歷數(shù)組——訪問(wèn)每一個(gè)數(shù)組元素

    遍歷數(shù)組也和遍歷切片類(lèi)似,代碼如下所示:

  • var team [3]string
  • team[0] = "hammer"
  • team[1] = "soldier"
  • team[2] = "mum"
  • ?
  • for k, v := range team {
  • fmt.Println(k, v)
  • }
  • 代碼輸出結(jié)果:

    0 hammer
    1 soldier
    2 mum

    代碼說(shuō)明如下:

    • 第 6 行,使用 for 循環(huán),遍歷 team 數(shù)組,遍歷出的鍵 k 為數(shù)組的索引,值 v 為數(shù)組的每個(gè)元素值。
    • 第 7 行,將每個(gè)鍵值打印出來(lái)。

    3.2?Go語(yǔ)言多維數(shù)組簡(jiǎn)述

    Go語(yǔ)言中允許使用多維數(shù)組,因?yàn)閿?shù)組屬于值類(lèi)型,所以多維數(shù)組的所有維度都會(huì)在創(chuàng)建時(shí)自動(dòng)初始化零值,多維數(shù)組尤其適合管理具有父子關(guān)系或者與坐標(biāo)系相關(guān)聯(lián)的數(shù)據(jù)。

    聲明多維數(shù)組的語(yǔ)法如下所示:

    var array_name [size1][size2]...[sizen] array_type

    其中,array_name 為數(shù)組的名字,array_type 為數(shù)組的類(lèi)型,size1、size2 等等為數(shù)組每一維度的長(zhǎng)度。

    結(jié)合上一節(jié)《Go語(yǔ)言數(shù)組》中所學(xué)到的知識(shí),下面以二維數(shù)組為例來(lái)簡(jiǎn)單講解一下多維數(shù)組的使用。

    二維數(shù)組是最簡(jiǎn)單的多維數(shù)組,二維數(shù)組本質(zhì)上是由多個(gè)一維數(shù)組組成的。

    【示例 1】聲明二維數(shù)組

  • // 聲明一個(gè)二維整型數(shù)組,兩個(gè)維度的長(zhǎng)度分別是 4 和 2
  • var array [4][2]int
  • // 使用數(shù)組字面量來(lái)聲明并初始化一個(gè)二維整型數(shù)組
  • array = [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
  • // 聲明并初始化數(shù)組中索引為 1 和 3 的元素
  • array = [4][2]int{1: {20, 21}, 3: {40, 41}}
  • // 聲明并初始化數(shù)組中指定的元素
  • array = [4][2]int{1: {0: 20}, 3: {1: 41}}
  • 下圖展示了上面示例中聲明的二維數(shù)組在每次聲明并初始化后包含的值。
    ?


    圖:二維數(shù)組及其外層數(shù)組和內(nèi)層數(shù)組的值


    為了訪問(wèn)單個(gè)元素,需要反復(fù)組合使用[ ]方括號(hào),如下所示。

    【示例 2】為二維數(shù)組的每個(gè)元素賦值

  • // 聲明一個(gè) 2×2 的二維整型數(shù)組
  • var array [2][2]int
  • // 設(shè)置每個(gè)元素的整型值
  • array[0][0] = 10
  • array[0][1] = 20
  • array[1][0] = 30
  • array[1][1] = 40
  • 只要類(lèi)型一致,就可以將多維數(shù)組互相賦值,如下所示,多維數(shù)組的類(lèi)型包括每一維度的長(zhǎng)度以及存儲(chǔ)在元素中數(shù)據(jù)的類(lèi)型。

    【示例 3】同樣類(lèi)型的多維數(shù)組賦值

  • // 聲明兩個(gè)二維整型數(shù)組
  • var array1 [2][2]int
  • var array2 [2][2]int
  • // 為array2的每個(gè)元素賦值
  • array2[0][0] = 10
  • array2[0][1] = 20
  • array2[1][0] = 30
  • array2[1][1] = 40
  • // 將 array2 的值復(fù)制給 array1
  • array1 = array2
  • 因?yàn)閿?shù)組中每個(gè)元素都是一個(gè)值,所以可以獨(dú)立復(fù)制某個(gè)維度,如下所示。

    【示例 4】使用索引為多維數(shù)組賦值

  • // 將 array1 的索引為 1 的維度復(fù)制到一個(gè)同類(lèi)型的新數(shù)組里
  • var array3 [2]int = array1[1]
  • // 將數(shù)組中指定的整型值復(fù)制到新的整型變量里
  • var value int = array1[1][0]
  • 3.3?Go語(yǔ)言切片詳解

    切片(slice)是對(duì)數(shù)組的一個(gè)連續(xù)片段的引用,所以切片是一個(gè)引用類(lèi)型(因此更類(lèi)似于 C/C++?中的數(shù)組類(lèi)型,或者?Python?中的 list 類(lèi)型),這個(gè)片段可以是整個(gè)數(shù)組,也可以是由起始和終止索引標(biāo)識(shí)的一些項(xiàng)的子集,需要注意的是,終止索引標(biāo)識(shí)的項(xiàng)不包括在切片內(nèi)。

    Go語(yǔ)言中切片的內(nèi)部結(jié)構(gòu)包含地址、大小和容量,切片一般用于快速地操作一塊數(shù)據(jù)集合,如果將數(shù)據(jù)集合比作切糕的話(huà),切片就是你要的“那一塊”,切的過(guò)程包含從哪里開(kāi)始(切片的起始位置)及切多大(切片的大小),容量可以理解為裝切片的口袋大小,如下圖所示。
    ?


    圖:切片結(jié)構(gòu)和內(nèi)存分配

    從數(shù)組或切片生成新的切片

    切片默認(rèn)指向一段連續(xù)內(nèi)存區(qū)域,可以是數(shù)組,也可以是切片本身。

    從連續(xù)內(nèi)存區(qū)域生成切片是常見(jiàn)的操作,格式如下:

    slice [開(kāi)始位置 : 結(jié)束位置]

    語(yǔ)法說(shuō)明如下:

    • slice:表示目標(biāo)切片對(duì)象;
    • 開(kāi)始位置:對(duì)應(yīng)目標(biāo)切片對(duì)象的索引;
    • 結(jié)束位置:對(duì)應(yīng)目標(biāo)切片的結(jié)束索引。


    從數(shù)組生成切片,代碼如下:

  • var a = [3]int{1, 2, 3}
  • fmt.Println(a, a[1:2])
  • 其中 a 是一個(gè)擁有 3 個(gè)整型元素的數(shù)組,被初始化為數(shù)值 1 到 3,使用 a[1:2] 可以生成一個(gè)新的切片,代碼運(yùn)行結(jié)果如下:

    [1 2 3] ?[2]

    其中 [2] 就是 a[1:2] 切片操作的結(jié)果。

    從數(shù)組或切片生成新的切片擁有如下特性:

    • 取出的元素?cái)?shù)量為:結(jié)束位置 - 開(kāi)始位置;
    • 取出元素不包含結(jié)束位置對(duì)應(yīng)的索引,切片最后一個(gè)元素使用 slice[len(slice)] 獲取;
    • 當(dāng)缺省開(kāi)始位置時(shí),表示從連續(xù)區(qū)域開(kāi)頭到結(jié)束位置;
    • 當(dāng)缺省結(jié)束位置時(shí),表示從開(kāi)始位置到整個(gè)連續(xù)區(qū)域末尾;
    • 兩者同時(shí)缺省時(shí),與切片本身等效;
    • 兩者同時(shí)為 0 時(shí),等效于空切片,一般用于切片復(fù)位。


    根據(jù)索引位置取切片 slice 元素值時(shí),取值范圍是(0~len(slice)-1),超界會(huì)報(bào)運(yùn)行時(shí)錯(cuò)誤,生成切片時(shí),結(jié)束位置可以填寫(xiě) len(slice) 但不會(huì)報(bào)錯(cuò)。

    下面通過(guò)實(shí)例來(lái)熟悉切片的特性。

    1) 從指定范圍中生成切片

    切片和數(shù)組密不可分,如果將數(shù)組理解為一棟辦公樓,那么切片就是把不同的連續(xù)樓層出租給使用者,出租的過(guò)程需要選擇開(kāi)始樓層和結(jié)束樓層,這個(gè)過(guò)程就會(huì)生成切片,示例代碼如下:

  • var highRiseBuilding [30]int
  • ?
  • for i := 0; i < 30; i++ {
  • highRiseBuilding[i] = i + 1
  • }
  • ?
  • // 區(qū)間
  • fmt.Println(highRiseBuilding[10:15])
  • ?
  • // 中間到尾部的所有元素
  • fmt.Println(highRiseBuilding[20:])
  • ?
  • // 開(kāi)頭到中間指定位置的所有元素
  • fmt.Println(highRiseBuilding[:2])
  • 代碼輸出如下:

    [11 12 13 14 15]
    [21 22 23 24 25 26 27 28 29 30]
    [1 2]

    代碼中構(gòu)建了一個(gè) 30 層的高層建筑,數(shù)組的元素值從 1 到 30,分別代表不同的獨(dú)立樓層,輸出的結(jié)果是不同的租售方案。

    代碼說(shuō)明如下:

    • 第 8 行,嘗試出租一個(gè)區(qū)間樓層。
    • 第 11 行,出租 20 層以上。
    • 第 14 行,出租 2 層以下,一般是商用鋪面。


    切片有點(diǎn)像C語(yǔ)言里的指針,指針可以做運(yùn)算,但代價(jià)是內(nèi)存操作越界,切片在指針的基礎(chǔ)上增加了大小,約束了切片對(duì)應(yīng)的內(nèi)存區(qū)域,切片使用中無(wú)法對(duì)切片內(nèi)部的地址和大小進(jìn)行手動(dòng)調(diào)整,因此切片比指針更安全、強(qiáng)大。

    2) 表示原有的切片

    生成切片的格式中,當(dāng)開(kāi)始和結(jié)束位置都被忽略時(shí),生成的切片將表示和原切片一致的切片,并且生成的切片與原切片在數(shù)據(jù)內(nèi)容上也是一致的,代碼如下:

  • a := []int{1, 2, 3}
  • fmt.Println(a[:])
  • a 是一個(gè)擁有 3 個(gè)元素的切片,將 a 切片使用 a[:] 進(jìn)行操作后,得到的切片與 a 切片一致,代碼輸出如下:

    [1 2 3]

    3) 重置切片,清空擁有的元素

    把切片的開(kāi)始和結(jié)束位置都設(shè)為 0 時(shí),生成的切片將變空,代碼如下:

  • a := []int{1, 2, 3}
  • fmt.Println(a[0:0])
  • 代碼輸出如下:

    []

    直接聲明新的切片

    除了可以從原有的數(shù)組或者切片中生成切片外,也可以聲明一個(gè)新的切片,每一種類(lèi)型都可以擁有其切片類(lèi)型,表示多個(gè)相同類(lèi)型元素的連續(xù)集合,因此切片類(lèi)型也可以被聲明,切片類(lèi)型聲明格式如下:

    var name []Type

    其中 name 表示切片的變量名,Type 表示切片對(duì)應(yīng)的元素類(lèi)型。

    下面代碼展示了切片聲明的使用過(guò)程:

  • // 聲明字符串切片
  • var strList []string
  • ?
  • // 聲明整型切片
  • var numList []int
  • ?
  • // 聲明一個(gè)空切片
  • var numListEmpty = []int{}
  • ?
  • // 輸出3個(gè)切片
  • fmt.Println(strList, numList, numListEmpty)
  • ?
  • // 輸出3個(gè)切片大小
  • fmt.Println(len(strList), len(numList), len(numListEmpty))
  • ?
  • // 切片判定空的結(jié)果
  • fmt.Println(strList == nil)
  • fmt.Println(numList == nil)
  • fmt.Println(numListEmpty == nil)
  • 代碼輸出結(jié)果:

    [] [] []
    0 0 0
    true
    true
    false

    代碼說(shuō)明如下:

    • 第 2 行,聲明一個(gè)字符串切片,切片中擁有多個(gè)字符串。
    • 第 5 行,聲明一個(gè)整型切片,切片中擁有多個(gè)整型數(shù)值。
    • 第 8 行,將 numListEmpty 聲明為一個(gè)整型切片,本來(lái)會(huì)在{}中填充切片的初始化元素,這里沒(méi)有填充,所以切片是空的,但是此時(shí)的 numListEmpty 已經(jīng)被分配了內(nèi)存,只是還沒(méi)有元素。
    • 第 11 行,切片均沒(méi)有任何元素,3 個(gè)切片輸出元素內(nèi)容均為空。
    • 第 14 行,沒(méi)有對(duì)切片進(jìn)行任何操作,strList 和 numList 沒(méi)有指向任何數(shù)組或者其他切片。
    • 第 17 行和第 18 行,聲明但未使用的切片的默認(rèn)值是 nil,strList 和 numList 也是 nil,所以和 nil 比較的結(jié)果是 true。
    • 第 19 行,numListEmpty 已經(jīng)被分配到了內(nèi)存,但沒(méi)有元素,因此和 nil 比較時(shí)是 false。


    切片是動(dòng)態(tài)結(jié)構(gòu),只能與 nil 判定相等,不能互相判定相等。聲明新的切片后,可以使用?append()?函數(shù)向切片中添加元素。

    使用 make() 函數(shù)構(gòu)造切片

    如果需要?jiǎng)討B(tài)地創(chuàng)建一個(gè)切片,可以使用 make() 內(nèi)建函數(shù),格式如下:

    make( []Type, size, cap )

    其中 Type 是指切片的元素類(lèi)型,size 指的是為這個(gè)類(lèi)型分配多少個(gè)元素,cap 為預(yù)分配的元素?cái)?shù)量,這個(gè)值設(shè)定后不影響 size,只是能提前分配空間,降低多次分配空間造成的性能問(wèn)題。

    示例如下:

  • a := make([]int, 2)
  • b := make([]int, 2, 10)
  • ?
  • fmt.Println(a, b)
  • fmt.Println(len(a), len(b))
  • 代碼輸出如下:

    [0 0] [0 0]
    2 2

    其中 a 和 b 均是預(yù)分配 2 個(gè)元素的切片,只是 b 的內(nèi)部存儲(chǔ)空間已經(jīng)分配了 10 個(gè),但實(shí)際使用了 2 個(gè)元素。

    容量不會(huì)影響當(dāng)前的元素個(gè)數(shù),因此 a 和 b 取 len 都是 2。

    溫馨提示

    使用 make() 函數(shù)生成的切片一定發(fā)生了內(nèi)存分配操作,但給定開(kāi)始與結(jié)束位置(包括切片復(fù)位)的切片只是將新的切片結(jié)構(gòu)指向已經(jīng)分配好的內(nèi)存區(qū)域,設(shè)定開(kāi)始與結(jié)束位置,不會(huì)發(fā)生內(nèi)存分配操作。

    3.4?Go語(yǔ)言append()為切片添加元素

    Go語(yǔ)言的內(nèi)建函數(shù) append() 可以為切片動(dòng)態(tài)添加元素,代碼如下所示:

  • var a []int
  • a = append(a, 1) // 追加1個(gè)元素
  • a = append(a, 1, 2, 3) // 追加多個(gè)元素, 手寫(xiě)解包方式
  • a = append(a, []int{1,2,3}...) // 追加一個(gè)切片, 切片需要解包
  • 不過(guò)需要注意的是,在使用 append() 函數(shù)為切片動(dòng)態(tài)添加元素時(shí),如果空間不足以容納足夠多的元素,切片就會(huì)進(jìn)行“擴(kuò)容”,此時(shí)新切片的長(zhǎng)度會(huì)發(fā)生改變。

    切片在擴(kuò)容時(shí),容量的擴(kuò)展規(guī)律是按容量的 2 倍數(shù)進(jìn)行擴(kuò)充,例如 1、2、4、8、16……,代碼如下:

  • var numbers []int
  • ?
  • for i := 0; i < 10; i++ {
  • numbers = append(numbers, i)
  • fmt.Printf("len: %d cap: %d pointer: %p\n", len(numbers), cap(numbers), numbers)
  • }
  • 代碼輸出如下:

    len: 1? cap: 1 pointer: 0xc0420080e8
    len: 2? cap: 2 pointer: 0xc042008150
    len: 3? cap: 4 pointer: 0xc04200e320
    len: 4? cap: 4 pointer: 0xc04200e320
    len: 5? cap: 8 pointer: 0xc04200c200
    len: 6? cap: 8 pointer: 0xc04200c200
    len: 7? cap: 8 pointer: 0xc04200c200
    len: 8? cap: 8 pointer: 0xc04200c200
    len: 9? cap: 16 pointer: 0xc042074000
    len: 10? cap: 16 pointer: 0xc042074000

    代碼說(shuō)明如下:

    • 第 1 行,聲明一個(gè)整型切片。
    • 第 4 行,循環(huán)向 numbers 切片中添加 10 個(gè)數(shù)。
    • 第 5 行,打印輸出切片的長(zhǎng)度、容量和指針變化,使用函數(shù)?len() 查看切片擁有的元素個(gè)數(shù),使用函數(shù)?cap() 查看切片的容量情況。


    通過(guò)查看代碼輸出,可以發(fā)現(xiàn)一個(gè)有意思的規(guī)律:切片長(zhǎng)度 len 并不等于切片的容量 cap。

    往一個(gè)切片中不斷添加元素的過(guò)程,類(lèi)似于公司搬家,公司發(fā)展初期,資金緊張,人員很少,所以只需要很小的房間即可容納所有的員工,隨著業(yè)務(wù)的拓展和收入的增加就需要擴(kuò)充工位,但是辦公地的大小是固定的,無(wú)法改變,因此公司只能選擇搬家,每次搬家就需要將所有的人員轉(zhuǎn)移到新的辦公點(diǎn)。

    • 員工和工位就是切片中的元素。
    • 辦公地就是分配好的內(nèi)存。
    • 搬家就是重新分配內(nèi)存。
    • 無(wú)論搬多少次家,公司名稱(chēng)始終不會(huì)變,代表外部使用切片的變量名不會(huì)修改。
    • 由于搬家后地址發(fā)生變化,因此內(nèi)存“地址”也會(huì)有修改。


    除了在切片的尾部追加,我們還可以在切片的開(kāi)頭添加元素:

  • var a = []int{1,2,3}
  • a = append([]int{0}, a...) // 在開(kāi)頭添加1個(gè)元素
  • a = append([]int{-3,-2,-1}, a...) // 在開(kāi)頭添加1個(gè)切片
  • 在切片開(kāi)頭添加元素一般都會(huì)導(dǎo)致內(nèi)存的重新分配,而且會(huì)導(dǎo)致已有元素全部被復(fù)制 1 次,因此,從切片的開(kāi)頭添加元素的性能要比從尾部追加元素的性能差很多。

    因?yàn)?append 函數(shù)返回新切片的特性,所以切片也支持鏈?zhǔn)讲僮?#xff0c;我們可以將多個(gè) append 操作組合起來(lái),實(shí)現(xiàn)在切片中間插入元素:

  • var a []int
  • a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i個(gè)位置插入x
  • a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i個(gè)位置插入切片
  • 每個(gè)添加操作中的第二個(gè) append 調(diào)用都會(huì)創(chuàng)建一個(gè)臨時(shí)切片,并將 a[i:] 的內(nèi)容復(fù)制到新創(chuàng)建的切片中,然后將臨時(shí)創(chuàng)建的切片再追加到 a[:i] 中。

    3.5?Go語(yǔ)言copy():切片復(fù)制(切片拷貝)

    Go語(yǔ)言的內(nèi)置函數(shù) copy() 可以將一個(gè)數(shù)組切片復(fù)制到另一個(gè)數(shù)組切片中,如果加入的兩個(gè)數(shù)組切片不一樣大,就會(huì)按照其中較小的那個(gè)數(shù)組切片的元素個(gè)數(shù)進(jìn)行復(fù)制。

    copy() 函數(shù)的使用格式如下:

    copy( destSlice, srcSlice []T) int

    其中 srcSlice 為數(shù)據(jù)來(lái)源切片,destSlice 為復(fù)制的目標(biāo)(也就是將 srcSlice 復(fù)制到 destSlice),目標(biāo)切片必須分配過(guò)空間且足夠承載復(fù)制的元素個(gè)數(shù),并且來(lái)源和目標(biāo)的類(lèi)型必須一致,copy() 函數(shù)的返回值表示實(shí)際發(fā)生復(fù)制的元素個(gè)數(shù)。

    下面的代碼展示了使用 copy() 函數(shù)將一個(gè)切片復(fù)制到另一個(gè)切片的過(guò)程:

  • slice1 := []int{1, 2, 3, 4, 5}
  • slice2 := []int{5, 4, 3}
  • copy(slice2, slice1) // 只會(huì)復(fù)制slice1的前3個(gè)元素到slice2中
  • copy(slice1, slice2) // 只會(huì)復(fù)制slice2的3個(gè)元素到slice1的前3個(gè)位置
  • 雖然通過(guò)循環(huán)復(fù)制切片元素更直接,不過(guò)內(nèi)置的 copy() 函數(shù)使用起來(lái)更加方便,copy() 函數(shù)的第一個(gè)參數(shù)是要復(fù)制的目標(biāo) slice,第二個(gè)參數(shù)是源 slice,兩個(gè) slice 可以共享同一個(gè)底層數(shù)組,甚至有重疊也沒(méi)有問(wèn)題。

    【示例】通過(guò)代碼演示對(duì)切片的引用和復(fù)制操作后對(duì)切片元素的影響。

  • package main
  • ?
  • import "fmt"
  • ?
  • func main() {
  • ?
  • // 設(shè)置元素?cái)?shù)量為1000
  • const elementCount = 1000
  • ?
  • // 預(yù)分配足夠多的元素切片
  • srcData := make([]int, elementCount)
  • ?
  • // 將切片賦值
  • for i := 0; i < elementCount; i++ {
  • srcData[i] = i
  • }
  • ?
  • // 引用切片數(shù)據(jù)
  • refData := srcData
  • ?
  • // 預(yù)分配足夠多的元素切片
  • copyData := make([]int, elementCount)
  • // 將數(shù)據(jù)復(fù)制到新的切片空間中
  • copy(copyData, srcData)
  • ?
  • // 修改原始數(shù)據(jù)的第一個(gè)元素
  • srcData[0] = 999
  • ?
  • // 打印引用切片的第一個(gè)元素
  • fmt.Println(refData[0])
  • ?
  • // 打印復(fù)制切片的第一個(gè)和最后一個(gè)元素
  • fmt.Println(copyData[0], copyData[elementCount-1])
  • ?
  • // 復(fù)制原始數(shù)據(jù)從4到6(不包含)
  • copy(copyData, srcData[4:6])
  • ?
  • for i := 0; i < 5; i++ {
  • fmt.Printf("%d ", copyData[i])
  • }
  • }
  • 代碼說(shuō)明如下:

    • 第 8 行,定義元素總量為 1000。
    • 第 11 行,預(yù)分配擁有 1000 個(gè)元素的整型切片,這個(gè)切片將作為原始數(shù)據(jù)。
    • 第 14~16 行,將 srcData 填充 0~999 的整型值。
    • 第 19 行,將 refData 引用 srcData,切片不會(huì)因?yàn)榈忍?hào)操作進(jìn)行元素的復(fù)制。
    • 第 22 行,預(yù)分配與 srcData 等大(大小相等)、同類(lèi)型的切片 copyData。
    • 第 24 行,使用 copy() 函數(shù)將原始數(shù)據(jù)復(fù)制到 copyData 切片空間中。
    • 第 27 行,修改原始數(shù)據(jù)的第一個(gè)元素為 999。
    • 第 30 行,引用數(shù)據(jù)的第一個(gè)元素將會(huì)發(fā)生變化。
    • 第 33 行,打印復(fù)制數(shù)據(jù)的首位數(shù)據(jù),由于數(shù)據(jù)是復(fù)制的,因此不會(huì)發(fā)生變化。
    • 第 36 行,將 srcData 的局部數(shù)據(jù)復(fù)制到 copyData 中。
    • 第 38~40 行,打印復(fù)制局部數(shù)據(jù)后的 copyData 元素。

    3.6?Go語(yǔ)言從切片中刪除元素

    Go語(yǔ)言并沒(méi)有對(duì)刪除切片元素提供專(zhuān)用的語(yǔ)法或者接口,需要使用切片本身的特性來(lái)刪除元素,根據(jù)要?jiǎng)h除元素的位置有三種情況,分別是從開(kāi)頭位置刪除、從中間位置刪除和從尾部刪除,其中刪除切片尾部的元素速度最快。

    從開(kāi)頭位置刪除

    刪除開(kāi)頭的元素可以直接移動(dòng)數(shù)據(jù)指針:

  • a = []int{1, 2, 3}
  • a = a[1:] // 刪除開(kāi)頭1個(gè)元素
  • a = a[N:] // 刪除開(kāi)頭N個(gè)元素
  • 也可以不移動(dòng)數(shù)據(jù)指針,但是將后面的數(shù)據(jù)向開(kāi)頭移動(dòng),可以用 append 原地完成(所謂原地完成是指在原有的切片數(shù)據(jù)對(duì)應(yīng)的內(nèi)存區(qū)間內(nèi)完成,不會(huì)導(dǎo)致內(nèi)存空間結(jié)構(gòu)的變化):

  • a = []int{1, 2, 3}
  • a = append(a[:0], a[1:]...) // 刪除開(kāi)頭1個(gè)元素
  • a = append(a[:0], a[N:]...) // 刪除開(kāi)頭N個(gè)元素
  • 還可以用 copy() 函數(shù)來(lái)刪除開(kāi)頭的元素:

  • a = []int{1, 2, 3}
  • a = a[:copy(a, a[1:])] // 刪除開(kāi)頭1個(gè)元素
  • a = a[:copy(a, a[N:])] // 刪除開(kāi)頭N個(gè)元素
  • 從中間位置刪除

    對(duì)于刪除中間的元素,需要對(duì)剩余的元素進(jìn)行一次整體挪動(dòng),同樣可以用 append 或 copy 原地完成:

  • a = []int{1, 2, 3, ...}
  • a = append(a[:i], a[i+1:]...) // 刪除中間1個(gè)元素
  • a = append(a[:i], a[i+N:]...) // 刪除中間N個(gè)元素
  • a = a[:i+copy(a[i:], a[i+1:])] // 刪除中間1個(gè)元素
  • a = a[:i+copy(a[i:], a[i+N:])] // 刪除中間N個(gè)元素
  • 從尾部刪除

  • a = []int{1, 2, 3}
  • a = a[:len(a)-1] // 刪除尾部1個(gè)元素
  • a = a[:len(a)-N] // 刪除尾部N個(gè)元素

  • 刪除開(kāi)頭的元素和刪除尾部的元素都可以認(rèn)為是刪除中間元素操作的特殊情況,下面來(lái)看一個(gè)示例。

    【示例】刪除切片指定位置的元素。

  • package main
  • ?
  • import "fmt"
  • ?
  • func main() {
  • ??? seq := []string{"a", "b", "c", "d", "e"}
  • ?
  • ??? // 指定刪除位置
  • ??? index := 2
  • ?
  • ??? // 查看刪除位置之前的元素和之后的元素
  • ??? fmt.Println(seq[:index], seq[index+1:])
  • ?
  • ??? // 將刪除點(diǎn)前后的元素連接起來(lái)
  • ??? seq = append(seq[:index], seq[index+1:]...)
  • ?
  • ??? fmt.Println(seq)
  • }
  • 代碼輸出結(jié)果:

    [a b] [d e]
    [a b d e]

    代碼說(shuō)明如下:

    • 第 1 行,聲明一個(gè)整型切片,保存含有從 a 到 e 的字符串。
    • 第 4 行,為了演示和講解方便,使用 index 變量保存需要?jiǎng)h除的元素位置。
    • 第 7 行,seq[:index] 表示的就是被刪除元素的前半部分,值為 [1 2],seq[index+1:] 表示的是被刪除元素的后半部分,值為?[4 5]。
    • 第 10 行,使用 append() 函數(shù)將兩個(gè)切片連接起來(lái)。
    • 第 12 行,輸出連接好的新切片,此時(shí),索引為 2 的元素已經(jīng)被刪除。


    代碼的刪除過(guò)程可以使用下圖來(lái)描述。


    圖:切片刪除元素的操作過(guò)程


    Go語(yǔ)言中刪除切片元素的本質(zhì)是,以被刪除元素為分界點(diǎn),將前后兩個(gè)部分的內(nèi)存重新連接起來(lái)。

    提示

    連續(xù)容器的元素刪除無(wú)論在任何語(yǔ)言中,都要將刪除點(diǎn)前后的元素移動(dòng)到新的位置,隨著元素的增加,這個(gè)過(guò)程將會(huì)變得極為耗時(shí),因此,當(dāng)業(yè)務(wù)需要大量、頻繁地從一個(gè)切片中刪除元素時(shí),如果對(duì)性能要求較高的話(huà),就需要考慮更換其他的容器了(如雙鏈表等能快速?gòu)膭h除點(diǎn)刪除元素)。

    3.7?Go語(yǔ)言range關(guān)鍵字:循環(huán)迭代切片

    通過(guò)前面的學(xué)習(xí)我們了解到切片其實(shí)就是多個(gè)相同類(lèi)型元素的連續(xù)集合,既然切片是一個(gè)集合,那么我們就可以迭代其中的元素,Go語(yǔ)言有個(gè)特殊的關(guān)鍵字 range,它可以配合關(guān)鍵字 for 來(lái)迭代切片里的每一個(gè)元素,如下所示:

  • // 創(chuàng)建一個(gè)整型切片,并賦值
  • slice := []int{10, 20, 30, 40}
  • // 迭代每一個(gè)元素,并顯示其值
  • for index, value := range slice {
  • fmt.Printf("Index: %d Value: %d\n", index, value)
  • }
  • 第 4 行中的 index 和 value 分別用來(lái)接收 range 關(guān)鍵字返回的切片中每個(gè)元素的索引和值,這里的 index 和 value 不是固定的,讀者也可以定義成其它的名字。

    關(guān)于 for 的詳細(xì)使用我們將在下一章《Go語(yǔ)言流程控制》中為大家詳細(xì)介紹。

    上面代碼的輸出結(jié)果為:

    Index: 0 Value: 10
    Index: 1 Value: 20
    Index: 2 Value: 30
    Index: 3 Value: 40

    當(dāng)?shù)衅瑫r(shí),關(guān)鍵字 range 會(huì)返回兩個(gè)值,第一個(gè)值是當(dāng)前迭代到的索引位置,第二個(gè)值是該位置對(duì)應(yīng)元素值的一份副本,如下圖所示。
    ?


    圖:使用 range 迭代切片會(huì)創(chuàng)建每個(gè)元素的副本


    需要強(qiáng)調(diào)的是,range 返回的是每個(gè)元素的副本,而不是直接返回對(duì)該元素的引用,如下所示。

    【示例 1】range 提供了每個(gè)元素的副本

  • // 創(chuàng)建一個(gè)整型切片,并賦值
  • slice := []int{10, 20, 30, 40}
  • // 迭代每個(gè)元素,并顯示值和地址
  • for index, value := range slice {
  • fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n", value, &value, &slice[index])
  • }
  • 輸出結(jié)果為:

    Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
    Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
    Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
    Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

    因?yàn)榈祷氐淖兞渴且粋€(gè)在迭代過(guò)程中根據(jù)切片依次賦值的新變量,所以 value 的地址總是相同的,要想獲取每個(gè)元素的地址,需要使用切片變量和索引值(例如上面代碼中的 &slice[index])。

    如果不需要索引值,也可以使用下劃線_來(lái)忽略這個(gè)值,代碼如下所示。

    【示例 2】使用空白標(biāo)識(shí)符(下劃線)來(lái)忽略索引值

  • // 創(chuàng)建一個(gè)整型切片,并賦值
  • slice := []int{10, 20, 30, 40}
  • // 迭代每個(gè)元素,并顯示其值
  • for _, value := range slice {
  • fmt.Printf("Value: %d\n", value)
  • }
  • 輸出結(jié)果為:

    Value: 10
    Value: 20
    Value: 30
    Value: 40

    關(guān)鍵字 range 總是會(huì)從切片頭部開(kāi)始迭代。如果想對(duì)迭代做更多的控制,則可以使用傳統(tǒng)的 for 循環(huán),代碼如下所示。

    【示例 3】使用傳統(tǒng)的 for 循環(huán)對(duì)切片進(jìn)行迭代

  • // 創(chuàng)建一個(gè)整型切片,并賦值
  • slice := []int{10, 20, 30, 40}
  • // 從第三個(gè)元素開(kāi)始迭代每個(gè)元素
  • for index := 2; index < len(slice); index++ {
  • fmt.Printf("Index: %d Value: %d\n", index, slice[index])
  • }
  • 輸出結(jié)果為:

    Index: 2 Value: 30
    Index: 3 Value: 40

    在前面幾節(jié)的學(xué)習(xí)中我們了解了兩個(gè)特殊的內(nèi)置函數(shù) len() 和 cap(),可以用于處理數(shù)組、切片和通道,對(duì)于切片,函數(shù) len() 可以返回切片的長(zhǎng)度,函數(shù) cap() 可以返回切片的容量,在上面的示例中,使用到了函數(shù) len() 來(lái)控制循環(huán)迭代的次數(shù)。

    當(dāng)然,range 關(guān)鍵字不僅僅可以用來(lái)遍歷切片,它還可以用來(lái)遍歷數(shù)組、字符串、map 或者通道等,這些我們將在后面的學(xué)習(xí)中詳細(xì)介紹。

    3.8?Go語(yǔ)言多維切片簡(jiǎn)述

    Go語(yǔ)言中同樣允許使用多維切片,聲明一個(gè)多維數(shù)組的語(yǔ)法格式如下:

    var sliceName [][]...[]sliceType

    其中,sliceName 為切片的名字,sliceType為切片的類(lèi)型,每個(gè)[ ]代表著一個(gè)維度,切片有幾個(gè)維度就需要幾個(gè)[ ]。

    下面以二維切片為例,聲明一個(gè)二維切片并賦值,代碼如下所示。

  • //聲明一個(gè)二維切片
  • var slice [][]int
  • //為二維切片賦值
  • slice = [][]int{{10}, {100, 200}}
  • 上面的代碼也可以簡(jiǎn)寫(xiě)為下面的樣子。

  • // 聲明一個(gè)二維整型切片并賦值
  • slice := [][]int{{10}, {100, 200}}
  • 上面的代碼中展示了一個(gè)包含兩個(gè)元素的外層切片,同時(shí)每個(gè)元素包又含一個(gè)內(nèi)層的整型切片,切片 slice 的值如下圖所示。
    ?


    圖:整型切片的切片的值


    通過(guò)上圖可以看到外層的切片包括兩個(gè)元素,每個(gè)元素都是一個(gè)切片,第一個(gè)元素中的切片使用單個(gè)整數(shù) 10 來(lái)初始化,第二個(gè)元素中的切片包括兩個(gè)整數(shù),即 100 和 200。

    這種組合可以讓用戶(hù)創(chuàng)建非常復(fù)雜且強(qiáng)大的數(shù)據(jù)結(jié)構(gòu),前面介紹過(guò)的關(guān)于內(nèi)置函數(shù)?append()?的規(guī)則也可以應(yīng)用到組合后的切片上,如下所示。

    【示例】組合切片的切片

  • // 聲明一個(gè)二維整型切片并賦值
  • slice := [][]int{{10}, {100, 200}}
  • // 為第一個(gè)切片追加值為 20 的元素
  • slice[0] = append(slice[0], 20)
  • Go語(yǔ)言里使用 append() 函數(shù)處理追加的方式很簡(jiǎn)明,先增長(zhǎng)切片,再將新的整型切片賦值給外層切片的第一個(gè)元素,當(dāng)上面代碼中的操作完成后,再將切片復(fù)制到外層切片的索引為 0 的元素,如下圖所示。
    ?


    圖:append 操作之后外層切片索引為 0 的元素的布局


    即便是這么簡(jiǎn)單的多維切片,操作時(shí)也會(huì)涉及眾多的布局和值,在函數(shù)間這樣傳遞數(shù)據(jù)結(jié)構(gòu)會(huì)很復(fù)雜,不過(guò)切片本身結(jié)構(gòu)很簡(jiǎn)單,可以用很小的成本在函數(shù)間傳遞。

    3.9?Go語(yǔ)言map(Go語(yǔ)言映射)

    Go語(yǔ)言中 map 是一種特殊的數(shù)據(jù)結(jié)構(gòu),一種元素對(duì)(pair)的無(wú)序集合,pair 對(duì)應(yīng)一個(gè) key(索引)和一個(gè) value(值),所以這個(gè)結(jié)構(gòu)也稱(chēng)為關(guān)聯(lián)數(shù)組或字典,這是一種能夠快速尋找值的理想結(jié)構(gòu),給定 key,就可以迅速找到對(duì)應(yīng)的 value。

    map 這種數(shù)據(jù)結(jié)構(gòu)在其他編程語(yǔ)言中也稱(chēng)為字典(Python)、hash 和 HashTable 等。

    map 概念

    map 是引用類(lèi)型,可以使用如下方式聲明:

    var mapname map[keytype]valuetype

    其中:

    • mapname 為 map 的變量名。
    • keytype 為鍵類(lèi)型。
    • valuetype 是鍵對(duì)應(yīng)的值類(lèi)型。

    提示:[keytype] 和 valuetype 之間允許有空格。

    在聲明的時(shí)候不需要知道 map 的長(zhǎng)度,因?yàn)?map 是可以動(dòng)態(tài)增長(zhǎng)的,未初始化的 map 的值是 nil,使用函數(shù) len() 可以獲取 map 中 pair 的數(shù)目。

    【示例】

  • package main
  • import "fmt"
  • ?
  • func main() {
  • var mapLit map[string]int
  • //var mapCreated map[string]float32
  • var mapAssigned map[string]int
  • mapLit = map[string]int{"one": 1, "two": 2}
  • mapCreated := make(map[string]float32)
  • mapAssigned = mapLit
  • mapCreated["key1"] = 4.5
  • mapCreated["key2"] = 3.14159
  • mapAssigned["two"] = 3
  • fmt.Printf("Map literal at \"one\" is: %d\n", mapLit["one"])
  • fmt.Printf("Map created at \"key2\" is: %f\n", mapCreated["key2"])
  • fmt.Printf("Map assigned at \"two\" is: %d\n", mapLit["two"])
  • fmt.Printf("Map literal at \"ten\" is: %d\n", mapLit["ten"])
  • }
  • 輸出結(jié)果:

    Map literal at "one" is: 1
    Map created at "key2" is: 3.14159
    Map assigned at "two" is: 3
    Map literal at "ten" is: 0

    示例中 mapLit 演示了使用{key1: value1, key2: value2}的格式來(lái)初始化 map ,就像數(shù)組和結(jié)構(gòu)體一樣。

    上面代碼中的 mapCreated 的創(chuàng)建方式mapCreated := make(map[string]float)等價(jià)于mapCreated := map[string]float{}?。

    mapAssigned 是 mapList 的引用,對(duì) mapAssigned 的修改也會(huì)影響到 mapLit 的值。

    注意:可以使用 make(),但不能使用 new() 來(lái)構(gòu)造 map,如果錯(cuò)誤的使用 new() 分配了一個(gè)引用對(duì)象,會(huì)獲得一個(gè)空引用的指針,相當(dāng)于聲明了一個(gè)未初始化的變量并且取了它的地址:

    mapCreated := new(map[string]float)

    接下來(lái)當(dāng)我們調(diào)用mapCreated["key1"] = 4.5的時(shí)候,編譯器會(huì)報(bào)錯(cuò):

    invalid operation: mapCreated["key1"] (index of type *map[string]float).

    map 容量

    和數(shù)組不同,map 可以根據(jù)新增的 key-value 動(dòng)態(tài)的伸縮,因此它不存在固定長(zhǎng)度或者最大限制,但是也可以選擇標(biāo)明 map 的初始容量 capacity,格式如下:

    make(map[keytype]valuetype, cap)

    例如:

    map2 := make(map[string]float, 100)

    當(dāng) map 增長(zhǎng)到容量上限的時(shí)候,如果再增加新的 key-value,map 的大小會(huì)自動(dòng)加 1,所以出于性能的考慮,對(duì)于大的 map 或者會(huì)快速擴(kuò)張的 map,即使只是大概知道容量,也最好先標(biāo)明。

    這里有一個(gè) map 的具體例子,即將音階和對(duì)應(yīng)的音頻映射起來(lái):

  • noteFrequency := map[string]float32 {
  • "C0": 16.35, "D0": 18.35, "E0": 20.60, "F0": 21.83,
  • "G0": 24.50, "A0": 27.50, "B0": 30.87, "A4": 440}
  • 用切片作為 map 的值

    既然一個(gè) key 只能對(duì)應(yīng)一個(gè) value,而 value 又是一個(gè)原始類(lèi)型,那么如果一個(gè) key 要對(duì)應(yīng)多個(gè)值怎么辦?例如,當(dāng)我們要處理 unix 機(jī)器上的所有進(jìn)程,以父進(jìn)程(pid 為整形)作為 key,所有的子進(jìn)程(以所有子進(jìn)程的 pid 組成的切片)作為 value。通過(guò)將 value 定義為 []int 類(lèi)型或者其他類(lèi)型的切片,就可以?xún)?yōu)雅的解決這個(gè)問(wèn)題,示例代碼如下所示:

  • mp1 := make(map[int][]int)
  • mp2 := make(map[int]*[]int)
  • 3.10?Go語(yǔ)言遍歷map(訪問(wèn)map中的每一個(gè)鍵值對(duì))

    map 的遍歷過(guò)程使用 for range 循環(huán)完成,代碼如下:

  • scene := make(map[string]int)
  • ?
  • scene["route"] = 66
  • scene["brazil"] = 4
  • scene["china"] = 960
  • ?
  • for k, v := range scene {
  • fmt.Println(k, v)
  • }
  • 遍歷對(duì)于Go語(yǔ)言的很多對(duì)象來(lái)說(shuō)都是差不多的,直接使用 for range 語(yǔ)法即可,遍歷時(shí),可以同時(shí)獲得鍵和值,如只遍歷值,可以使用下面的形式:

  • for _, v := range scene {
  • 將不需要的鍵使用_改為匿名變量形式。

    只遍歷鍵時(shí),使用下面的形式:

  • for k := range scene {
  • 無(wú)須將值改為匿名變量形式,忽略值即可。

    注意:遍歷輸出元素的順序與填充順序無(wú)關(guān),不能期望 map 在遍歷時(shí)返回某種期望順序的結(jié)果。

    如果需要特定順序的遍歷結(jié)果,正確的做法是先排序,代碼如下:

  • scene := make(map[string]int)
  • ?
  • // 準(zhǔn)備map數(shù)據(jù)
  • scene["route"] = 66
  • scene["brazil"] = 4
  • scene["china"] = 960
  • ?
  • // 聲明一個(gè)切片保存map數(shù)據(jù)
  • var sceneList []string
  • ?
  • // 將map數(shù)據(jù)遍歷復(fù)制到切片中
  • for k := range scene {
  • sceneList = append(sceneList, k)
  • }
  • ?
  • // 對(duì)切片進(jìn)行排序
  • sort.Strings(sceneList)
  • ?
  • // 輸出
  • fmt.Println(sceneList)
  • 代碼輸出如下:

    [brazil china route]

    代碼說(shuō)明如下:

    • 第 1 行,創(chuàng)建一個(gè) map 實(shí)例,鍵為字符串,值為整型。
    • 第 4~6 行,將 3 個(gè)鍵值對(duì)寫(xiě)入 map 中。
    • 第 9 行,聲明 sceneList 為字符串切片,以緩沖和排序 map 中的所有元素。
    • 第 12 行,將 map 中元素的鍵遍歷出來(lái),并放入切片中。
    • 第 17 行,對(duì) sceneList 字符串切片進(jìn)行排序,排序時(shí),sceneList 會(huì)被修改。
    • 第 20 行,輸出排好序的 map 的鍵。


    sort.Strings 的作用是對(duì)傳入的字符串切片進(jìn)行字符串字符的升序排列,排序接口的使用將在后面的章節(jié)中介紹。

    3.11?Go語(yǔ)言map元素的刪除和清空

    Go語(yǔ)言提供了一個(gè)內(nèi)置函數(shù) delete(),用于刪除容器內(nèi)的元素,下面我們簡(jiǎn)單介紹一下如何用 delete() 函數(shù)刪除 map 內(nèi)的元素。

    使用 delete() 函數(shù)從 map 中刪除鍵值對(duì)

    使用 delete() 內(nèi)建函數(shù)從 map 中刪除一組鍵值對(duì),delete() 函數(shù)的格式如下:

    delete(map, 鍵)

    其中 map 為要?jiǎng)h除的 map 實(shí)例,鍵為要?jiǎng)h除的 map 中鍵值對(duì)的鍵。

    從 map 中刪除一組鍵值對(duì)可以通過(guò)下面的代碼來(lái)完成:

  • scene := make(map[string]int)
  • ?
  • // 準(zhǔn)備map數(shù)據(jù)
  • scene["route"] = 66
  • scene["brazil"] = 4
  • scene["china"] = 960
  • ?
  • delete(scene, "brazil")
  • ?
  • for k, v := range scene {
  • fmt.Println(k, v)
  • }
  • 代碼輸出如下:

    route 66
    china 960

    這個(gè)例子中使用 delete() 函數(shù)將 brazil 從 scene 這個(gè) map 中刪除了。

    清空 map 中的所有元素

    有意思的是,Go語(yǔ)言中并沒(méi)有為 map 提供任何清空所有元素的函數(shù)、方法,清空 map 的唯一辦法就是重新 make 一個(gè)新的 map,不用擔(dān)心垃圾回收的效率,Go語(yǔ)言中的并行垃圾回收效率比寫(xiě)一個(gè)清空函數(shù)要高效的多。

    3.12?Go語(yǔ)言map的多鍵索引——多個(gè)數(shù)值條件可以同時(shí)查詢(xún)

    3.13?Go語(yǔ)言sync.Map(在并發(fā)環(huán)境中使用的map)

    Go語(yǔ)言中的 map 在并發(fā)情況下,只讀是線程安全的,同時(shí)讀寫(xiě)是線程不安全的。

    下面來(lái)看下并發(fā)情況下讀寫(xiě) map 時(shí)會(huì)出現(xiàn)的問(wèn)題,代碼如下:

  • // 創(chuàng)建一個(gè)int到int的映射
  • m := make(map[int]int)
  • ?
  • // 開(kāi)啟一段并發(fā)代碼
  • go func() {
  • ?
  • // 不停地對(duì)map進(jìn)行寫(xiě)入
  • for {
  • m[1] = 1
  • }
  • ?
  • }()
  • ?
  • // 開(kāi)啟一段并發(fā)代碼
  • go func() {
  • ?
  • // 不停地對(duì)map進(jìn)行讀取
  • for {
  • _ = m[1]
  • }
  • ?
  • }()
  • ?
  • // 無(wú)限循環(huán), 讓并發(fā)程序在后臺(tái)執(zhí)行
  • for {
  • ?
  • }
  • 運(yùn)行代碼會(huì)報(bào)錯(cuò),輸出如下:

    fatal error: concurrent map read and map write

    錯(cuò)誤信息顯示,并發(fā)的 map 讀和 map 寫(xiě),也就是說(shuō)使用了兩個(gè)并發(fā)函數(shù)不斷地對(duì) map 進(jìn)行讀和寫(xiě)而發(fā)生了競(jìng)態(tài)問(wèn)題,map 內(nèi)部會(huì)對(duì)這種并發(fā)操作進(jìn)行檢查并提前發(fā)現(xiàn)。

    需要并發(fā)讀寫(xiě)時(shí),一般的做法是加鎖,但這樣性能并不高,Go語(yǔ)言在 1.9 版本中提供了一種效率較高的并發(fā)安全的 sync.Map,sync.Map 和 map 不同,不是以語(yǔ)言原生形態(tài)提供,而是在 sync 包下的特殊結(jié)構(gòu)。

    sync.Map 有以下特性:

    • 無(wú)須初始化,直接聲明即可。
    • sync.Map 不能使用 map 的方式進(jìn)行取值和設(shè)置等操作,而是使用 sync.Map 的方法進(jìn)行調(diào)用,Store 表示存儲(chǔ),Load 表示獲取,Delete 表示刪除。
    • 使用 Range 配合一個(gè)回調(diào)函數(shù)進(jìn)行遍歷操作,通過(guò)回調(diào)函數(shù)返回內(nèi)部遍歷出來(lái)的值,Range 參數(shù)中回調(diào)函數(shù)的返回值在需要繼續(xù)迭代遍歷時(shí),返回 true,終止迭代遍歷時(shí),返回 false。


    并發(fā)安全的 sync.Map?演示代碼如下:

  • package main
  • ?
  • import (
  • "fmt"
  • "sync"
  • )
  • ?
  • func main() {
  • ?
  • var scene sync.Map
  • ?
  • // 將鍵值對(duì)保存到sync.Map
  • scene.Store("greece", 97)
  • scene.Store("london", 100)
  • scene.Store("egypt", 200)
  • ?
  • // 從sync.Map中根據(jù)鍵取值
  • fmt.Println(scene.Load("london"))
  • ?
  • // 根據(jù)鍵刪除對(duì)應(yīng)的鍵值對(duì)
  • scene.Delete("london")
  • ?
  • // 遍歷所有sync.Map中的鍵值對(duì)
  • scene.Range(func(k, v interface{}) bool {
  • ?
  • fmt.Println("iterate:", k, v)
  • return true
  • })
  • ?
  • }
  • 代碼輸出如下:

    100 true
    iterate: egypt 200
    iterate: greece 97

    代碼說(shuō)明如下:

    • 第 10 行,聲明 scene,類(lèi)型為 sync.Map,注意,sync.Map 不能使用 make 創(chuàng)建。
    • 第 13~15 行,將一系列鍵值對(duì)保存到 sync.Map 中,sync.Map 將鍵和值以 interface{} 類(lèi)型進(jìn)行保存。
    • 第 18 行,提供一個(gè) sync.Map 的鍵給 scene.Load() 方法后將查詢(xún)到鍵對(duì)應(yīng)的值返回。
    • 第 21 行,sync.Map 的 Delete 可以使用指定的鍵將對(duì)應(yīng)的鍵值對(duì)刪除。
    • 第 24 行,Range() 方法可以遍歷 sync.Map,遍歷需要提供一個(gè)匿名函數(shù),參數(shù)為 k、v,類(lèi)型為 interface{},每次 Range() 在遍歷一個(gè)元素時(shí),都會(huì)調(diào)用這個(gè)匿名函數(shù)把結(jié)果返回。


    sync.Map 沒(méi)有提供獲取 map 數(shù)量的方法,替代方法是在獲取 sync.Map 時(shí)遍歷自行計(jì)算數(shù)量,sync.Map 為了保證并發(fā)安全有一些性能損失,因此在非并發(fā)情況下,使用 map 相比使用 sync.Map 會(huì)有更好的性能。

    3.14?Go語(yǔ)言list(列表)

    列表是一種非連續(xù)的存儲(chǔ)容器,由多個(gè)節(jié)點(diǎn)組成,節(jié)點(diǎn)通過(guò)一些變量記錄彼此之間的關(guān)系,列表有多種實(shí)現(xiàn)方法,如單鏈表、雙鏈表等。

    列表的原理可以這樣理解:假設(shè) A、B、C 三個(gè)人都有電話(huà)號(hào)碼,如果 A 把號(hào)碼告訴給 B,B 把號(hào)碼告訴給 C,這個(gè)過(guò)程就建立了一個(gè)單鏈表結(jié)構(gòu),如下圖所示。
    ?


    圖:三人單向通知電話(huà)號(hào)碼形成單鏈表結(jié)構(gòu)


    如果在這個(gè)基礎(chǔ)上,再?gòu)?C 開(kāi)始將自己的號(hào)碼告訴給自己所知道號(hào)碼的主人,這樣就形成了雙鏈表結(jié)構(gòu),如下圖所示。
    ?


    圖:三人相互通知電話(huà)號(hào)碼形成雙鏈表結(jié)構(gòu)


    那么如果需要獲得所有人的號(hào)碼,只需要從 A 或者 C 開(kāi)始,要求他們將自己的號(hào)碼發(fā)出來(lái),然后再通知下一個(gè)人如此循環(huán),這樣就構(gòu)成了一個(gè)列表遍歷的過(guò)程。

    如果 B 換號(hào)碼了,他需要通知 A 和 C,將自己的號(hào)碼移除,這個(gè)過(guò)程就是列表元素的刪除操作,如下圖所示。
    ?


    圖:從雙鏈表中刪除一人的電話(huà)號(hào)碼


    在Go語(yǔ)言中,列表使用 container/list 包來(lái)實(shí)現(xiàn),內(nèi)部的實(shí)現(xiàn)原理是雙鏈表,列表能夠高效地進(jìn)行任意位置的元素插入和刪除操作。

    初始化列表

    list 的初始化有兩種方法:分別是使用 New() 函數(shù)和 var 關(guān)鍵字聲明,兩種方法的初始化效果都是一致的。

    1) 通過(guò) container/list 包的 New() 函數(shù)初始化 list

    變量名 := list.New()

    2) 通過(guò) var 關(guān)鍵字聲明初始化 list

    var 變量名 list.List

    列表與切片和 map 不同的是,列表并沒(méi)有具體元素類(lèi)型的限制,因此,列表的元素可以是任意類(lèi)型,這既帶來(lái)了便利,也引來(lái)一些問(wèn)題,例如給列表中放入了一個(gè) interface{} 類(lèi)型的值,取出值后,如果要將 interface{} 轉(zhuǎn)換為其他類(lèi)型將會(huì)發(fā)生宕機(jī)。

    在列表中插入元素

    雙鏈表支持從隊(duì)列前方或后方插入元素,分別對(duì)應(yīng)的方法是 PushFront 和 PushBack。

    提示

    這兩個(gè)方法都會(huì)返回一個(gè) *list.Element 結(jié)構(gòu),如果在以后的使用中需要?jiǎng)h除插入的元素,則只能通過(guò) *list.Element 配合 Remove() 方法進(jìn)行刪除,這種方法可以讓刪除更加效率化,同時(shí)也是雙鏈表特性之一。

    下面代碼展示如何給 list 添加元素:

  • l := list.New()
  • ?
  • l.PushBack("fist")
  • l.PushFront(67)
  • 代碼說(shuō)明如下:

    • 第 1 行,創(chuàng)建一個(gè)列表實(shí)例。
    • 第 3 行,將 fist 字符串插入到列表的尾部,此時(shí)列表是空的,插入后只有一個(gè)元素。
    • 第 4 行,將數(shù)值 67 放入列表,此時(shí),列表中已經(jīng)存在 fist 元素,67 這個(gè)元素將被放在 fist 的前面。


    列表插入元素的方法如下表所示。
    ?

    方 ?法功 ?能
    InsertAfter(v interface {}, mark * Element) * Element在 mark 點(diǎn)之后插入元素,mark 點(diǎn)由其他插入函數(shù)提供
    InsertBefore(v interface?{}, mark * Element) *Element在 mark 點(diǎn)之前插入元素,mark 點(diǎn)由其他插入函數(shù)提供
    PushBackList(other *List)添加 other 列表元素到尾部
    PushFrontList(other *List)添加 other 列表元素到頭部

    從列表中刪除元素

    列表插入函數(shù)的返回值會(huì)提供一個(gè) *list.Element 結(jié)構(gòu),這個(gè)結(jié)構(gòu)記錄著列表元素的值以及與其他節(jié)點(diǎn)之間的關(guān)系等信息,從列表中刪除元素時(shí),需要用到這個(gè)結(jié)構(gòu)進(jìn)行快速刪除。

    列表操作元素:

  • package main
  • ?
  • import "container/list"
  • ?
  • func main() {
  • l := list.New()
  • ?
  • // 尾部添加
  • l.PushBack("canon")
  • ?
  • // 頭部添加
  • l.PushFront(67)
  • ?
  • // 尾部添加后保存元素句柄
  • element := l.PushBack("fist")
  • ?
  • // 在fist之后添加high
  • l.InsertAfter("high", element)
  • ?
  • // 在fist之前添加noon
  • l.InsertBefore("noon", element)
  • ?
  • // 使用
  • l.Remove(element)
  • }
  • 代碼說(shuō)明如下:
    第 6 行,創(chuàng)建列表實(shí)例。
    第 9 行,將字符串?canon 插入到列表的尾部。
    第 12 行,將數(shù)值?67 添加到列表的頭部。
    第 15 行,將字符串?fist 插入到列表的尾部,并將這個(gè)元素的內(nèi)部結(jié)構(gòu)保存到 element 變量中。
    第 18 行,使用 element 變量,在 element 的位置后面插入 high 字符串。
    第 21 行,使用 element 變量,在 element 的位置前面插入 noon 字符串。
    第 24 行,移除 element 變量對(duì)應(yīng)的元素。

    下表中展示了每次操作后列表的實(shí)際元素情況。
    ?

    列表元素操作的過(guò)程操作內(nèi)容列表元素
    l.PushBack("canon")canon
    l.PushFront(67)67,?canon
    element := l.PushBack("fist")67, canon, fist
    l.InsertAfter("high", element)67, canon, fist, high
    l.InsertBefore("noon", element)67, canon, noon, fist, high
    l.Remove(element)67, canon, noon, high

    遍歷列表——訪問(wèn)列表的每一個(gè)元素

    遍歷雙鏈表需要配合 Front() 函數(shù)獲取頭元素,遍歷時(shí)只要元素不為空就可以繼續(xù)進(jìn)行,每一次遍歷都會(huì)調(diào)用元素的 Next() 函數(shù),代碼如下所示。

  • l := list.New()
  • ?
  • // 尾部添加
  • l.PushBack("canon")
  • ?
  • // 頭部添加
  • l.PushFront(67)
  • ?
  • for i := l.Front(); i != nil; i = i.Next() {
  • fmt.Println(i.Value)
  • }
  • 代碼輸出如下:

    67
    canon

    代碼說(shuō)明如下:

    • 第 1 行,創(chuàng)建一個(gè)列表實(shí)例。
    • 第 4 行,將 canon 放入列表尾部。
    • 第 7 行,在隊(duì)列頭部放入 67。
    • 第 9 行,使用 for 語(yǔ)句進(jìn)行遍歷,其中 i:=l.Front() 表示初始賦值,只會(huì)在一開(kāi)始執(zhí)行一次,每次循環(huán)會(huì)進(jìn)行一次 i != nil 語(yǔ)句判斷,如果返回 false,表示退出循環(huán),反之則會(huì)執(zhí)行 i = i.Next()。
    • 第 10 行,使用遍歷返回的 *list.Element 的 Value 成員取得放入列表時(shí)的原值。

    3.15?Go語(yǔ)言nil:空值/零值

    在Go語(yǔ)言中,布爾類(lèi)型的零值(初始值)為 false,數(shù)值類(lèi)型的零值為 0,字符串類(lèi)型的零值為空字符串"",而指針、切片、映射、通道、函數(shù)和接口的零值則是 nil。

    nil 是Go語(yǔ)言中一個(gè)預(yù)定義好的標(biāo)識(shí)符,有過(guò)其他編程語(yǔ)言開(kāi)發(fā)經(jīng)驗(yàn)的開(kāi)發(fā)者也許會(huì)把 nil 看作其他語(yǔ)言中的 null(NULL),其實(shí)這并不是完全正確的,因?yàn)镚o語(yǔ)言中的 nil 和其他語(yǔ)言中的 null 有很多不同點(diǎn)。

    下面通過(guò)幾個(gè)方面來(lái)介紹一下Go語(yǔ)言中 nil。

    nil 標(biāo)識(shí)符是不能比較的

  • package main
  • ?
  • import (
  • "fmt"
  • )
  • ?
  • func main() {
  • fmt.Println(nil==nil)
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    # command-line-arguments
    .\main.go:8:21: invalid operation: nil == nil (operator == not defined on nil)

    這點(diǎn)和 python 等動(dòng)態(tài)語(yǔ)言是不同的,在 python 中,兩個(gè) None 值永遠(yuǎn)相等。

    >>> None == None
    True

    從上面的運(yùn)行結(jié)果不難看出,==對(duì)于 nil 來(lái)說(shuō)是一種未定義的操作。

    nil 不是關(guān)鍵字或保留字

    nil 并不是Go語(yǔ)言的關(guān)鍵字或者保留字,也就是說(shuō)我們可以定義一個(gè)名稱(chēng)為 nil 的變量,比如下面這樣:

    var nil = errors.New("my god")

    雖然上面的聲明語(yǔ)句可以通過(guò)編譯,但是并不提倡這么做。

    nil 沒(méi)有默認(rèn)類(lèi)型

  • package main
  • ?
  • import (
  • "fmt"
  • )
  • ?
  • func main() {
  • fmt.Printf("%T", nil)
  • print(nil)
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    # command-line-arguments
    .\main.go:9:10: use of untyped nil

    不同類(lèi)型 nil 的指針是一樣的

  • package main
  • ?
  • import (
  • "fmt"
  • )
  • ?
  • func main() {
  • var arr []int
  • var num *int
  • fmt.Printf("%p\n", arr)
  • fmt.Printf("%p", num)
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    0x0
    0x0

    通過(guò)運(yùn)行結(jié)果可以看出 arr 和 num 的指針都是 0x0。

    不同類(lèi)型的 nil 是不能比較的

  • package main
  • ?
  • import (
  • "fmt"
  • )
  • ?
  • func main() {
  • var m map[int]string
  • var ptr *int
  • fmt.Printf(m == ptr)
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    # command-line-arguments
    .\main.go:10:20: invalid operation: arr == ptr (mismatched types []int and *int)

    兩個(gè)相同類(lèi)型的 nil 值也可能無(wú)法比較

    在Go語(yǔ)言中 map、slice 和 function 類(lèi)型的 nil 值不能比較,比較兩個(gè)無(wú)法比較類(lèi)型的值是非法的,下面的語(yǔ)句無(wú)法編譯。

  • package main
  • ?
  • import (
  • "fmt"
  • )
  • ?
  • func main() {
  • var s1 []int
  • var s2 []int
  • fmt.Printf(s1 == s2)
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    # command-line-arguments
    .\main.go:10:19: invalid operation: s1 == s2 (slice can only be compared to nil)

    通過(guò)上面的錯(cuò)誤提示可以看出,能夠?qū)⑸鲜霾豢杀容^類(lèi)型的空值直接與 nil 標(biāo)識(shí)符進(jìn)行比較,如下所示:

  • package main
  • ?
  • import (
  • "fmt"
  • )
  • ?
  • func main() {
  • var s1 []int
  • fmt.Println(s1 == nil)
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    true

    nil 是 map、slice、pointer、channel、func、interface 的零值

  • package main
  • ?
  • import (
  • "fmt"
  • )
  • ?
  • func main() {
  • var m map[int]string
  • var ptr *int
  • var c chan int
  • var sl []int
  • var f func()
  • var i interface{}
  • fmt.Printf("%#v\n", m)
  • fmt.Printf("%#v\n", ptr)
  • fmt.Printf("%#v\n", c)
  • fmt.Printf("%#v\n", sl)
  • fmt.Printf("%#v\n", f)
  • fmt.Printf("%#v\n", i)
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    map[int]string(nil)
    (*int)(nil)
    (chan int)(nil)
    []int(nil)
    (func())(nil)
    <nil>

    零值是Go語(yǔ)言中變量在聲明之后但是未初始化被賦予的該類(lèi)型的一個(gè)默認(rèn)值。

    不同類(lèi)型的 nil 值占用的內(nèi)存大小可能是不一樣的

    一個(gè)類(lèi)型的所有的值的內(nèi)存布局都是一樣的,nil 也不例外,nil 的大小與同類(lèi)型中的非 nil 類(lèi)型的大小是一樣的。但是不同類(lèi)型的 nil 值的大小可能不同。

  • package main
  • ?
  • import (
  • "fmt"
  • "unsafe"
  • )
  • ?
  • func main() {
  • var p *struct{}
  • fmt.Println( unsafe.Sizeof( p ) ) // 8
  • ?
  • var s []int
  • fmt.Println( unsafe.Sizeof( s ) ) // 24
  • ?
  • var m map[int]bool
  • fmt.Println( unsafe.Sizeof( m ) ) // 8
  • ?
  • var c chan string
  • fmt.Println( unsafe.Sizeof( c ) ) // 8
  • ?
  • var f func()
  • fmt.Println( unsafe.Sizeof( f ) ) // 8
  • ?
  • var i interface{}
  • fmt.Println( unsafe.Sizeof( i ) ) // 16
  • }
  • 運(yùn)行結(jié)果如下所示:

    PS D:\code> go run .\main.go
    8
    24
    8
    8
    8
    16

    具體的大小取決于編譯器和架構(gòu),上面打印的結(jié)果是在 64 位架構(gòu)和標(biāo)準(zhǔn)編譯器下完成的,對(duì)應(yīng) 32 位的架構(gòu)的,打印的大小將減半。

    3.16?Go語(yǔ)言make和new關(guān)鍵字的區(qū)別及實(shí)現(xiàn)原理

    ?

    ?

    總結(jié)

    以上是生活随笔為你收集整理的第03章 Go语言容器(container)的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。

    如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。