Go語言學習筆記05--切片slice與字典map
1.陣列案例 //統計輸入20個字元中字母出現的個數案例 func checkNumFromInput(){ inputCharArr := [20]byte{},checkNum := [26]byte{} //輸入 for i:=0; i<20; i++{ fmt.Scanf("%c",&inputCharArr[i]); } //核驗 for j:=0; j<20; j++{ checkNum[inputCharArr[j]-'a']++; } //輸出 for k:=0; k<len(checkNum); k++{ if checkNum[k] != 0{ fmt.Printf("字母%c,出現的次數是%d次\n",'a'+k,checkNum[k]); } } } //模擬雙色球搖號,6個不重複(1~33)紅球,和一個藍球 func getTicketArray () [7]int{ finalArr := [7]int{} rand.Seed(time.Now().UnixNano()) //一輪一個隨機紅球 for i:=0; i<6; i++{ temp := rand.Intn(33)+1,flag := true //判重 for j:=0; j<i; j++{ if temp == finalArr[j]{ i--,flag = false break } } //判是否加 if(flag){ finalArr[i] = temp } } //隨機藍球 finalArr[6] = rand.Intn(33)+1 return finalArr } 2.二維陣列 go語言中的二維陣列和傳統c語言中的二維陣列大同小異,還是需要注意宣告和賦值的語法就行 至於陣列的特性,仍舊是第一維度表示行,第二維度表示列。陣列訪問也仍舊需要行列兩個維度的引數。 eg: //var arr [3][5]int = [3][5]int{{1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7}}; var arr [3][5]int = [3][5]int{}; for i:=0; i<3; i++{ for j:=0; j<5; j++{ arr[i][j] = rand.Intn(10); } } fmt.Println(arr);//[[1 7 7 9 1] [8 5 0 6 0] [4 1 2 9 8]] fmt.Println(arr[0][1]);//7 3.切片slice 與傳統語言相比,go語言中的陣列是一個長度固定的固有結構,因此對於陣列的所有操作是不會影響到原陣列的。 這樣統一的規定雖然避免了很多情況下對於原陣列的誤操作,但是陣列大多數情況下是有必要發生修改的, 因此go語言提出了切片(Slice)的概念,切片從某種意義上來說可以認為是陣列的一種可修改的表現方式。 var 切片名 []資料型別 = []資料型別{...} eg: var slice1 []int = []int{1,2,3,4,5,...} 如果單獨檢視切片的語法可能會覺得有些奇怪,那麼直接將切片的語法和陣列的語法相對比結合就能看到些端倪 eg: var slice1 []int = []int{1,2,3,4,5,...} var array [5]int = [5]int{1,2,3,4,5} 非常明顯看到,切片可以認為其實就是長度不固定的陣列。其定義語法與陣列定義方式幾乎相同。 ps: 陣列資料存棧區,切片資料存堆區 (1)切片的操作 1)make方法與切片的自動推導型別 如果每一次定義切片都採用標準宣告方式,會顯得程式碼十分冗餘,因此提出了make方式 切片名 := make(切片型別,切片長度); eg: slice2 := make([]int, 5); slice2[0] = 10; slice2[1] = 11; ... 需要說明的是這裡的長度並不是說切片就只能有5個長度,而是暫時只分配5個容量 之後會隨著需求不斷對切片進行容量擴充。 ps:切片長度,即切片中實際儲存了內容的部分 ps:切片容量,是切片中用於系統分配儲存空間的標尺 2)append方法與切片擴充套件 前面提到過切片是一種類似於可以被修改的陣列,因此go語言提供了append方法對切片進行擴容 切片名 = append(切片名,擴充套件內容1,擴充套件內容2,...) eg: slice3 := []int{1,2,3,4,5}; fmt.Println(slice3);//[1,2,3,4,5] slice3 = append(slice3,11,22,33,44,55); fmt.Println(slice3);//[1,2,3,4,5,11,22,33,44,55] ps:go語言中的append方法其意義為【擴容】,而不是修改, 因此append擴充的切片內容會在已知內容之後 eg: slice4 := make([]int, 5);//[0,0,0,0,0] slice4 = append(slice4,1,2,3);//[0,0,0,0,0,1,2,3] 3)cap方法-切片容量與切片長度 cap(切片名)方法能夠返回切片容量 len(切片名)方法能夠返回切片長度 其區別就是: ·容量必然大於等於長度,因為系統必須為內容分配出足夠的儲存空間。 ·只要新增內容長度必然增加,而容量卻不一定增加 ·如果長度超過了容量,切片才會對容量進行擴充套件,而且每次擴充套件都是上次的倍數。 eg: slice5 := []int{0,0,0,0,0}; //[0,0,0,0,0] fmt.Printf("長度:%d",len(slice5)); //5 fmt.Printf("容量:%d",cap(slice5)); //5 slice5 = append(slice5, 1,2,3); //[0,0,0,0,0,1,2,3] fmt.Printf("長度:%d",len(slice5)); //8 fmt.Printf("容量:%d",cap(slice5)); //10 slice5 = append(slice5, 4); //[0,0,0,0,0,1,2,3,4] fmt.Printf("長度:%d",len(slice5)); //9 fmt.Printf("容量:%d",cap(slice5)); //10 ps: 如果切片的長度擴充超過了容量擴充的2倍,那麼本次容量擴充就會以長度擴充為標準 eg: slice6 := []int{0,0,0,0,0} //[0,0,0,0,0] fmt.Printf("長度:%d",len(slice6)); //5 fmt.Printf("容量:%d",cap(slice6)); //5 slice6 = append(slice6, 1,2,3,4,5,6,7,8); //[0,0,0,0,0,1,2,3,4,5,6,7,8] fmt.Printf("長度:%d",len(slice6)); //13 fmt.Printf("容量:%d",cap(slice6)); //13 上面這個例子就能夠明顯看出,本來切片slice6的一次擴容應當是擴充套件2倍,即變成10 但是由於本次內容長度的擴充套件已經超過了10,所以容量的擴充就會以長度為最低標準,也是13 並且下次在擴充套件的時候,倍數基準就會以13這個數為基準來擴容了 slice6 = append(slice6, 10,11); //[0,0,0,0,0,1,2,3,4,5,6,7,8,10,11] fmt.Printf("長度:%d",len(slice6)); //15 fmt.Printf("容量:%d",cap(slice6)); //26 ps: 如果整體資料沒有超過1024byte,每次擴充套件為上次的倍數 如果整體資料超過了1024byte,每次擴充套件為上次的1/4 ps: 有個疑問就是當切片沒能被初始化,而是通過make方法只創建出結構時 超過2倍的單次擴容總是會讓容量趨向於大於長度的一個偶數值,而不是倍數增長 目前這個問題的具體原因還沒能被我理解 期望隨著學習的深入,能夠搞懂切片的建立方式對容量有什麼不同的影響。 sliceTemp := make([]int, 5); sliceTemp = append(sliceTemp, 1,2,3,4,5,6,7,8); fmt.Printf("長度:%d",len(sliceTemp)); //13 fmt.Printf("容量:%d",cap(sliceTemp)); //14 4)切片快速遍歷 go語言中的切片和陣列除了不能修改之外,可以認為切片的標準訪問方法操作與陣列相同 eg: slice7 := []int{1,2,3,4,5}; for i,v := range slice7{ fmt.Printf("序號:%d,值:%d\n",i,v); } ps:但是一定一定注意,切片在列印的時候只能以長度為標準,而不能以容量作為輸出標準。 因為go語言不會對切片容量中的資料初始化,而是隻初始化長度內的資料 eg: slice8 := []int{0,0,0,0,0}; //[0,0,0,0,0] slice8 = append(slice8,1,2);//[0,0,0,0,0,1,2] 此時長度為7,容量為10 //合法操作 for i:=0; i<len(slice8); i++{ fmt.Printf("%d",slice8[i]); } //違法操作 for i:=0; i<cap(slice8); i++{ fmt.Printf("%d",slice8[i]); } (2)切片擷取 事實上在很多情況下,go語言的開發過程中大都使用切片來代替陣列使用。 而切片的擷取從某種意義上來說,go語言的切片擷取與其他所有傳統語言的操作都不太相同。 eg: baseSlice := []int{1,2,3,4,5,6}; subSlice := baseSlice[1:4]; 1)切片擷取 go語言的切片擷取不會對原切片造成影響 eg: fmt.Println(subSlice);//[2,3,4] fmt.Println(baseSlice);//[1,2,3,4,5,6] 因為go語言切片擷取是從原有的切片中“放大”一部分,並不會真的從原切片中將資料剪切出來 2)切片擷取的操作 但是對擷取切片的操作會對原切片造成影響,因為子切片和切片本身來講都是同一個東西(記憶體地址) eg: fmt.Printf("%p",baseSlice);//0xc00008a000 fmt.Printf("%p",subSlice);//0xc00008a008 subSlice[1] = 100; fmt.Println(subSlice);//[2,100,4] fmt.Println(baseSlice);//[1,2,100,4,5,6] 3)切片擷取的“閾值”與擴充套件 在對切片進行擷取的時候,實際上還存在第三個引數:容量。 切片[其實下標:結束下標:子切片容量] 其表示的含義是對切片擷取後,生成的子切片最大容量是多少。 由於子切片是原切片的“放大”操作,實際上是二位一體的東西, 子切片的容量必須小於等於原切片的容量! eg: baseSlice := []int{1,2,3,4,5,6}; baseSlice = append(baseSlice, 1000); fmt.Printf("%d",len(baseSlice));//原長度:7 fmt.Printf("%d",cap(baseSlice));//原容量:12 ------------------------------------------- ------------------------------------------- subSlice := baseSlice[1:4:3]; fmt.Println(subSlice); fmt.Printf("%d",len(subSlice)); fmt.Printf("%d",cap(subSlice)); 錯誤,【容量】必須大於等於【結束下標】 ------------------------------------------- subSlice := baseSlice[1:4:5]; fmt.Println(subSlice);//[2,3,4]; fmt.Printf("%d",len(subSlice));//切片長度:3 fmt.Printf("%d",cap(subSlice));//切片容量:4 ------------------------------------------- subSlice := baseSlice[1:4:10]; fmt.Println(subSlice);//[2,3,4]; fmt.Printf("%d",len(subSlice));//切片長度:3 fmt.Printf("%d",cap(subSlice));//切片容量:9 ------------------------------------------- subSlice := baseSlice[1:4:13]; fmt.Println(subSlice); fmt.Printf("%d",len(subSlice)); fmt.Printf("%d",cap(subSlice)); 錯誤,【容量】不得超過【原切片的容量】 (3)切片追加與拷貝 1)append方法 append追加後記憶體地址可能發生變化,因為舊的容量不夠用。 這可能對於指標傳遞時產生一些“舊變,新不變”的問題。 eg: slice := []int{}; /* 這意味著這兩個變數指向同一塊記憶體地址,換句話說 這兩個變數都指向了這個空切片 */ temp := slice; fmt.Printf("%p\n",slice); //0x1181f88 fmt.Printf("%p\n",temp); //0x1181f88 fmt.Println(temp); //[] /* 此時切片的容量增加,因為原有的記憶體地址處的切片已經放不下追加的內容 因此go語言會自動尋找能夠放下追加內容的切片的合適位置 因此在slice追加內容後,slice指向了一個新的記憶體地址 而temp還仍然指向著之前的記憶體地址 (即值傳遞,傳遞的內容是一個地址而已) 所以,追加append操作過程中如果出現了賦值傳遞 那麼小心傳遞的內容不會跟隨傳遞後的內容變化而一同變化,那只是一張快照而已。 */ slice = append(slice, 1,2,3); fmt.Printf("%p\n",slice); //0xc00008e000 fmt.Printf("%p\n",temp); //0x1181f88 fmt.Println(temp); //[] 2)copy方法 go語言中陣列切片的copy方法,非常類似於JS中的陣列擷取方法subString copy(desSlice目標切片[起始下標:結束下標], resSlice原切片[起始下標:結束下標]); 對於拷貝範圍是可選部分,允許不寫,而如果不寫預設從起始位置開始拷貝,內容是全部原切片 eg: slice := []int{1,2,3,4,5}; slice2 := make([]int, 5); //預設拷貝全部 copy(slice2, slice); fmt.Println(slice2);//[1,2,3,4,5] //指定拷貝範圍,但起始結束都不寫,也是拷貝全部 copy(slice2, slice[:]); fmt.Println(slice2);//[1,2,3,4,5] //指定拷貝範圍,起始不寫,預設從起始拷貝 copy(slice2, slice[:3]); fmt.Println(slice2);//[1,2,3,0,0] //指定拷貝範圍,結束不寫,預設拷貝到結束 copy(slice2, slice[2:]); fmt.Println(slice2);//[3,4,5,0,0] //指定拷貝範圍,指定拷貝到範圍 copy(slice2[1:], slice[2,4]); fmt.Println(slice2);//[0,3,4,0,0] ps:對於拷貝切片操作來說,目標切片的長度是必須能夠存放下要拷貝的內容的 只要長度超過要拷貝的內容,那麼多長都無所謂,沒有限制說必須小於原切片。 但是若目標切片的長度小於要拷貝的內容,則會造成拷貝內容的丟失 eg: slice := []int{1,2,3,4,5}; slice2 := make([]int, 2); copy(slice2, slice); fmt.Println(slice2);//[1,2] ps:目標拷貝切片和原切片是兩個完全沒有任何關聯的內容,一處修改另一處不會跟隨變化 這與切片拷貝完全不是一回事。 eg: slice := []int{1,2,3,4,5}; slice2 := make([]int, 5); copy(slice2, slice); slice2[2] = 100; fmt.Println(slice1);//[1,2,3,4,5] fmt.Println(slice2);//[1,2,100,4,5] (4)切片傳參 切片傳參是地址傳遞,換句話說就是內部修改外部跟隨變化,與go語言中的陣列是一個明顯的不同。 eg: func test(slice []int){ slice[2] = 100; } func main() { slice := []int{1,2,3,4}; test(slice); fmt.Println(slice);//[1,2,100,4] } 但切片傳參後若內容追加append,則外部不跟隨變化(內容追加後記憶體發生變更) eg: func test(slice []int){ slice = append(slice, 100); } func main() { slice := []int{1,2,3}; test(slice); fmt.Println(slice);//[1,2,3] } 總之,修改沒問題,但追加就會出問題。當然將追加後的內容作為返回值再返回出來則一定不會出問題。 (5)切片案例:猜數字 func splitNumberToSlice(num int) []int{ numGe := num%10; numShi := num%100/10; numBai := num/100; return []int{numBai,numShi,numGe}; } func main() { rand.Seed(time.Now().UnixNano()); pivotSlice := splitNumberToSlice(rand.Intn(899)+100); var userNum int; for{ fmt.Println("請輸入一個三位數:"); fmt.Scan(&userNum); if userNum>=100&&userNum<=999{ userSlice := splitNumberToSlice(userNum); flag := 0; fmt.Println("從左到右:"); for i:=0; i<3; i++{ if userSlice[i]<pivotSlice[i]{ fmt.Printf("第%d位小了\n",i+1); }else if userSlice[i]>pivotSlice[i]{ fmt.Printf("第%d位大了\n",i+1); }else{ fmt.Printf("第%d位正確\n",i+1); flag++; } } if flag==3{ fmt.Printf("您猜對了,數字就是:%d\n", userNum); break; } }else{ fmt.Println("您輸入的數字範圍不合法,請輸入100~999之間的三位數\n"); } } } 4.字典map (1)基本資訊 在go語言中map資料型別表示字典結構,類似於傳統c/c++/php中的字典、py中的列表、javascript的物件。 它是由鍵值對構成,鍵與值之間用冒號分隔,鍵值對之間用逗號分隔的無序儲存方式。 (key可以是任何非複雜資料型別,value可以是任意資料型別。) eg: var map名稱 map[keyType]valueType = map[keyType]valueType{}; eg: var userDic map[string]int = map[string]int{"jack":100}; 1)map的初始化 eg: userDic := map[string]int{"lilei":10}; userDic := make(map[string]int, 10); ps: 在go語言中map資料型別的零值是nil,因此map建立後必須初始化一下,否則無法正常使用。 eg: var userDic map[string]int; userDic["jack"] = 100;//違法操作 2)map自動擴容 在go語言中map和陣列切片並不相同,map是自動擴容的,因此使用make初始化時給多少長度無所謂。 eg: userDic := make(map[string]int);//長度壓根就可以不寫 userDic["jack"] = 100; userDic["andy"] = 50;//正確 3)map的長度與容量 在go語言中map的長度是不能夠用len來計算的,因為map內部對於資料的儲存是無序的鏈式儲存 (鏈式儲存,即每個鍵值對在儲存內容之外,還會儲存下一個鍵值對所在的記憶體首地址) 因此對於map的長度只能通過range遍歷計算。正因如此,對於map而言容量和長度的概念變得毫無意義。 但是某些情況下我們卻仍然需要獲知map中的鍵值對的個數 所以最終,go語言規定len方法作用於map的時候,返回的結果是map鍵值對的個數 而且對於map也不在考慮容量的問題,因為容量恆等於長度,即map中鍵值對的個數。 eg: fmt.Println(len(userDic));//1 for k,v := range userDic{ fmt.Printf("%s--%d\n",k,v); } 4)map的鍵名唯一 在go語言中map字典在【定義】時,key是唯一的,重複定義key會導致丟擲異常。 注意僅僅是定義,使用的時候是無所謂的。因為使用重複key值表示對key所對應的值的修改。 eg: userDic := map[string]int{"jack":100,"jack":20};//絕對違法! // userDic := map[string]int{"jack":100}; userDic["jack"] = 20;//沒毛病 (2)猜數字案例-改寫: func splitNumberToMap(num int) map[string]int{ numGe := num%10; numShi := num%100/10; numBai := num/100; return map[string]int{"百位":numBai,"十位":numShi,"個位":numGe}; } func main() { rand.Seed(time.Now().UnixNano()); pivotMap := splitNumberToMap(rand.Intn(899)+100); var userNum int; for{ fmt.Println("請輸入一個三位數:"); fmt.Scan(&userNum); if userNum>=100&&userNum<=999{ userMap := splitNumberToMap(userNum); flag := 0; for k,v := range userMap{ if v<pivotMap[k]{ fmt.Printf("%s小了\n",k); }else if v>pivotMap[k]{ fmt.Printf("%s大了\n",k); }else{ fmt.Printf("%s正確\n",k); flag++; } } if flag==3{ fmt.Printf("您猜對了,數字就是:%d\n", userNum); break; } }else{ fmt.Println("您輸入的數字範圍不合法,請輸入100~999之間的三位數\n"); } } } (3)map的值 因為在go語言中,對map的訪問總是能夠得到一個確定的值,哪怕這個key並不存在於map中也是如此。 因此map提出了一種判別key值是否存在的機制。 eg: userDic := map[string]int{"jack":100,"frank":300}; //真值 val,flag := userDic["jack"]; fmt.Printf("%d,%t");//100,true //假值 val,flag := userDic["Alice"]; fmt.Printf("%d,%t");//0,false (4)delete-map的刪除 在go語言中map的內容的刪除,實際上就是鍵值對的刪除。 eg: delete(要執行刪除操作的map, 要刪除鍵值對的key) eg: delete(userDic, "jack"); ps: 需要注意的是,delete秉承了go語言的一貫簡潔的作風, 不但不存在返回值,而且不論key值是否存在都會正常向下執行。(簡潔的有點大勁了感覺) 因此一般不確定key值是否存在時,用val,flag判別一下在刪除是一個不錯的選擇 eg: userDic := map[string]int{"jack":100}; val,flag = userDic["frank"]; if flag{ delete(userDic, "frank"); }else{ fmt.Println("map中不存在frank這樣的key"); } (5)map傳參與返回值 在go語言中的map由於是自動擴容的,所以記憶體地址在變數完成記憶體分配後是不會發生變動的 所以map的傳值採用的是真正意義上的地址傳遞。內部操作,外部變化 eg: //合併map中的陣列切片 func joinSliceFromMap(tempMap map[string][]int)[]int{ //粗糙演算法,先算容量,在做合併 //程式碼冗餘到刺眼 //finalArrLength := 0; //for _,v := range tempMap{ // finalArrLength += len(v); //} //finalArr := make([]int, finalArrLength); //testIndex := 0; //for _,v := range tempMap{ // copy(finalArr[testIndex:],v); // testIndex += len(v); //} //擴容演算法,長度無所謂,有內容就填充 finalArr := make([]int,0); for _,v := range tempMap{ for i:=0; i<len(v); i++{ finalArr = append(finalArr, v[i]); } } return finalArr; } func main() { myMap := map[string][]int{ "jack":[]int{1,2,3,4,5}, "frank":[]int{5,4,3,2,1}, } resultArr := joinSliceFromMap(myMap); fmt.Println(resultArr);//[1,2,3,4,5,5,4,3,2,1] } --------------------- 作者:Frank·Ming 來源:CSDN 原文:https://blog.csdn.net/u013792921/article/details/84504092 版權宣告:本文為博主原創文章,轉載請附上博文連結!