【Go-6】數據結構與集合

6. 數據結構與集合

數據結構是編程中用于組織和存儲數據的方式,直接影響程序的效率和性能。Go語言提供了多種內置的數據結構,如數組、切片、Map和結構體,支持不同類型的數據管理和操作。本章將詳細介紹Go語言中的主要數據結構與集合,涵蓋它們的定義、使用方法、操作技巧以及底層原理。通過豐富的示例和深入的解釋,幫助你全面掌握Go語言的數據結構,為構建高效、可維護的程序奠定堅實的基礎。

6.1 數組

數組是具有固定大小和相同類型元素的有序集合。在Go語言中,數組的長度是其類型的一部分,這意味著具有不同長度的數組屬于不同的類型。

數組的聲明與初始化

1. 聲明數組

使用var關鍵字聲明數組時,需要指定數組的長度和元素類型。

var arr [5]int

解釋:

  • arr是一個長度為5的整型數組。
  • 所有元素默認初始化為0。

2. 聲明并初始化數組

可以在聲明數組的同時為其元素賦值。

var arr [3]string = [3]string{"apple", "banana", "cherry"}

簡化聲明:

當聲明和初始化數組時,Go可以根據初始化的元素數量自動推斷數組的長度。

arr := [3]string{"apple", "banana", "cherry"}

使用省略長度

通過使用...,Go可以根據初始化的元素數量自動確定數組的長度。

arr := [...]float64{1.1, 2.2, 3.3, 4.4}

3. 多維數組

Go支持多維數組,最常見的是二維數組。

var matrix [3][4]int

初始化二維數組:

matrix := [2][3]int{{1, 2, 3},{4, 5, 6},
}

完整示例:

package mainimport "fmt"func main() {// 聲明并初始化一維數組var arr [5]int = [5]int{1, 2, 3, 4, 5}fmt.Println("一維數組:", arr)// 使用省略長度聲明數組arr2 := [...]string{"Go", "Python", "Java"}fmt.Println("省略長度的一維數組:", arr2)// 聲明并初始化二維數組matrix := [2][3]int{{1, 2, 3},{4, 5, 6},}fmt.Println("二維數組:", matrix)
}

輸出:

一維數組: [1 2 3 4 5]
省略長度的一維數組: [Go Python Java]
二維數組: [[1 2 3] [4 5 6]]
數組的操作

1. 訪問數組元素

通過索引訪問數組元素,索引從0開始。

package mainimport "fmt"func main() {arr := [3]string{"apple", "banana", "cherry"}fmt.Println("第一個元素:", arr[0]) // 輸出: applefmt.Println("第二個元素:", arr[1]) // 輸出: bananafmt.Println("第三個元素:", arr[2]) // 輸出: cherry
}

2. 修改數組元素

數組元素是可修改的,只需通過索引賦值。

package mainimport "fmt"func main() {arr := [3]int{10, 20, 30}fmt.Println("原數組:", arr)arr[1] = 25fmt.Println("修改后的數組:", arr) // 輸出: [10 25 30]
}

3. 遍歷數組

使用for循環或range關鍵字遍歷數組。

使用傳統for循環:

package mainimport "fmt"func main() {arr := [3]string{"apple", "banana", "cherry"}for i := 0; i < len(arr); i++ {fmt.Printf("元素 %d: %s\n", i, arr[i])}
}

使用range遍歷:

package mainimport "fmt"func main() {arr := [3]string{"apple", "banana", "cherry"}for index, value := range arr {fmt.Printf("元素 %d: %s\n", index, value)}
}

4. 數組長度

數組的長度是其類型的一部分,可以通過len函數獲取。

package mainimport "fmt"func main() {arr := [5]int{1, 2, 3, 4, 5}fmt.Println("數組長度:", len(arr)) // 輸出: 5
}

5. 數組作為函數參數

在Go中,數組作為函數參數時,會復制整個數組。因此,對于大數組,推薦使用指針或切片。

package mainimport "fmt"// 函數接收數組參數
func printArray(arr [3]int) {for _, v := range arr {fmt.Println(v)}
}func main() {arr := [3]int{1, 2, 3}printArray(arr)
}

輸出:

1
2
3
注意事項
  • 固定長度:數組的長度在聲明時固定,無法動態改變。如果需要動態長度,建議使用切片。

  • 類型區別:不同長度的數組屬于不同類型,即[3]int[4]int是不同的類型。

    var a [3]int
    var b [4]int
    // a = b // 編譯錯誤: cannot use b (type [4]int) as type [3]int in assignment
    
  • 數組拷貝:數組作為值類型會被復制。因此,在函數中修改數組不會影響原數組,除非使用指針傳遞。

6.2 切片

切片是基于數組的動態數據結構,比數組更靈活和強大。切片的長度和容量可以動態變化,是Go語言中最常用的數據結構之一。

切片的聲明與初始化

1. 聲明切片

切片不需要在聲明時指定長度,可以通過多種方式聲明。

var s []int

解釋:

  • s是一個整型切片,初始為nil

2. 使用make函數創建切片

make函數用于創建切片、Map和Channel。對于切片,make需要指定類型、長度和可選的容量。

s1 := make([]int, 5)          // 長度為5,容量為5,元素初始化為0
s2 := make([]int, 3, 10)      // 長度為3,容量為10

3. 字面量初始化

可以在聲明時通過字面量賦值初始化切片。

