1. 程式人生 > >Go內部培訓——節點解析21-30

Go內部培訓——節點解析21-30

21. Go 並行通道Channel

  • Channel是連線並行協程(goroutine)的通道。你可以向一個通道寫入資料然後從另外一個通道讀取資料。
package main
import "fmt"
func main() {
// 使用`make(chan 資料型別)`來建立一個Channel
// Channel的型別就是它們所傳遞的資料的型別
messages := make(chan string)
// 使用`channel <-`語法來向一個Channel寫入資料
// 這裡我們從一個新的協程向messages通道寫入資料ping
go func() { messages <-
"ping" }() // 使用`<-channel`語法來從Channel讀取資料 // 這裡我們從main函式所在的協程來讀取剛剛寫入 // messages通道的資料 msg := <-messages fmt.Println(msg) }

22. Go常量

  • Go支援定義字元常量,字串常量,布林型常量和數值常量。
package main
import "fmt"
import "math"
// "const" 關鍵字用來定義常量
const s string = "constant"
func main() {
fmt.Println(s)
// "const"關鍵字可以出現在任何"var"關鍵字出現的地方
// 區別是常量必須有初始值 const n = 500000000 // 常量表達式可以執行任意精度數學計算 const d = 3e20 / n fmt.Println(d) // 數值型常量沒有具體型別,除非指定一個型別 // 比如顯式型別轉換 fmt.Println(int64(d)) // 數值型常量可以在程式的邏輯上下文中獲取型別 // 比如變數賦值或者函式呼叫。 // 例如,對於math包中的Sin函式,它需要一個float64型別的變數 fmt.Println(math.Sin(n)) }

23. Go 超時

  • 超時對那些連線外部資源的程式來說是很重要的,否則就需要限定執行時間。在Go裡面實現超時很簡單。我們可以使用channel和select很容易地做到。
package main
import "time"
import "fmt"
func main() {
// 在這個例子中,假設我們執行了一個外部呼叫,2秒之後將結果寫入c1
c1 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c1 <- "result 1"
}()
// 這裡使用select來實現超時,`res := <-c1`等待通道結果,
// `<- Time.After`則在等待1秒後返回一個值,因為select首先
// 執行那些不再阻塞的case,所以這裡會執行超時程式,如果
// `res := <-c1`超過1秒沒有執行的話
select {
case res := <-c1:
fmt.Println(res)
case <-time.After(time.Second * 1):
fmt.Println("timeout 1")
}
// 如果我們將超時時間設為3秒,這個時候`res := <-c2`將在
// 超時case之前執行,從而能夠輸出寫入通道c2的值
c2 := make(chan string, 1)
go func() {
time.Sleep(time.Second * 2)
c2 <- "result 2"
}()
select {
case res := <-c2:
fmt.Println(res)
case <-time.After(time.Second * 3):
fmt.Println("timeout 2")
}
}

24. Go 錯誤處理

  • 在Go裡面通常採用顯式返回錯誤程式碼的方式來進行錯誤處理。這個和Java或者Ruby裡面使用異常或者是C裡面執行正常返回結果,發生錯誤返回錯誤程式碼的方式不同。Go的這種錯誤處理的方式使得我們能夠很容易看出哪些函式可能返回錯誤,並且能夠像呼叫那些沒有錯誤返回的函式一樣呼叫。
package main
import "errors"
import "fmt"
// Go語言裡面約定錯誤程式碼是函式的最後一個返回值,
// 並且型別是error,這是一個內建的介面
func f1(arg int) (int, error) {
if arg == 42 {
// errors.New使用錯誤資訊作為引數,構建一個基本的錯誤
return -1, errors.New("can't work with 42")
}
// 返回錯誤為nil表示沒有錯誤
return arg + 3, nil
}
// 你可以通過實現error介面的方法Error()來自定義錯誤
// 下面我們自定義一個錯誤型別來表示上面例子中的引數錯誤
type argError struct {
arg int
prob string
}
func (e *argError) Error() string {
return fmt.Sprintf("%d - %s", e.arg, e.prob)
}
func f2(arg int) (int, error) {
if arg == 42 {
// 這裡我們使用&argError語法來建立一個新的結構體物件,
// 並且給它的成員賦值
return -1, &argError{arg, "can't work with it"}
}
return arg + 3, nil
}
func main() {
// 下面的兩個迴圈例子用來測試我們的帶有錯誤返回值的函式
// 在for迴圈語句裡面,使用了if來判斷函式返回值是否為nil是
// Go語言裡面的一種約定做法。
for _, i := range []int{7, 42} {
if r, e := f1(i); e != nil {
fmt.Println("f1 failed:", e)
} else {
fmt.Println("f1 worked:", r)
}
}
for _, i := range []int{7, 42} {
if r, e := f2(i); e != nil {
fmt.Println("f2 failed:", e)
} else {
fmt.Println("f2 worked:", r)
}
}
// 如果你需要使用自定義錯誤型別返回的錯誤資料,你需要使用型別斷言
// 來獲得一個自定義錯誤型別的例項才行。
_, e := f2(42)
if ae, ok := e.(*argError); ok {
fmt.Println(ae.arg)
fmt.Println(ae.prob)
}
}

25. Go 打點器

  • Timer是讓你等待一段時間然後去做一件事情,這件事情只會做一次。而Ticker是讓你按照一定的時間間隔迴圈往復地做一件事情,除非你手動停止它。
package main
import "time"
import "fmt"
func main() {
// Ticker使用和Timer相似的機制,同樣是使用一個通道來發送資料。
// 這裡我們使用range函式來遍歷通道資料,這些資料每隔500毫秒被
// 傳送一次,這樣我們就可以接收到
ticker := time.NewTicker(time.Millisecond * 500)
go func() {
for t := range ticker.C {
fmt.Println("Tick at", t)
}
}()
// Ticker和Timer一樣可以被停止。一旦Ticker停止後,通道將不再
// 接收資料,這裡我們將在1500毫秒之後停止
time.Sleep(time.Millisecond * 1500)
ticker.Stop()
fmt.Println("Ticker stopped")
}

26. Go 遞迴函式

  • Go語言支援遞迴函式,這裡是一個經典的斐波拉切數列的列子。

package main
import "fmt"
// fact函式不斷地呼叫自身,直到達到基本狀態fact(0)
func fact(n int) int {
if n == 0 {
return 1
}
return n * fact(n-1)
}
func main() {
fmt.Println(fact(7))

27. Go 讀取檔案

  • 讀寫檔案是很多程式的基本任務,下面我們看看Go裡面的檔案讀取。
package main
import (
"bufio"
"fmt"
"io"
"io/ioutil"
"os"
)
// 讀取檔案的函式呼叫大多數都需要檢查錯誤,
// 使用下面這個錯誤檢查方法可以方便一點
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// 最基本的檔案讀寫任務就是把整個檔案的內容讀取到記憶體
// 最基本的檔案讀寫任務就是把整個檔案的內容讀取到記憶體
dat, err := ioutil.ReadFile("/tmp/dat")
check(err)
fmt.Print(string(dat))
// 有的時候你想更多地控制到底是讀取檔案的哪個部分,這個
// 時候你可以使用`os.Open`開啟一個檔案獲取一個`os.File`
// 物件
f, err := os.Open("/tmp/dat")
// 從這個檔案中讀取一些位元組,並且由於位元組陣列長度所限,
// 最多讀取5個位元組,另外還需要注意實際能夠讀取的位元組
// 數量
b1 := make([]byte, 5)
n1, err := f.Read(b1)
check(err)
fmt.Printf("%d bytes: %s\n", n1, string(b1))
// 你也可以使用`Seek`來跳轉到檔案中的一個已知位置,並從
// 那個位置開始讀取資料
o2, err := f.Seek(6, 0)
check(err)
b2 := make([]byte, 2)
n2, err := f.Read(b2)
check(err)
fmt.Printf("%d bytes @ %d: %s\n", n2, o2, string(b2))
// `io`包提供了一些幫助檔案讀取的函式。例如上面的方法如果
// 使用方法`ReadAtLeast`函式來實現,將使得程式更健壯
o3, err := f.Seek(6, 0)
check(err)
b3 := make([]byte, 2)
n3, err := io.ReadAtLeast(f, b3, 2)
check(err)
fmt.Printf("%d bytes @ %d: %s\n", n3, o3, string(b3))
// 沒有內建的rewind方法,但是可以使用`Seek(0,0)`來實現
_, err = f.Seek(0, 0)
check(err)
// `bufio`包提供了緩衝讀取檔案的方法,這將使得檔案讀取更加
// 高效
r4 := bufio.NewReader(f)
b4, err := r4.Peek(5)
check(err)
fmt.Printf("5 bytes: %s\n", string(b4))
// 最後關閉開啟的檔案。一般來講這個方法會在開啟檔案的時候,
// 使用defer來延遲關閉
f.Close()
}

28. Go 方法

  • 一般的函式定義叫做函式,定義在結構體上面的函式叫做該結構體的方法。
package main
import "fmt"
type rect struct {
width, height int
}
// 這個area方法有一個限定型別*rect,
// 表示這個函式是定義在rect結構體上的方法
func (r *rect) area() int {
return r.width * r.height
}
// 方法的定義限定型別可以為結構體型別
// 也可以是結構體指標型別
// 區別在於如果限定型別是結構體指標型別
// 那麼在該方法內部可以修改結構體成員資訊
func (r rect) perim() int {
return 2*r.width + 2*r.height
}
func main() {
r := rect{width: 10, height: 5}
// 呼叫方法
fmt.Println("area: ", r.area())
fmt.Println("perim:", r.perim())
// Go語言會自動識別方法呼叫的引數是結構體變數還是
// 結構體指標,如果你要修改結構體內部成員值,那麼使用
// 結構體指標作為函式限定型別,也就是說引數若是結構體
//變數,僅僅會發生值拷貝。
rp := &r
fmt.Println("area: ", rp.area())
fmt.Println("perim:", rp.perim())
}

29. Go 工作池

  • 我們來看一下如何使用gorouotine和channel來實現工作池。
package main
import "fmt"
import "time"
// 我們將在worker函式裡面執行幾個並行例項,這個函式從jobs通道
// 裡面接受任務,然後把執行結果傳送到results通道。每個job我們
// 都休眠一會兒,來模擬一個耗時任務。
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Println("worker", id, "processing job", j)
time.Sleep(time.Second)
results <- j * 2
}
}
func main() {
// 為了使用我們的工作池,我們需要傳送工作和接受工作的結果,
// 這裡我們定義兩個通道,一個jobs,一個results
jobs := make(chan int, 100)
results := make(chan int, 100)
// 這裡啟動3個worker協程,一開始的時候worker阻塞執行,因為
// jobs通道里面還沒有工作任務
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// 這裡我們傳送9個任務,然後關閉通道,告知任務傳送完成
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
// 然後我們從results裡面獲得結果
for a := 1; a <= 9; a++ {
<-results
}

30. Go 關閉通道

  • 關閉通道的意思是該通道將不再允許寫入資料。這個方法可以讓通道資料的接受端知道資料已經全部發送完成了。
package main
import "fmt"
// 在這個例子中,我們使用通道jobs在main函式所在的協程和一個數據
// 接收端所在的協程通訊。當我們資料傳送完成後,我們關閉jobs通道
func main() {
jobs := make(chan int, 5)
done := make(chan bool)
// 這裡是資料接收端協程,它重複使用`j, more := <-jobs`來從通道
// jobs獲取資料,這裡的more在通道關閉且通道中不再有資料可以接收的
// 時候為false,我們通過判斷more來決定所有的資料是否已經接收完成。
// 如果所有資料接收完成,那麼向done通道寫入true
go func() {
for {
j, more := <-jobs
if more {
fmt.Println("received job", j)
} else {
fmt.Println("received all jobs")
done <- true
return
}
}
}()
// 這裡向jobs通道寫入三個資料,然後關閉通道
for j := 1; j <= 3; j++ {
jobs <- j
fmt.Println("sent job", j)
}
close(jobs)
fmt.Println("sent all jobs")
// 我們知道done通道在接收資料的時候會阻塞,所以在所有的資料傳送
// 接收完成後,寫入done的資料將在這裡被接收,然後程式結束。
<-done
}