【建議收藏】Go語言關鍵知識點總結

LingBrown發表於2024-06-30

容器

陣列和切片

在Go語言中,陣列和切片是兩個基本的資料結構,用於儲存和操作一組元素。它們有一些相似之處,但也有許多不同之處。下面我們詳細介紹陣列和切片的特點、用法以及它們之間的區別。

陣列

陣列是固定長度的序列,儲存相同型別的元素。陣列的長度在定義時就固定下來,不能改變。

package main

import "fmt"

func main() {
    // 定義一個長度為5的整型陣列
    var arr [5]int
    fmt.Println(arr) // 輸出: [0 0 0 0 0]

    // 定義並初始化一個長度為5的整型陣列
    arr2 := [5]int{1, 2, 3, 4, 5}
    fmt.Println(arr2) // 輸出: [1 2 3 4 5]

    // 讓編譯器推斷陣列長度
    arr3 := [...]int{1, 2, 3}
    fmt.Println(arr3) // 輸出: [1 2 3]
}

可以使用索引來訪問和修改陣列中的元素:

package main

import "fmt"

func main() {
    arr := [3]int{1, 2, 3}
    fmt.Println(arr[0]) // 輸出: 1

    arr[1] = 10
    fmt.Println(arr) // 輸出: [1 10 3]
}

可以使用for迴圈來遍歷陣列:

package main

import "fmt"

func main() {
    arr := [3]int{1, 2, 3}
    for i, v := range arr {
        fmt.Println(i, v)
    }
}

切片

切片是動態陣列,可以按需增長。切片由三個部分組成:指標、長度和容量。指標指向陣列中切片的起始位置,長度是切片中的元素個數,容量是從切片起始位置到陣列末尾的元素個數。

package main

import "fmt"

func main() {
    // 建立一個長度和容量為3的整型切片
    slice := make([]int, 3)
    fmt.Println(slice) // 輸出: [0 0 0]

    // 定義並初始化一個切片
    slice2 := []int{1, 2, 3, 4, 5}
    fmt.Println(slice2) // 輸出: [1 2 3 4 5]
}

切片可以透過陣列或另一個切片生成:

package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    slice := arr[1:4]
    fmt.Println(slice) // 輸出: [2 3 4]
}

可以使用內建的append函式向切片追加元素:

package main

import "fmt"

func main() {
    slice := []int{1, 2, 3}
    slice = append(slice, 4, 5)
    fmt.Println(slice) // 輸出: [1 2 3 4 5]
}

其他操作和陣列基本一樣,下面再說下陣列和切片的區別:

  1. 長度
    • 陣列的長度是固定的,定義後不能改變。
    • 切片的長度是動態的,可以透過append函式增加元素。
  2. 靈活性
    • 陣列在使用上較為僵化,因為長度固定,適用於元素數量已知且固定的場景。
    • 切片更加靈活,適用於需要動態新增或刪除元素的場景。
  3. 效能
    • 陣列的訪問速度通常比切片快,因為它們是固定大小的,編譯器可以進行更多的最佳化。
    • 切片在效能上稍遜,但由於其靈活性,使用更加廣泛。

container包

在Go語言的標準庫中,container包提供了三種常見的資料結構:(heap)、雙向連結串列(list)和環形佇列(ring)。這些資料結構為開發者提供了高效的插入、刪除和訪問操作。下面我們詳細介紹這三個資料結構及其用法。

heap 包實現了堆資料結構。堆是一種特殊的樹狀結構,可以用於實現優先佇列。
要使用 container/heap 包,必須定義一個實現 heap.Interface 介面的型別。該介面包含以下方法:

  • Len() int:返回元素數量。
  • Less(i, j int) bool:報告索引 i 處的元素是否小於索引 j 處的元素。
  • Swap(i, j int):交換索引 i 和 j 處的元素。
  • Push(x interface{}):將元素 x 新增到堆中。
  • Pop() interface{}:移除並返回堆中的最小元素。

這些方法要求使用者明確實現堆的各種操作,增加了使用的複雜度。

package main

import (
    "container/heap"
    "fmt"
)

// 定義一個實現 heap.Interface 的型別
type IntHeap []int

func (h IntHeap) Len() int           { return len(h) }
func (h IntHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h IntHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }

func (h *IntHeap) Push(x interface{}) {
    *h = append(*h, x.(int))
}

func (h *IntHeap) Pop() interface{} {
    old := *h
    n := len(old)
    x := old[n-1]
    *h = old[0 : n-1]
    return x
}

func main() {
    h := &IntHeap{2, 1, 5}
    heap.Init(h)
    heap.Push(h, 3)
    fmt.Printf("最小元素: %d\n", (*h)[0])
    for h.Len() > 0 {
        fmt.Printf("%d ", heap.Pop(h))
    }
    // 輸出: 最小元素: 1
    //      1 2 3 5
}