s3 := []string{"Go", "Python", "Java"}

4. 從數組或其他切片創建切片

arr := [5]int{1, 2, 3, 4, 5}
s4 := arr[1:4] // 包含索引1、2、3,即 [2, 3, 4]

5. 使用append函數擴展切片

切片的長度可以通過append函數動態增長。

s := []int{1, 2, 3}
s = append(s, 4, 5) // s現在為 [1, 2, 3, 4, 5]

完整示例:

package mainimport "fmt"func main() {// 使用make創建切片s1 := make([]int, 5)fmt.Println("s1:", s1) // 輸出: [0 0 0 0 0]s2 := make([]int, 3, 10)fmt.Println("s2:", s2) // 輸出: [0 0 0]// 字面量初始化s3 := []string{"Go", "Python", "Java"}fmt.Println("s3:", s3) // 輸出: [Go Python Java]// 從數組創建切片arr := [5]int{1, 2, 3, 4, 5}s4 := arr[1:4]fmt.Println("s4:", s4) // 輸出: [2 3 4]// 使用append擴展切片s4 = append(s4, 6, 7)fmt.Println("s4 after append:", s4) // 輸出: [2 3 4 6 7]
}

輸出:

s1: [0 0 0 0 0]
s2: [0 0 0]
s3: [Go Python Java]
s4: [2 3 4]
s4 after append: [2 3 4 6 7]
切片的操作

1. 添加元素

使用append函數向切片添加元素,可以添加單個或多個元素。

package mainimport "fmt"func main() {s := []int{1, 2, 3}s = append(s, 4)fmt.Println("添加一個元素:", s) // 輸出: [1 2 3 4]s = append(s, 5, 6)fmt.Println("添加多個元素:", s) // 輸出: [1 2 3 4 5 6]
}

2. 刪除元素

Go語言沒有內置的刪除函數,但可以通過切片操作實現。

示例:刪除索引為2的元素

package mainimport "fmt"func main() {s := []int{1, 2, 3, 4, 5}index := 2 // 刪除元素3s = append(s[:index], s[index+1:]...)fmt.Println("刪除元素后的切片:", s) // 輸出: [1 2 4 5]
}

3. 修改元素

直接通過索引修改切片中的元素。

package mainimport "fmt"func main() {s := []string{"apple", "banana", "cherry"}s[1] = "blueberry"fmt.Println("修改后的切片:", s) // 輸出: [apple blueberry cherry]
}

4. 切片截取

通過切片操作可以創建子切片,指定起始和結束索引。

package mainimport "fmt"func main() {s := []int{10, 20, 30, 40, 50}sub1 := s[1:4]fmt.Println("sub1:", sub1) // 輸出: [20 30 40]sub2 := s[:3]fmt.Println("sub2:", sub2) // 輸出: [10 20 30]sub3 := s[2:]fmt.Println("sub3:", sub3) // 輸出: [30 40 50]
}

5. 復制切片

使用copy函數復制切片內容。

package mainimport "fmt"func main() {src := []int{1, 2, 3, 4, 5}dst := make([]int, len(src))copy(dst, src)fmt.Println("源切片:", src)fmt.Println("目標切片:", dst)
}

輸出:

源切片: [1 2 3 4 5]
目標切片: [1 2 3 4 5]

6. 切片的容量

切片的容量是從切片的起始位置到底層數組末尾的元素數量。使用cap函數可以獲取切片的容量。

package mainimport "fmt"func main() {s := make([]int, 3, 5)fmt.Println("切片:", s)              // 輸出: [0 0 0]fmt.Println("長度:", len(s))        // 輸出: 3fmt.Println("容量:", cap(s))        // 輸出: 5s = append(s, 1, 2)fmt.Println("切片 after append:", s) // 輸出: [0 0 0 1 2]fmt.Println("長度:", len(s))          // 輸出: 5fmt.Println("容量:", cap(s))          // 輸出: 5// 再次添加元素,容量會自動增長s = append(s, 3)fmt.Println("切片 after second append:", s) // 輸出: [0 0 0 1 2 3]fmt.Println("長度:", len(s))                // 輸出: 6fmt.Println("容量:", cap(s))                // 輸出: 10 (通常會翻倍)
}

輸出:

切片: [0 0 0]
長度: 3
容量: 5
切片 after append: [0 0 0 1 2]
長度: 5
容量: 5
切片 after second append: [0 0 0 1 2 3]
長度: 6
容量: 10
切片的底層原理

切片在Go語言中是一個引用類型,包含三個部分:

  1. 指針:指向底層數組的第一個元素。
  2. 長度(len):切片中的元素數量。
  3. 容量(cap):從切片的起始位置到底層數組末尾的元素數量。

示例:

package mainimport "fmt"func main() {arr := [5]int{1, 2, 3, 4, 5}s := arr[1:4]fmt.Printf("數組: %v\n", arr)fmt.Printf("切片: %v, len=%d, cap=%d\n", s, len(s), cap(s)) // 輸出: [2 3 4], len=3, cap=4// 修改切片中的元素s[0] = 20fmt.Println("修改后的數組:", arr) // 輸出: [1 20 3 4 5]
}

輸出:

數組: [1 2 3 4 5]
切片: [2 3 4], len=3, cap=4
修改后的數組: [1 20 3 4 5]

解釋:

  • 切片s指向數組arr的索引1到3。
  • 修改切片中的元素也會影響底層數組。

