容器
陣列和切片
在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]
}
其他操作和陣列基本一樣,下面再說下陣列和切片的區別:
- 長度:
- 陣列的長度是固定的,定義後不能改變。
- 切片的長度是動態的,可以透過append函式增加元素。
- 靈活性:
- 陣列在使用上較為僵化,因為長度固定,適用於元素數量已知且固定的場景。
- 切片更加靈活,適用於需要動態新增或刪除元素的場景。
- 效能:
- 陣列的訪問速度通常比切片快,因為它們是固定大小的,編譯器可以進行更多的最佳化。
- 切片在效能上稍遜,但由於其靈活性,使用更加廣泛。
container包
在Go語言的標準庫中,container包提供了三種常見的資料結構:堆(heap)、雙向連結串列(list)和環形佇列(ring)。這些資料結構為開發者提供了高效的插入、刪除和訪問操作。下面我們詳細介紹這三個資料結構及其用法。
head
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)
}
常見問題
- 避免在接收端關閉通道:通常由傳送方負責關閉channel。
- 避免重複關閉通道:多次關閉同一個channel會導致panic。
- 避免從未使用的通道傳送和接收:未使用的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")
}