4.go語言複合型別簡述

2023-09-05 15:01:27

1. 本章前瞻

很好,經過很長的時間,你終於來到go語言的複合型別中,這裡會介紹go語言的3種複合結構:切片(slice,可變陣列),對映(map)和字串(string)。

有些老手可能會問:

1.那結構體(struct)呢,你怎麼不介紹?

答:現在還沒法完整地介紹結構體(struct),主要是沒法介紹結構體相關的方法。

2.對於字串(string),字串(string)怎麼會是複合型別呢?

答:字串(string)可以認為元素無法變更的byte陣列,為此我認為它是複合型別。

這裡開始會有新版本的內容加入進來,本來的內容會以go1.20為主,但是由於go語言的半年更新週期,現在就必須加上go1.21的相關內容,請學習愉快!

2.來自leetcode的例題

題目非常地簡單,讓我先水一下,本題唯一的要點是使用map[int]struct{}這種go語言風格的set型別

描述

26. 刪除有序陣列中的重複項

給你一個 升序排列 的陣列 nums ,請你 原地 刪除重複出現的元素,使每個元素 只出現一次 ,返回刪除後陣列的新長度。元素的 相對順序 應該保持 一致 。然後返回 nums 中唯一元素的個數。

分析

真的有如開始所說的那樣簡單,使用map,注意在go語言中map是使用列表實現的,我們就可以輕鬆地去重

題解

func removeDuplicates(nums []int) (k int) {
	numMap := make(map[int]struct{}) //struct{}的大小為0,map[int]struct{}一般用作集合

	k = 0
	for _, v := range nums {
		if _, ok := numMap[v]; !ok {
			numMap[v] = struct{}{}
			nums[k] = v
			k++
		}
	}

	return
}

3. 複合型別新版本的變化

這裡說的新版本是指go1.20之後的變化, 這裡解說的是近幾個版本中關於複合型別的重要變化。

3.1 string和[]byte的高效轉化

reflect.StringHeader在業內經常被濫用,使用不方便,很容易出現隱性問題,為了解決這個問題,go1.20對於string和[]byte型別進行高效轉化,unsafe提供了

func String(ptr *byte, len IntegerType) string
func StringData(str string) *byte
func Slice(ptr *ArbitraryType, len IntegerType) []ArbitraryType

利用上述三個函數,可以方便地進行string和[]byte的高效轉化,如果你對於go1.20之前的高效轉化有興趣,可以看字串的4.3.2章節

func StringToBytes(s string) []byte {
    return unsafe.Slice(unsafe.StringData(s), len(s))
}

func BytesToString(b []byte) string {
    return unsafe.String(&b[0], len(b))
}

3.2 內建函數clear

在go1.21之前,你如果需要清空一個map,那麼你必須進行迴圈delete

	myMap := map[string]int{
		"A": 1,
		"B": 2,
		"C": 3,
	}

	fmt.Println(myMap)
	for k := range myMap {
		delete(myMap, k)
	}

	fmt.Println(myMap)

而現在go1.21中你可以

	myMap = map[string]int{
		"A": 1,
		"B": 2,
		"C": 3,
	}
	fmt.Println(myMap)

	clear(myMap)
	fmt.Println(myMap)

但是如果你將其用於切片,你會發現切片中的元素不會被刪除,只會被清零。

	mySlice := []int{1, 2, 3, 4, 5, 6, 7, 8}
	fmt.Println(mySlice) 	//輸出為[1 2 3 4 5 6 7 8]

	clear(mySlice)
	fmt.Println(mySlice)   //輸出為[0 0 0 0 0 0 0 0]

4. 複合型別概述

下面是使用AI生成的複合型別,注意:如果你有需求,可以回顧這些知識,但是基本上可以跳過。

4.1 切片

Go語言中的切片(slice)是一種基於陣列的靈活且強大的資料結構,它是對陣列的一個參照。切片提供了一種方便的方式來操作和操作集合。

切片有三個關鍵屬性:

  1. 底層陣列(Underlying Array):切片參照一個具體的陣列,底層陣列是切片資料的實際儲存位置。
  2. 長度(Length):切片的當前元素個數。
  3. 容量(Capacity):切片底層陣列中從切片的開始位置到底層陣列結束的元素個數。

切片在宣告、初始化、操作和比較方面都比陣列更加靈活。你可以使用make()函數建立一個切片,使用append()函數向切片新增元素,使用copy()函數複製切片,還可以使用切片的索引操作獲取或修改特定位置的元素。