容量的影響:

  • 當切片的容量足夠時,使用append不會重新分配底層數組。
  • 當容量不足時,append會分配一個新的底層數組,將原有數據復制過來。

示例:

package mainimport "fmt"func main() {arr := [3]int{1, 2, 3}s := arr[:]fmt.Printf("切片: %v, len=%d, cap=%d\n", s, len(s), cap(s)) // 輸出: [1 2 3], len=3, cap=3// 使用append添加元素,容量不足,會創建新數組s = append(s, 4)fmt.Printf("切片 after append: %v, len=%d, cap=%d\n", s, len(s), cap(s)) // 輸出: [1 2 3 4], len=4, cap=6// 修改新切片,不影響原數組s[0] = 10fmt.Println("切片 after modification:", s) // 輸出: [10 2 3 4]fmt.Println("原數組:", arr)                // 輸出: [1 2 3]
}

輸出:

切片: [1 2 3], len=3, cap=3
切片 after append: [1 2 3 4], len=4, cap=6
切片 after modification: [10 2 3 4]
原數組: [1 2 3]

解釋:

  • 初始切片s的容量為3。
  • append操作導致切片容量增長,并分配了新的底層數組。
  • 修改新切片不影響原數組。
注意事項
  • 切片與數組的關系:切片是對數組的引用,修改切片會影響底層數組,反之亦然。
  • 內存管理:切片本身不存儲數據,數據存儲在底層數組中。切片可以通過多個切片引用同一個底層數組,可能導致數據共享和競態條件。
  • 切片的零值var s []int聲明的切片是nil,長度和容量均為0。可以通過appendmake初始化切片。

6.3 Map

Map是鍵值對的無序集合,鍵和值可以是不同的類型。Map在Go中作為內置數據類型提供,類似于Python的字典或Java的HashMap。它在快速查找、插入和刪除數據方面表現出色。

Map 的聲明與使用

1. 聲明Map

使用var關鍵字聲明Map時,需要指定鍵和值的類型。

var capitals map[string]string

解釋:

  • capitals是一個鍵類型為string,值類型為string的Map。
  • 初始值為nil,需要使用make函數初始化。

2. 使用make初始化Map

capitals = make(map[string]string)

3. 聲明并初始化Map

可以在聲明時通過字面量賦值初始化Map。

capitals := map[string]string{"中國": "北京","美國": "華盛頓","日本": "東京",
}

4. 添加和訪問元素

通過鍵訪問或添加元素。

capitals["德國"] = "柏林" // 添加元素
capital := capitals["美國"] // 訪問元素
fmt.Println("美國的首都是:", capital) // 輸出: 美國的首都是: 華盛頓

5. 完整示例

package mainimport "fmt"func main() {// 聲明并初始化Mapcapitals := map[string]string{"中國": "北京","美國": "華盛頓","日本": "東京",}fmt.Println("原始Map:", capitals)// 添加元素capitals["德國"] = "柏林"fmt.Println("添加德國后的Map:", capitals)// 訪問元素capital := capitals["美國"]fmt.Println("美國的首都是:", capital)// 修改元素capitals["日本"] = "大阪"fmt.Println("修改日本后的Map:", capitals)// 刪除元素delete(capitals, "德國")fmt.Println("刪除德國后的Map:", capitals)
}

輸出:

原始Map: map[中國:北京 美國:華盛頓 日本:東京]
添加德國后的Map: map[中國:北京 美國:華盛頓 德國:柏林 日本:東京]
美國的首都是: 華盛頓
修改日本后的Map: map[中國:北京 美國:華盛頓 德國:柏林 日本:大阪]
刪除德國后的Map: map[中國:北京 美國:華盛頓 日本:大阪]
Map 的遍歷與修改

1. 遍歷Map

使用for循環結合range關鍵字遍歷Map。

package mainimport "fmt"func main() {capitals := map[string]string{"中國": "北京","美國": "華盛頓","日本": "東京",}for country, capital := range capitals {fmt.Printf("%s 的首都是 %s\n", country, capital)}
}

輸出示例:

中國 的首都是 北京
美國 的首都是 華盛頓
日本 的首都是 東京

2. 僅遍歷鍵或值

如果只需要鍵或值,可以使用_忽略不需要的部分。

僅遍歷鍵:

for country := range capitals {fmt.Println("國家:", country)
}

僅遍歷值:

for _, capital := range capitals {fmt.Println("首都:", capital)
}

3. 修改Map元素

在遍歷過程中可以直接修改Map的元素。

package mainimport "fmt"func main() {capitals := map[string]string{"中國": "北京","美國": "華盛頓","日本": "東京",}// 修改所有首都名稱for country := range capitals {capitals[country] = "首都-" + capitals[country]}fmt.Println("修改后的Map:", capitals)
}

輸出:

修改后的Map: map[中國:首都-北京 美國:首都-華盛頓 日本:首都-東京]

4. 檢查鍵是否存在

在訪問Map的元素時,可以同時檢查鍵是否存在。

package mainimport "fmt"func main() {capitals := map[string]string{"中國": "北京","美國": "華盛頓",}capital, exists := capitals["日本"]if exists {fmt.Println("日本的首都是:", capital)} else {fmt.Println("日本的首都不存在")}
}

輸出:

日本的首都不存在

5. 使用delete函數刪除元素