list

list 包實現了雙向連結串列(doubly linked list)。雙向連結串列允許高效的插入和刪除操作。

package main

import (
    "container/list"
    "fmt"
)

func main() {
    l := list.New()

    // 在連結串列前插入元素
    l.PushFront(1)
    l.PushFront(2)

    // 在連結串列後插入元素
    l.PushBack(3)

    // 遍歷連結串列
    for e := l.Front(); e != nil; e = e.Next() {
        fmt.Println(e.Value)
    }
    // 輸出:
    // 2
    // 1
    // 3
}

ring

ring 包實現了環形佇列(circular list)。環形佇列是一種首尾相連的佇列結構。

package main

import (
    "container/ring"
    "fmt"
)

func main() {
    // 建立一個長度為3的環
    r := ring.New(3)

    // 初始化環中的值
    for i := 0; i < r.Len(); i++ {
        r.Value = i
        r = r.Next()
    }

    // 遍歷環中的元素
    r.Do(func(p interface{}) {
        fmt.Println(p.(int))
    })
    // 輸出:
    // 0
    // 1
    // 2
}

Channel

什麼是Channel

在Go語言中,channel是用於在不同的goroutine之間進行通訊的機制。它可以讓一個goroutine將值傳送到一個通道中,另一個goroutine從通道中接收值。channel的設計使得goroutine之間的通訊和同步變得簡潔而高效。

建立Channel

建立一個channel使用make函式,指定其傳遞的值的型別:

ch := make(chan int)

可以建立帶緩衝的channel,緩衝大小在make時指定:

ch := make(chan int, 100)

傳送和接收

傳送和接收操作使用箭頭符號<-:

ch <- 1   // 傳送值1到channel
value := <-ch  // 從channel接收值並賦值給變數value

關閉Channel

channel可以被主動關閉,關閉channel使用close函式:

close(ch)

一旦一個channel被關閉,再往該channel傳送值會導致panic,從已關閉的channel接收值將立即返回該型別的零值並且不會阻塞(如果通道里還存在未被接收的元素,這些元素也會正常返回,直到所有元素都被接收,才會開始返回零值)。

其他操作

無緩衝通道(緩衝大小為0)

  • 傳送操作會阻塞直到有goroutine來接收這個值。
  • 接收操作會阻塞直到有值被髮送到channel。

緩衝通道

  • 傳送操作會在緩衝區滿時阻塞。
  • 接收操作會在緩衝區為空時阻塞。

Select語句

select語句可以用於處理多個channel操作。它會阻塞直到其中一個channel可以進行操作。select語句中的各個分支是隨機選擇的:

select {
case val := <-ch1:
    fmt.Println("Received", val)
case ch2 <- 1:
    fmt.Println("Sent 1")
default:
    fmt.Println("No communication")
}

示例

基於channel,實現一個簡單的生產者-消費者模型:

package main

import (
    "fmt"
    "time"
)

func producer(ch chan int) {
    //迴圈往通道傳送5個元素,間隔1秒
    for i := 0; i < 5; i++ {
        fmt.Println("Producing", i)
        ch <- i
        time.Sleep(time.Second)
    }
    //傳送完所有訊息後關閉通道
    close(ch)
}

func consumer(ch chan int) {
    //可以透過range遍歷通道的元素
    //因為生產者已經關閉了通道,所以遍歷完所有元素後,迴圈會自己退出
    for val := range ch {
        fmt.Println("Consuming", val)
        time.Sleep(time.Second)
    }
}

func main() {
    ch := make(chan int, 2)
    go producer(ch)
    consumer(ch)
}

常見問題

  1. 避免在接收端關閉通道:通常由傳送方負責關閉channel。
  2. 避免重複關閉通道:多次關閉同一個channel會導致panic。
  3. 避免從未使用的通道傳送和接收:未使用的channel操作會導致死鎖。比如只接收,沒傳送,程式會一直阻塞在接收處。

函式

在Go語言中,函式是一等公民(first-class citizen),這意味著函式可以像其他型別(例如整數、字串等)一樣使用和操作。這一特性使得函式的使用非常靈活和強大。具體來說,函式作為一等公民具有以下特點:

函式可以賦值給變數

你可以將一個函式賦值給一個變數,這樣就可以透過這個變數來呼叫函式:

package main

import "fmt"

func main() {
    add := func(a, b int) int {
        return a + b
    }
    fmt.Println(add(3, 4)) // 輸出: 7
}

函式可以作為引數傳遞給另一個函式

函式可以作為引數傳遞給其他函式,這使得可以實現高階函式:

package main

import "fmt"