切片還支援一些內建的方法,如len()cap(),分別返回切片的長度和容量。此外,切片是參照型別,傳遞切片作為引數不會複製整個切片,而只是複製參照,這使得函數能夠修改原始資料。

以下是一些關於建立和使用切片的範例程式碼:

package main

import "fmt"

func main() {
	// 建立一個整數陣列
	a := [5]int{1, 2, 3, 4, 5}

	// 建立一個基於陣列的切片
	s := a[1:3]
	fmt.Println(s) // 輸出: [2 3]

	// 使用make函數建立一個長度為5的切片,初始值為0
	m := make([]int, 5)
	fmt.Println(m) // 輸出: [0 0 0 0 0]

	// 使用append函數向切片新增元素
	m = append(m, 1, 2, 3)
	fmt.Println(m) // 輸出: [0 0 0 0 0 1 2 3]

	// 使用copy函數複製切片
	n := make([]int, len(m))
	copy(n, m)
	fmt.Println(n) // 輸出: [0 0 0 0 0 1 2 3]

	// 這裡寫段
	m = append(m[:len(m)-2], m[2:]...)
	fmt.Println(m) // 輸出: [0 0 3]
}

以上程式碼展示了切片的基本操作,包括建立、新增元素、複製和刪除元素等。切片是Go語言中非常強大且有用的資料結構,可以幫助開發者更高效地處理資料和實現複雜的邏輯。

4.2 對映

Go語言中的對映(map)是一種特殊的資料結構,它是一個無序的鍵值對集合。對映的鍵必須是唯一的,但值可以重複。對映的元素是鍵值對,每個鍵都對映到一個值。

對映的宣告使用如下語法:

var m map[keyType]valueType

其中,keyType是鍵的型別,valueType是值的型別。

對映的操作包括:

  1. m[k]:使用鍵k存取對映,返回對應的值。如果鍵k在對映中不存在,會返回該對映型別的零值。
  2. m[k] = v:使用鍵k將值v賦值給對映。如果鍵k在對映中不存在,會建立一個新的鍵值對。
  3. delete(m, k):刪除對映m中鍵為k的鍵值對。
  4. len(m):返回對映中鍵值對的數量。
  5. m[k] == vm[k] != v:用於判斷對映中是否存在某個鍵k,並且其對應的值是否等於v

需要注意的是,對映是參照型別,傳遞的是參照而不是整個對映的拷貝。因此,對對映的修改會影響原始對映。此外,對映的鍵必須是唯一的,但值可以重複。對映的元素是無序的,每次迭代時順序可能不同。

以下是一個使用對映的範例程式碼:

package main

import "fmt"

func main() {
	// 建立一個對映
	m := make(map[string]int)
	//賦值
	m = map[string]int{
		"我是水貨": 1,
		"水貨":   2,
		"水":    3,
	}
	// 新增鍵值對到對映
	m["貨"] = 4

	// 存取對映中的元素
	fmt.Println(m["我是水貨"]) // 輸出: 1
	fmt.Println(m["水貨"])   // 輸出: 2
	fmt.Println(m["水"])    // 輸出: 3

	// 修改對映中的元素
	m["水貨"] = 10
	fmt.Println(m["水貨"]) // 輸出: 10

	// 刪除對映中的元素
	delete(m, "水")
	fmt.Println(m["水"]) // 輸出: 0

	// 檢查對映中是否存在某個鍵
	_, ok := m["水貨"]
	fmt.Println(ok) // 輸出: true
	_, ok = m["水"]
	fmt.Println(ok) // 輸出: false
}

以上程式碼展示瞭如何宣告、存取、修改和刪除對映中的元素,以及如何檢查對映中是否存在某個鍵。

4.3 字串

在Go語言中,string是一種內建型別,表示位元組的序列。這些位元組通常用於表示Unicode字元序列。String是不可變的,也就是說,一旦一個字串被建立,就不能修改它。

字串之間可以進行比較操作(==、!=、<、>),而字串與位元組陣列之間可以通過+號進行拼接,也可以通過==進行比較操作。字串也可以通過使用索引語法s[i]獲取指定位置的位元組,但不可進行修改操作。

Go語言的string型別是不可變的,這意味著你不能修改字串的內容。如果你需要一個可變的字串,可以將字串轉換為位元組陣列([]byte),然後進行修改操作。