package mainimport "fmt"func main() {capitals := map[string]string{"中國": "北京","美國": "華盛頓","日本": "東京",}delete(capitals, "美國")fmt.Println("刪除美國后的Map:", capitals)
}

輸出:

刪除美國后的Map: map[中國:北京 日本:東京]
注意事項
  • Map的零值:未初始化的Map為nil,不能進行讀寫操作。需要使用make或字面量初始化Map。

    var m map[string]int
    // m["key"] = 1 // 運行時錯誤: assignment to entry in nil mapm = make(map[string]int)
    m["key"] = 1 // 正確
    
  • Map的無序性:Map中的元素是無序的,遍歷時元素的順序是不確定的。如果需要有序的數據結構,建議使用切片或其他結構。

    示例:

    package mainimport "fmt"func main() {m := map[string]int{"apple":  5,"banana": 3,"cherry": 7,}for k, v := range m {fmt.Printf("%s: %d\n", k, v)}// 輸出順序不確定
    }
    
  • Map的鍵類型:Map的鍵必須是可比較的類型,如布爾型、數字、字符串、指針、接口和結構體(前提是結構體的所有字段都是可比較的)。切片、Map和函數類型不能作為鍵。

    // 合法鍵類型
    m1 := map[string]int{}
    m2 := map[int]bool{}
    m3 := map[struct{ a int; b string }]float64{}// 非法鍵類型
    // m4 := map[[]int]string{}      // 編譯錯誤: invalid map key type []int
    // m5 := map[map[string]int]int{} // 編譯錯誤: invalid map key type map[string]int
    // m6 := map[func(){}]bool{}     // 編譯錯誤: invalid map key type func()
    

6.4 結構體

結構體是由多個字段組成的復合數據類型,可以包含不同類型的數據。結構體在Go語言中用于創建自定義的數據類型,方便組織和管理復雜的數據。

定義結構體

使用type關鍵字定義結構體。

基本語法:

type StructName struct {Field1 Type1Field2 Type2// ...
}

示例:

type Person struct {Name stringAge  int
}

嵌入結構體

結構體可以嵌入其他結構體,實現類似繼承的功能。

type Address struct {City    stringZipCode string
}type Employee struct {PersonAddressPosition string
}
結構體實例化

1. 使用字面量

p1 := Person{Name: "Alice", Age: 30}

2. 不指定字段名

p2 := Person{"Bob", 25}

3. 使用new關鍵字

new函數返回指向新分配的零值的指針。

p3 := new(Person)
p3.Name = "Charlie"
p3.Age = 28

4. 部分初始化

未初始化的字段會使用類型的零值。

p4 := Person{Name: "Diana"}
fmt.Println(p4.Age) // 輸出: 0

完整示例:

package mainimport "fmt"// 定義結構體
type Person struct {Name stringAge  int
}func main() {// 使用字面量初始化p1 := Person{Name: "Alice", Age: 30}fmt.Println("p1:", p1)// 不指定字段名p2 := Person{"Bob", 25}fmt.Println("p2:", p2)// 使用new關鍵字p3 := new(Person)p3.Name = "Charlie"p3.Age = 28fmt.Println("p3:", *p3)// 部分初始化p4 := Person{Name: "Diana"}fmt.Println("p4:", p4)
}

輸出:

p1: {Alice 30}
p2: {Bob 25}
p3: {Charlie 28}
p4: {Diana 0}
嵌套結構體

結構體可以嵌入其他結構體,實現數據的層次化管理。

package mainimport "fmt"// 定義Address結構體
type Address struct {City    stringZipCode string
}// 定義Person結構體
type Person struct {Name    stringAge     intAddress Address
}func main() {p := Person{Name: "Eve",Age:  35,Address: Address{City:    "New York",ZipCode: "10001",},}fmt.Println("Person:", p)fmt.Println("City:", p.Address.City)
}

輸出:

Person: {Eve 35 {New York 10001}}
City: New York

匿名嵌入結構體

通過匿名字段,可以直接訪問嵌套結構體的字段,類似于繼承。

package mainimport "fmt"// 定義Address結構體
type Address struct {City    stringZipCode string
}// 定義Person結構體,匿名嵌入Address
type Person struct {Name stringAge  intAddress
}func main() {p := Person{Name: "Frank",Age:  40,Address: Address{City:    "Los Angeles",ZipCode: "90001",},}fmt.Println("Person:", p)fmt.Println("City:", p.City) // 直接訪問嵌套結構體的字段
}

輸出:

Person: {Frank 40 {Los Angeles 90001}}
City: Los Angeles
方法與結構體

Go語言支持為結構體類型定義方法,使得結構體更具行為性。

1. 定義方法

方法是在特定類型上定義的函數。通過方法,可以操作結構體的字段。

基本語法:

func (receiver StructType) MethodName(params) returnTypes {// 方法體
}

示例:

package mainimport "fmt"// 定義結構體
type Rectangle struct {Width, Height float64
}// 定義方法計算面積
func (r Rectangle) Area() float64 {return r.Width * r.Height
}// 定義方法計算周長
func (r Rectangle) Perimeter() float64 {return 2*(r.Width + r.Height)
}func main() {rect := Rectangle{Width: 10, Height: 5}fmt.Println("面積:", rect.Area())         // 輸出: 面積: 50fmt.Println("周長:", rect.Perimeter())   // 輸出: 周長: 30
}

2. 方法的接收者

接收者可以是值類型或指針類型。使用指針接收者可以修改結構體的字段,避免復制整個結構體。