func applyOperation(a, b int, op func(int, int) int) int {
    return op(a, b)
}

func main() {
    add := func(a, b int) int {
        return a + b
    }
    result := applyOperation(5, 3, add)
    fmt.Println(result) // 輸出: 8
}

函式可以作為返回值從另一個函式返回

函式可以從另一個函式返回,這使得可以動態生成函式:

package main

import "fmt"

func createMultiplier(factor int) func(int) int {
    return func(x int) int {
        return x * factor
    }
}

func main() {
    double := createMultiplier(2)
    triple := createMultiplier(3)
    fmt.Println(double(4)) // 輸出: 8
    fmt.Println(triple(4)) // 輸出: 12
}

函式可以巢狀定義

在Go語言中,可以在函式內部定義另一個函式:

package main

import "fmt"

func main() {
    outer := func() {
        fmt.Println("This is the outer function.")

        inner := func() {
            fmt.Println("This is the inner function.")
        }

        inner()
    }

    outer()
}

函式可以作為匿名函式

匿名函式是一種無需命名的函式,可以直接使用:

package main

import "fmt"

func main() {
    result := func(a, b int) int {
        return a + b
    }(3, 5)
    
    fmt.Println(result) // 輸出: 8
}

閉包(Closures)

Go語言支援閉包,閉包是一個函式,這個函式可以捕獲並記住其所在環境的變數:

package main

import "fmt"

func main() {
    x := 10

    // 定義一個修改外部變數x的閉包
    closure := func() int {
        x += 1
        return x
    }

    fmt.Println(closure()) // 輸出: 11
    fmt.Println(x)         // 輸出: 11
}

package main

import "fmt"

func main() {
    counter := func() func() int {
        count := 0
        return func() int {
            count++
            return count
        }
    }()
    
    fmt.Println(counter()) // 輸出: 1
    fmt.Println(counter()) // 輸出: 2
    fmt.Println(counter()) // 輸出: 3
}

錯誤處理

Go語言中的錯誤處理方式不同於傳統的異常處理機制。它採用了明確的、基於值的錯誤處理方法。每個函式可以返回一個錯誤值來表示是否出現了問題。

基本錯誤處理

Go語言中使用內建的error介面型別來表示錯誤。error介面定義如下:

type error interface {
    Error() string
}

函式通常返回一個error型別的值來表示操作是否成功。如果沒有錯誤,返回nil。

package main

import (
    "errors"
    "fmt"
)

// 定義一個函式,返回錯誤
func divide(a, b int) (int, error) {
    if b == 0 {
        //如果有問題,透過New方法新建一個錯誤資訊
        return 0, errors.New("division by zero")
    }
    //如果沒有錯誤返回nil
    return a / b, nil
}

func main() {
    result, err := divide(4, 2)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }

    result, err = divide(4, 0)
    if err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Result:", result)
    }
}

自定義錯誤型別

除了使用errors.New建立簡單錯誤外,Go語言允許我們定義自己的錯誤型別,實現更豐富的錯誤資訊。

package main

import (
    "fmt"
)

// 自定義錯誤型別
type MyError struct {
    Code    int
    Message string
}

// 實現error介面的Error方法
func (e *MyError) Error() string {
    return fmt.Sprintf("Code: %d, Message: %s", e.Code, e.Message)
}

// 定義一個函式,返回自定義錯誤(只要實現了Error()方法,就可以直接返回error型別)
func doSomething(flag bool) error {
    if !flag {
        return &MyError{Code: 123, Message: "something went wrong"}
    }
    return nil
}

func main() {
    err := doSomething(false)
    if err != nil {
        fmt.Println("Error:", err)
        
        // 型別斷言,獲取具體的錯誤型別
        if myErr, ok := err.(*MyError); ok {
            fmt.Println("Custom Error Code:", myErr.Code)
        }
    }
}

異常處理機制

Go語言也有類似異常的處理機制,即defer、panic和recover,但它們主要用於處理程式中不可恢復的錯誤。

  • defer:用於延遲執行一個函式,在函式返回前執行。如果一個函式里面有多個defer語句,寫在最後面的defer最先執行。
  • panic:意料之外的錯誤,也可以手動呼叫。如果panic沒有處理,程式會終止。
  • recover:恢復panic,並停止程式終止的過程。
package main

import "fmt"

func main() {
    defer func() {
        //使用defer執行一個匿名函式,確保recover一定能執行
        if r := recover(); r != nil {
            //恢復panic,此處可以進行異常處理,比如列印日誌
            fmt.Println("Recovered from panic:", r)
        }
    }()

    fmt.Println("Starting the program")
    //手動觸發一個panic
    panic("Something went wrong!")
    fmt.Println("This line will not be executed")
}

相關文章