在Go語言中,字串的本質是一個位元組陣列([]byte)。因此,它們之間可以互相轉換。例如,你可以將字串轉換為位元組陣列,然後對位元組陣列進行修改,再將修改後的位元組陣列轉換回字串。

需要注意的是,字串的長度是固定的,不能進行修改。如果你需要一個可變長度的字串,可以將字串轉換為切片(slice),然後進行修改操作。

以下是一些關於Go語言string型別的範例程式碼:

package main  
  
import "fmt"  
  
func main() {  
    // 宣告一個字串變數  
    str := "Hello, World!"  
  
    // 獲取字串的長度  
    fmt.Println(len(str))  // 輸出: 13  
  
    // 將字串轉換為位元組陣列  
    bytes := []byte(str)  
  
    // 修改位元組陣列中的某個元素  
    bytes[0] = 'M'  
  
    // 將修改後的位元組陣列轉換回字串  
    str = string(bytes)  
    fmt.Println(str)  // 輸出: Mello, World!  
}

以上程式碼演示瞭如何宣告一個字串變數、獲取字串的長度、將字串轉換為位元組陣列、修改位元組陣列中的元素,以及將修改後的位元組陣列轉換回字串。

4.3.1 字串的底層機構

在Go語言中,字串底層結構是一個位元組序列,其資料結構定義如下:

type stringStruct struct {  
    str unsafe.Pointer  
    len int  
}

這裡,str是一個指向底層位元組陣列的指標,而len表示字串的位元組長度。這個結構體定義在runtime/string.go檔案中。

字串的賦值操作實際上是結構體的複製過程,不包含指標指向的內容的複製。這意味著,字串是不可變的,一旦初始化後就不能修改。如果你需要一個可變的字串,可以將字串轉換為位元組陣列,然後進行修改操作。修改後的位元組陣列再通過string()函數可以轉回為字串。

另外,字串可以支援切片操作,不同位置的切片底層存取的是同一塊記憶體資料。由於唯讀的特性,相同字串面值常數通常對應同一個字串常數。

需要注意的是,Go語言的字串底層儲存是基於UTF-8編碼的。UTF-8是一種可變長度的編碼方式,每個字元由1到4個位元組組成,具體取決於字元的Unicode碼位。在UTF-8編碼中,ASCII字元只需要1個位元組表示,雙位元組字元需要2個位元組表示,以此類推。因此,對於非ASCII字元的字串,其長度可能不等於位元組長度。

綜上所述,Go語言的字串底層資料結構是一個指向底層位元組陣列的指標和字串長度的組合。字串是不可變的,但可以通過轉換為位元組陣列進行修改。字串支援切片操作和UTF-8編碼。

4.3.2 string和[]byte的轉化

在Go語言中,可以使用unsafe包來轉換string[]byteunsafe包提供了一組函數,可以在不進行邊界檢查的情況下直接存取記憶體地址。

要將string轉換為[]byte,可以使用[]byte()型別強轉,可以使用unsafe.Pointer()函數將字串的指標轉換為位元組陣列的指標,然後使用*(*[]byte)(unsafe.Pointer(uintptr))進行型別斷言。

下面是一個範例程式碼:

package main  
  
import (  
 "fmt"  
 "unsafe"  
)  
  
func main() {  
 str := "Breeze0806"  
 strPtr := unsafe.Pointer(&str)  
 bytesPtr := *(*[]byte)(unsafe.Pointer(uintptr(strPtr)))  
 fmt.Println(bytesPtr)  
}

要將[]byte轉換為string,可以使用string()型別強轉,也可以使用以下方式。但是,需要注意的是,如果位元組陣列包含無效的UTF-8序列,轉換後的字串可能會出現亂碼。

下面是一個範例程式碼:

package main  
  
import (  
 "fmt"  
 "unsafe"  
)  
  
func main() {  
	b := []byte{66, 114, 101, 101, 122, 101, 48, 56, 48, 54}
	str = *(*string)(unsafe.Pointer(&b))
	fmt.Println(str) // Output: Breeze0806
}

需要注意的是,使用unsafe包進行型別轉換是不安全的,因為它繞過了Go語言的型別檢查機制。因此,應該謹慎使用,並確保轉換後的資料是正確的。

6. 下一篇

《使用go語言的資料型別解決leetcode題目》