示例:

package mainimport "fmt"// 定義結構體
type Counter struct {count int
}// 值接收者方法
func (c Counter) Increment() {c.count++fmt.Println("Inside Increment (value receiver):", c.count)
}// 指針接收者方法
func (c *Counter) IncrementPointer() {c.count++fmt.Println("Inside IncrementPointer (pointer receiver):", c.count)
}func main() {c := Counter{count: 10}c.Increment() // 修改的是副本fmt.Println("After Increment:", c.count) // 輸出: 10c.IncrementPointer() // 修改的是原始值fmt.Println("After IncrementPointer:", c.count) // 輸出: 11// 使用指針變量cp := &ccp.IncrementPointer()fmt.Println("After cp.IncrementPointer:", c.count) // 輸出: 12
}

輸出:

Inside Increment (value receiver): 11
After Increment: 10
Inside IncrementPointer (pointer receiver): 11
After IncrementPointer: 11
Inside IncrementPointer (pointer receiver): 12
After cp.IncrementPointer: 12

3. 方法的作用

方法可以提供結構體的行為和操作,增強代碼的可讀性和可維護性。例如,可以為結構體定義打印、驗證、計算等功能。

示例:驗證結構體字段

package mainimport ("fmt""errors"
)// 定義結構體
type User struct {Username stringEmail    stringAge      int
}// 定義方法驗證User
func (u *User) Validate() error {if u.Username == "" {return errors.New("用戶名不能為空")}if u.Email == "" {return errors.New("郵箱不能為空")}if u.Age < 0 || u.Age > 150 {return errors.New("年齡不合法")}return nil
}func main() {user := User{Username: "john_doe",Email:    "john@example.com",Age:      28,}if err := user.Validate(); err != nil {fmt.Println("驗證失敗:", err)} else {fmt.Println("用戶信息合法")}// 測試不合法的用戶invalidUser := User{Username: "",Email:    "invalid@example.com",Age:      200,}if err := invalidUser.Validate(); err != nil {fmt.Println("驗證失敗:", err) // 輸出: 驗證失敗: 用戶名不能為空} else {fmt.Println("用戶信息合法")}
}

輸出:

用戶信息合法
驗證失敗: 用戶名不能為空
注意事項
  • 接收者的選擇:根據方法是否需要修改結構體的字段,選擇值接收者或指針接收者。一般情況下,使用指針接收者可以避免復制結構體,提升性能,且可以修改結構體的字段。

    // 修改結構體字段
    func (p *Person) SetName(name string) {p.Name = name
    }
    
  • 方法的命名:方法名應簡潔明了,能夠清晰描述方法的功能。例如,CalculateAreaPrintDetails等。

  • 方法與函數的區別:方法是與特定類型相關聯的函數,而函數是獨立的。合理使用方法可以提升代碼的可讀性和組織性。

6.5 指針與結構體

指針是存儲變量內存地址的變量。在Go語言中,指針與結構體結合使用,可以提高程序的性能,避免大量數據的復制,同時實現對結構體的修改和共享。

指針基礎

1. 聲明指針

使用*符號聲明指針類型。

var p *int

解釋:

  • p是一個指向int類型的指針,初始值為nil

2. 獲取變量的地址

使用&符號獲取變量的內存地址。

a := 10
p := &a
fmt.Println("a的地址:", p) // 輸出: a的地址: 0xc0000140b0

3. 解引用指針

使用*符號訪問指針指向的值。

fmt.Println("p指向的值:", *p) // 輸出: p指向的值: 10

4. 修改指針指向的值

通過指針修改變量的值。

*p = 20
fmt.Println("修改后的a:", a) // 輸出: 修改后的a: 20

完整示例:

package mainimport "fmt"func main() {var a int = 10var p *int = &afmt.Println("變量a的值:", a)         // 輸出: 10fmt.Println("指針p的地址:", p)       // 輸出: a的地址fmt.Println("指針p指向的值:", *p)     // 輸出: 10// 修改指針指向的值*p = 30fmt.Println("修改后的a:", a)         // 輸出: 30
}

輸出:

變量a的值: 10
指針p的地址: 0xc0000140b0
指針p指向的值: 10
修改后的a: 30
指針與結構體

將指針與結構體結合使用,可以避免復制整個結構體,尤其是當結構體較大時,提高程序的性能。此外,通過指針,可以在函數中修改結構體的字段。

1. 定義結構體并使用指針

package mainimport "fmt"// 定義結構體
type Person struct {Name stringAge  int
}func main() {p := Person{Name: "Alice", Age: 25}fmt.Println("原始結構體:", p) // 輸出: {Alice 25}// 獲取結構體的指針ptr := &p// 修改指針指向的結構體字段ptr.Age = 26fmt.Println("修改后的結構體:", p) // 輸出: {Alice 26}
}

2. 結構體指針作為函數參數

通過將結構體指針作為函數參數,可以在函數中修改結構體的字段,而無需返回修改后的結構體。

package mainimport "fmt"// 定義結構體
type Rectangle struct {Width, Height float64
}// 定義函數,接受結構體指針并修改字段
func Resize(r *Rectangle, width, height float64) {r.Width = widthr.Height = height
}func main() {rect := Rectangle{Width: 10, Height: 5}fmt.Println("原始矩形:", rect) // 輸出: {10 5}Resize(&rect, 20, 10)fmt.Println("修改后的矩形:", rect) // 輸出: {20 10}
}

3. 指針與方法接收者

前面章節中提到方法接收者可以是指針類型,這樣可以在方法中修改結構體的字段。

package mainimport "fmt"// 定義結構體
type Counter struct {count int
}// 定義指針接收者方法
func (c *Counter) Increment() {c.count++
}func main() {c := Counter{count: 0}fmt.Println("初始計數:", c.count) // 輸出: 0c.Increment()fmt.Println("計數 after Increment:", c.count) // 輸出: 1// 使用指針變量cp := &ccp.Increment()fmt.Println("計數 after cp.Increment:", c.count) // 輸出: 2
}

輸出:

初始計數: 0
計數 after Increment: 1
計數 after cp.Increment: 2
指針的高級用法

1. 指針與切片

切片本身是一個引用類型,包含指向底層數組的指針。可以通過指針修改切片元素。

package mainimport "fmt"func main() {s := []int{1, 2, 3}ptr := &s// 修改切片元素(*ptr)[1] = 20fmt.Println("修改后的切片:", s) // 輸出: [1 20 3]
}

2. 指針與Map

Map是引用類型,使用指針傳遞Map不會帶來額外的性能開銷。通常不需要使用指針傳遞Map,但在某些情況下可以提高靈活性。

package mainimport "fmt"func main() {capitals := make(map[string]string)capitals["中國"] = "北京"capitals["美國"] = "華盛頓"modifyMap(&capitals)fmt.Println("修改后的Map:", capitals) // 輸出: map[中國:北京 美國:紐約]
}func modifyMap(m *map[string]string) {(*m)["美國"] = "紐約"
}

3. 指針數組

數組中可以存儲指針類型的元素,適用于需要引用和共享數據的場景。

package mainimport "fmt"func main() {a, b, c := 1, 2, 3ptrArr := []*int{&a, &b, &c}for i, ptr := range ptrArr {fmt.Printf("ptrArr[%d] 指向的值: %d\n", i, *ptr)}// 修改通過指針數組修改原始變量*ptrArr[0] = 10fmt.Println("修改后的a:", a) // 輸出: 10
}

輸出:

ptrArr[0] 指向的值: 1
ptrArr[1] 指向的值: 2
ptrArr[2] 指向的值: 3
修改后的a: 10
注意事項
  • 指針的零值:未初始化的指針為nil。在使用指針前,確保其已被正確初始化,避免運行時錯誤。

    var p *int
    // fmt.Println(*p) // 運行時錯誤: invalid memory address or nil pointer dereference
    
  • 避免懸掛指針:確保指針指向的變量在指針使用期間保持有效,避免指針指向已經釋放或超出作用域的變量。

    func getPointer() *int {x := 10return &x
    }func main() {p := getPointer()fmt.Println(*p) // 不安全:x已經超出作用域,可能導致未定義行為
    }
    
  • 使用指針優化性能:對于大型結構體,使用指針傳遞可以避免復制整個結構體,提高性能。

    type LargeStruct struct {Data [1000]int
    }func process(ls LargeStruct) { // 復制整個結構體// ...
    }func processPointer(ls *LargeStruct) { // 傳遞指針// ...
    }
    
  • nil指針檢查:在使用指針前,最好檢查指針是否為nil,以避免運行時錯誤。

    if p != nil {fmt.Println(*p)
    } else {fmt.Println("指針為nil")
    }
    

6.6 組合與接口(拓展內容)

雖然用戶沒有列出組合與接口,在數據結構與集合章節中,了解結構體的組合以及接口的使用也是非常重要的。因此,這里提供對組合和接口的簡要介紹。

組合(Composition)

組合是通過嵌入一個結構體到另一個結構體中,實現代碼復用和功能擴展的一種方式。通過組合,可以創建復雜的數據結構,同時保持代碼的簡潔和模塊化。

示例:

package mainimport "fmt"// 定義基本結構體
type Address struct {City    stringZipCode string
}// 定義復合結構體,通過組合Address
type Person struct {Name    stringAge     intAddress // 組合
}func main() {p := Person{Name: "Grace",Age:  28,Address: Address{City:    "San Francisco",ZipCode: "94105",},}fmt.Printf("Person: %+v\n", p)fmt.Println("City:", p.City) // 直接訪問組合結構體的字段
}

輸出:

Person: {Name:Grace Age:28 Address:{City:San Francisco ZipCode:94105}}
City: San Francisco

優勢:

  • 代碼復用:通過組合,可以復用已有的結構體,減少重復代碼。
  • 靈活性:組合比繼承更靈活,避免了繼承帶來的復雜性。
接口(Interface)

接口定義了一組方法簽名,任何實現了這些方法的類型都滿足該接口。接口提供了多態性,使得代碼更加靈活和可擴展。

示例:

package mainimport "fmt"// 定義接口
type Greeter interface {Greet(name string) string
}// 定義實現接口的結構體
type EnglishGreeter struct{}func (eg EnglishGreeter) Greet(name string) string {return "Hello, " + name + "!"
}type ChineseGreeter struct{}func (cg ChineseGreeter) Greet(name string) string {return "你好," + name + "!"
}func main() {var g Greeterg = EnglishGreeter{}fmt.Println(g.Greet("Alice")) // 輸出: Hello, Alice!g = ChineseGreeter{}fmt.Println(g.Greet("Bob"))   // 輸出: 你好,Bob!
}

輸出:

Hello, Alice!
你好,Bob!

解釋:

  • Greeter接口定義了一個Greet方法。
  • EnglishGreeterChineseGreeter結構體實現了Greet方法,滿足Greeter接口。
  • 通過接口類型變量g,可以調用不同實現的Greet方法,實現多態性。

接口的優勢:

  • 解耦合:通過接口,可以將代碼模塊之間的依賴解耦,提高代碼的靈活性和可維護性。
  • 多態性:同一接口可以由不同類型實現,允許不同的對象以統一的方式被處理。
  • 可擴展性:無需修改現有代碼,只需實現新的接口即可擴展功能。
注意事項
  • 接口隱式實現:在Go語言中,類型只需實現接口的方法,不需要顯式聲明實現關系。這種隱式實現提高了代碼的靈活性和簡潔性。

    type Reader interface {Read(p []byte) (n int, err error)
    }type MyReader struct{}func (r MyReader) Read(p []byte) (n int, err error) {// 實現Read方法return 0, nil
    }func main() {var r Readerr = MyReader{}
    }
    
  • 空接口(interface{}:空接口可以表示任何類型,是實現通用數據結構和函數的重要工具。

    func printAnything(a interface{}) {fmt.Println(a)
    }func main() {printAnything(100)printAnything("Hello")printAnything(true)
    }
    

    輸出:

    100
    Hello
    true
    
  • 類型斷言和類型切換:在使用接口時,可能需要進行類型斷言或類型切換,以訪問具體類型的方法或字段。

    類型斷言示例:

    func main() {var i interface{} = "Go Language"s, ok := i.(string)if ok {fmt.Println("字符串長度:", len(s))} else {fmt.Println("不是字符串類型")}
    }
    

    類型切換示例:

    func main() {var i interface{} = 3.14switch v := i.(type) {case int:fmt.Println("整數:", v)case float64:fmt.Println("浮點數:", v)case string:fmt.Println("字符串:", v)default:fmt.Println("未知類型")}
    }
    

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/908189.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/908189.shtml
英文地址,請注明出處:http://en.pswp.cn/news/908189.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

3. 簡述node.js特性與底層原理

&#x1f63a;&#x1f63a;&#x1f63a; 一、Node.js 底層原理&#xff08;簡化版&#xff09; Node.js 是一個 基于 Chrome V8 引擎構建的 JavaScript 運行時&#xff0c;底層核心由幾部分組成&#xff1a; 組成部分簡要說明 1.V8 引擎 將 JS 編譯成機器碼執行&#xff0…

Web開發主流前后端框架總結

&#x1f5a5; 一、前端主流框架 前端框架的核心是提升用戶界面開發效率&#xff0c;實現高交互性應用。當前三大主流框架各有側重&#xff1a; React (Meta/Facebook) 核心特點&#xff1a;采用組件化架構與虛擬DOM技術&#xff08;減少真實DOM操作&#xff0c;優化渲染性能&…

大語言模型備案與深度合成算法備案的區別與聯系

“什么情況下做算法備案&#xff1f;” “什么情況下做大模型備案呢&#xff1f;” 進行大模型備案的企業必然要進行算法備案&#xff0c;而進行算法備案的企業則需根據其提供的服務性質判斷是否需要進行大模型備案。 算法備案與大模型備案已經是個老生常談的話題了&#xf…

微軟PowerBI考試 PL300-Power BI 入門

Power BI 入門 上篇更新了微軟PowerBI考試 PL-300學習指南&#xff0c;今天分享PowerBI入門學習內容。 簡介 Microsoft Power BI 是一個完整的報表解決方案&#xff0c;通過開發工具和聯機平臺提供數據準備、數據可視化、分發和管理。 Power BI 可以從使用單個數據源的簡單…

【Hive入門】

之前實習寫的筆記&#xff0c;上傳留個備份。 1. 使用docker-compose快速搭建Hive集群 使用docker快速配置Hive環境 拉取鏡像 2. Hive數據類型 隱式轉換&#xff1a;窄的可以向寬的轉換顯式轉換&#xff1a;cast 3. Hive讀寫文件 SerDe:序列化&#xff08;對象轉為字節碼…

設計模式——簡單工廠模式(創建型)

摘要 本文主要介紹了簡單工廠模式&#xff0c;包括其定義、結構、實現方式、適用場景、實戰示例以及思考。簡單工廠模式是一種創建型設計模式&#xff0c;通過工廠類根據參數決定創建哪一種產品類的實例&#xff0c;封裝了對象創建的細節&#xff0c;使客戶端無需關心具體類的…

抽象工廠模式與策略模式結合使用小案例

目錄 1.前言1.示例說明1.1定義通用接口1.2 定義抽象工廠1.3 支付寶實現1.4 微信實現1.5 客戶端使用代碼&#xff08;組合使用&#xff09;1.6 示例結果輸出1.7 總結 1.前言 上一篇章就通過簡單的案例來了解抽象工廠模式和策略模式的使用&#xff0c;現在就用個支付場景的小案例…

通過WiFi無線連接小米手機攝像頭到電腦的方法

通過WiFi無線連接小米手機攝像頭到電腦的方法 以下是基于Scrcpy和DroidCam兩種工具的無線連接方案&#xff0c;需提前完成開發者模式與USB調試的開啟&#xff08;參考原教程步驟&#xff09;&#xff1a; 方法一&#xff1a;Scrcpy無線投屏&#xff08;無需手機端安裝&#xf…

2025軟件供應鏈安全最佳實踐︱證券DevSecOps下供應鏈與開源治理實踐

項目背景&#xff1a;近年來&#xff0c;云計算、AI人工智能、大數據等信息技術的不斷發展、各行各業的信息電子化的步伐不斷加快、信息化的水平不斷提高&#xff0c;網絡安全的風險不斷累積&#xff0c;金融證券行業面臨著越來越多的威脅挑戰。特別是近年以來&#xff0c;開源…

Java高級 | 【實驗二】Springboot 控制器類+相關注解知識

隸屬文章&#xff1a; Java高級 | &#xff08;二十二&#xff09;Java常用類庫-CSDN博客 系列文章&#xff1a; Java高級 | 【實驗一】Spring Boot安裝及測試 最新-CSDN博客 目錄 一、MVC模式 二、SpringBoot基礎——控制層Controller詳解 &#xff08;一&#xff09;主要工…

MySQL 事務深度解析:面試核心知識點與實戰

&#x1f91f;致敬讀者 &#x1f7e9;感謝閱讀&#x1f7e6;笑口常開&#x1f7ea;生日快樂?早點睡覺 &#x1f4d8;博主相關 &#x1f7e7;博主信息&#x1f7e8;博客首頁&#x1f7eb;專欄推薦&#x1f7e5;活動信息 文章目錄 Java 中 MySQL 事務深度解析&#xff1a;面試…

【趣味Html】第11課:動態閃爍發光粒子五角星

打造炫酷的動態閃爍發光粒子五角星效果 前言 在現代Web開發中&#xff0c;視覺效果的重要性不言而喻。今天我們將深入探討如何使用HTML5 Canvas和JavaScript創建一個令人驚艷的動態閃爍發光粒子五角星效果。這個項目不僅展示了Canvas的強大功能&#xff0c;還涉及了粒子系統、…

6.RV1126-OPENCV 形態學基礎膨脹及腐蝕

一.膨脹 1.膨脹原理 膨脹的本質就是通過微積分的轉換&#xff0c;將圖像A和圖形B進行卷積操作合并成一個AB圖像。核就是指任意的形狀或者大小的圖形B。例如下圖&#xff0c;將核(也就是圖形B)通過微積分卷積&#xff0c;和圖像A合并成一個圖像AB。 2.特點 圖像就會更加明亮 …

機器學習實戰37-基于情感字典和機器學習的股市輿情分析可視化系統

文章目錄 一、項目背景數字時代情感分析情況二、項目流程1.數據采集與預處理2.復合情感分析模型構建3.輿情分析可視化:三、機器學習算法原理1.支持向量機基礎2.核函數與高維映射3.情感分類特征融合4.模型訓練與優化四、實現代碼五、系統特點與優勢1.復合情感分析模型2.多維度可…

STM32F407VET6學習筆記9:編譯輸出固定大小.bin文件

今日學習如何輸出固定大小的.bin編譯文件 目錄 Keil_V5 fromelf.exe 軟件目錄&#xff1a; 魔棒添加命令輸出bin文件&#xff1a; 輸出固定大小的bin文件&#xff1a; 計算bin文件大小&#xff1a; 安裝 SRecord 工具集&#xff1a; 使用SRecord&#xff1a; 參考文章&#…

【Web應用】若依框架:基礎篇14 源碼閱讀-后端代碼分析

文章目錄 ?前言?一、課程講解?總結 標題詳情作者JosieBook頭銜CSDN博客專家資格、阿里云社區專家博主、軟件設計工程師博客內容開源、框架、軟件工程、全棧&#xff08;,NET/Java/Python/C&#xff09;、數據庫、操作系統、大數據、人工智能、工控、網絡、程序人生口號成為你…

Java 單例模式詳解

目錄 1. 餓漢式&#xff08;Eager Initialization&#xff09; 2. 懶漢式&#xff08;Lazy Initialization&#xff09; 3. 懶漢式 同步鎖&#xff08;線程安全&#xff09; 4. 雙重檢查鎖&#xff08;Double-Checked Locking&#xff09; 5. 靜態內部類&#xff08;推薦…

從 AMQP 到 RabbitMQ:核心組件設計與工作原理(一)

一、引言 ** 在當今分布式系統盛行的時代&#xff0c;消息隊列作為一種關鍵的中間件技術&#xff0c;承擔著系統間異步通信、解耦和削峰填谷的重要職責。AMQP&#xff08;Advanced Message Queuing Protocol&#xff09;作為一種高級消息隊列協議&#xff0c;為消息隊列的實現…

概率單純形(Probability Simplex)

目錄 定義性質在統計學中的應用在機器學習中的應用在信息論中的應用在優化問題中的應用在其他領域的應用 定義 定義&#xff1a;在數學中&#xff0c;概率單純形&#xff08;Probability Simplex&#xff09;是指在 n n n維空間中&#xff0c;所有分量非負且分量之和為1的向量…

項目練習:Vue2中el-button上的@click事件失效

文章目錄 一、問題描述二、解決 一、問題描述 button按鈕上綁定了一個click事件 對應的方法寫在methods中 但是&#xff0c;測試點擊時&#xff0c;無法觸發函數 二、解決 1、問題代碼 <el-buttonclick"changeConfirm(Y)"type"success"plainicon&qu…