# Golang 筆記 [TOC] ## Go 的設計理念 > 主要以解決實務工程問題,而非設計出一個在語法機制上很漂亮的語言。 ## Go 的特性 * open source * 靜態型別的編譯語言;語法類似腳本語言 * 跨平台 * 內建 GC (可手動調整觸發時機) * 內建 concurrency * 內建 functional programming * lightweight object system * coding style 強制統一 * 快速編譯 * 豐富的標準函式庫 * 內建相關開發工具 * compiler * 套件管理 (package management) * 語法重排 (syntax formatting) * 語法檢查 (syntax checking) ## Go 受批評的特質 * 缺乏泛型 * 缺乏函式重載 * 缺乏運算子重載 ## Go 的使用情境 > 網頁程式和其他伺服器程式是 Go 的強項,是目前最適合的開發項目 (sweet spot)。 ## Go 程式 ### 執行 > 使用 go run 執行後,Go 編譯器會偷偷在系統暫存區建立相對應的執行檔;在未修改程式碼時,第二次執行的速度會比較快。 ``` cmd go run main.go ``` ### 組成 > 每個 Go 程式都要在一開始設置套件 (package) 名稱,套件名稱實際上就是 namespace 的概念;套件的命名規則,application 一律採用 main 這個套件名稱,library 則開法者自訂。 ``` go // Set current package. package main // Import some package. import "fmt" // Enter main function. func main() { // Print out string `"Hello World"` on stand output. fmt.Println("Hello World") } ``` ### 專案的命名模式 > Go 專案不需要專案設定檔,完全依靠專案的內隱知識來自動設置專案;Go 編譯器利用根目錄的名稱來自動命名編譯出來的命令列工具,故可省略專案設定檔。 * 在建立函式庫套件時,我們會讓專案目錄名稱和套件名稱一致,套件使用者才不會搞混。 ### 專案架構 #### GOROOT > 路徑內存放的是 Go 語言內建的工具,我們不應該去動該路徑內的東西,以免造成 Go 開發軟體的毀損。 * Go 編譯器 * Go 相關工具 * 標準函式庫 #### GOPATH > 路徑內存放的第三方應用程式和函式庫;自己所開發的 Go 程式。 * bin :存放執行檔 * lib :存放二進胃函式庫檔 * src :存放 Go 專案原始碼 ## Go 資料型別 ### 數字 > 另外有提供大數函式庫,由於大數是用軟體模擬的,會比內建來的慢。 #### 複數 * complex64 * complex128 ### 字串 > 在預設情形下,應該優先使用 string 型別,除非有明確的理由,才會使用另外兩種型別。 * string : 是以 UTF-8 編碼來處理的字串,string 的值視為單值而非字元陣列。 * byte : 則保持字串原始的內容,不處理編碼。 * rune : 則是將字串以 Unicode code point 切開時所用的型別。 ### Array 和 Slice > 兩者的差別主要在於陣列的長度是固定的,而切片可以伸縮長度。 ### Channel > 通道用於共時性 (concurrency) 程式中,在 goroutine 之間傳遞資料。 ## Go Variable ### 識別字規範 * 第一個字母須是字母或底線 * 不可使用保留字 > 使用單一底線做為識別字時,代表捨棄接收到的值。 ``` go package main import ( "fmt" "log" ) func main() { _, err := fmt.Println("Hello World") if err != nil { log.Fatal(err) } } ``` ### 保留字 > golang 的保留字目前只有 25個 ![](https://i.imgur.com/osbDPWE.png) ### 識別字撰碼 * 當識別字使用 PascalCase 時,表示該識別字是公開的 (public)。 * 當使用 camelCase 時,表示該識別字是私有的 (private)。 ### 型別轉換 > 在 Go 中不能直接把不同型別的資料相結合,例如整數和浮點數不能直接相加。 ## 使用 defer 優雅地處理系統資源 > defer 敘述會自動延遲到 defer 所在的函式結束時才觸發指令,不需要手動控制程式流程。 ``` go f, err := os.Create("file.txt") if err != nil { log.Fatal(err) } defer f.Close() ``` ## 使用 Array 和 Slice ### Array > 使用迭代器時,對陣列元素的修改是沒有效果的。 ``` go package main import "fmt" func main() { arr := [5]int{1, 2, 3, 4, 5} for _, e := range arr { e = e * e } for _, e := range arr { fmt.Println(e) } } ``` ### Slice > 切片的內部,其實也是陣列,切片本身不儲存值,而是儲存到陣列的參考 (reference),簡單地說,切片和陣列內部儲存同一份資料,但透過兩個不同的變數來處理。 * 可在 Runtime 動態產生,這時須使用 make 關鍵字 ``` go package main import ( "fmt" ) func main() { slice := make([]int, 5) for i := 0; i < len(slice); i++ { n := i + 1 slice[i] = n * n } for _, e := range slice { fmt.Println(e) } } ``` ## 使用 Map > 要注意的是,Map 是無序的,我們多執行幾次,就會發現每次的順序都不一樣,見下例。 ``` go package main import ( "fmt" ) func main() { m := make(map[string]string) m["Go"] = "Beego" m["Python"] = "Django" m["Ruby"] = "Rails" m["PHP"] = "Laravel" for i := 0; i < 10; i++ { for k, v := range m { fmt.Println(fmt.Sprintf("%s: %s", k, v)) } fmt.Println("") } } ``` ## 使用指標 > 指標本身存的值是指向另一個值的記憶體位置 (memory address),我們通常不會直接使用指標的值,而會透過指標間接操作另一個值。 ### 動態配置記憶體 ``` go package main import ( "fmt" ) type Point struct { x float64 y float64 } func main() { p := new(Point) p.x = 3.0 p.y = 4.0 fmt.Println(fmt.Sprintf("(%.2f, %.2f)", p.x, p.y)) } ``` > make 函式其實也是動態分配記憶體的一種語法,但 make 回傳的不是指標,而是該型別本身。 ## 撰寫函式 ### 多個回傳值 > Go 函式允許多個回傳值,多回傳值時常用於錯誤處理 (error handling) 。 ``` go package main import ( "log" ) func divmod(a int, b int) (int, int) { return a / b, a % b } func main() { m, n := divmod(5, 3) if !(m == 1) { log.Fatal("Wrong value m") } if !(n == 2) { log.Fatal("Wrong value n") } } ``` ### 不定長度參數 > 不定參數傳入函式後,參數本身是一個切片,在函式中透過此切片即可取得個別的參數值。 ``` go package main import ( "log" ) func sum(args ...float64) float64 { sum := 0.0 for _, e := range args { sum += e } return sum } func main() { s := sum(1, 2, 3, 4, 5) if !(s == 15.0) { log.Fatal("Wrong value") } } ``` ### 預設變數、函式重載 > Go 的函式本身不支援預設變數;Go 也不支援函式重載。 ### 傳值呼叫 > Go 的所有函式都是傳值呼叫 (call by value),即使是傳遞指標,也是拷貝指標的位址,但不會拷貝指標所指向的值;當值很大時,傳遞指標比傳整個值有效率。 ### init 函式 > init 函式是一個特殊的函式,若程式碼內有 init 會在程式一開始執行的時候呼叫該函式,順序在 main 函式之前。 ## 使用 Class 和 Object > 嚴格來說,Go 只能撰寫基於物件的程式 (object-based programming),無法撰寫物件導向程式 (object-oriented programming),因為 Go 僅支援一部分的物件導向特性,像是 Go 不支援繼承。 ### 靜態方法 (Static Method) > 由於 Go 語言沒有將物件導向的概念直接加在語法中,不需要用這種語法,直接用頂層函式即可。 ### 使用嵌入 (Embedding) 取代繼承 (Inheritance) >我們重用 Point 的方法,再加入 Point3D 特有的方法,實際上的效果等同於繼承; >然而,Point 和 Point3D 兩者在類別關係上卻是不相干的獨立物件。 >在 Go 語言中,需要使用介面 (interface) 來解決這個議題。 ### 用介面 (Interface)實踐繼承和多型 > 只有方法宣告,但缺乏方法實作的型別。 ``` go type IPoint interface { X() float64 Y() float64 SetX(float64) SetY(float64) } ``` * 嵌入是介面用來繼承介面的手法 #### 多型 >若 Point 類別實作 IPoint 介面,而 Point3D 內嵌 Point,則符合多型。 ``` go type IPoint interface { X() float64 Y() float64 } type Point struct { x float64 y float64 } func (p *Point) X() float64 { return p.x } func (p *Point) Y() float64 { return p.y } type Point3D struct { Point z float64 } func (p3 *Point3D) Z() float64 { return p3.z } func main() { points:=make([]IPoint,0) p1:=&Point { } p2:=&Point3D { } points = append(points, p1, p2) } ``` #### 用空介面當萬用型別 > 寫做 interface{},空介面也是一種特殊型別,該型別可放入任何值;基本上,空介面和介面使用的時機不同,要將其視為兩種不同的概念。 ## Funtional Programming ### 函式物件 > 函數式程式的基本前提是函式為一級物件,簡單地說,函式也是值。 ### 閉包 > 帶有狀態的函式,稱為閉包 (closure)。 ### 嚴格求值 (Strict Evaluation) > 給函式的實際參數總是在應用這個函式之前求值;主流的程式語言大抵上都是 strict evaluation。 ``` go //此範例會錯誤 package main import ( "fmt" ) func main() { fmt.Println(len([]int{1, 2, 3/0, 4})) } ``` ## 錯誤處理 > 在 Go 中,錯誤物件是一個 interface。 ### 一般錯誤 > 拋出錯誤物件,交由後續的程式自行處理 * 回傳一般錯誤物件 ``` go package main import( "fmt" "errors" ) func main(){ err:= errors.New("something error") if err != nil { fmt.Println(err) } fmt.Println("More message") } ``` * 回傳布林值 ``` go package main import( "log" ) func main(){ m := map[int]string{ 1: "one", 2: "two", 3: "three", } v, ok := m[1] if !ok { log.Fatal("Unable to retrieve key/value pair") } if !(v == "one") { log.Fatal("Wrong value") } } ``` ### 嚴重錯誤 > 引發 panic 事件,將程式提早結束;如果想要從嚴重錯誤中回復,要用 recover 函式,並且要搭配特定的語法。濫用 recover 會使得程式可讀性較差,不會將其常態性使用。 ``` go package main import ( "fmt" ) func main() { defer func() { err := recover() if err != nil { fmt.Println("Recover from panic") } }() panic("Some error") // It didn't occur. fmt.Println("More message") } ``` ## 撰寫共時性 (Concurrency) 程式 ### goroutine 是輕量級執行緒 (lightweight thread) > Go 程式以 goroutine 做為並行執行的程式碼區塊,goroutine 類似於執行緒,但更輕量,一次啟動數百甚至數千個以上的 goroutine 也不會占用太多記憶體;並時性程式和傳統的循序式程式的思維不太一樣,執行並時性程式時無法保證程式運行的先後順序,需注意。 * waitGroup : 一般來說 gorotine 和主程式並時執行,若沒有使用,會在主程式結束時即提早結束。 ``` go package main import ( "log" "os" "sync" ) func main() { // A goroutine-safe console printer. logger := log.New(os.Stdout, "", 0) // Sync between goroutines. var wg sync.WaitGroup // Add goroutine 1. wg.Add(1) go func() { defer wg.Done() logger.Println("Print from goroutine 1") }() // Add goroutine 2. wg.Add(1) go func() { defer wg.Done() logger.Println("Print from goroutine 2") }() logger.Println("Print from main") // Wait all goroutines. wg.Wait() } ``` ### 利用 channel 在 goroutine 間傳遞資料 > Go 用 channel 在不同並行程式間傳遞資料;由於通道在傳輸時,會阻塞 (blocking) 程式的行進,在此處,我們不需要另外設置 WaitGroup。 ``` go package main import "fmt" func main() { // Create a channel message := make(chan string) // Init a goroutine. go func() { // Send some data into the channel. message <- "Hello from channel" }() // Receive the data from the channel. msg := <-message fmt.Println(msg) } ``` #### channel 緩衝 >在 goroutine 數量小於 buffered 大小時,可以直接傳送資料,無需等待。 ``` go package main import ( "log" "os" "sync" ) func main() { // A goroutine-safe console printer. logger := log.New(os.Stdout, "", 0) // Sync among all goroutines. var wg sync.WaitGroup // Make a buffered channel. ch := make(chan int, 10) for i := 1; i <= 10; i++ { ch <- i wg.Add(1) go func() { defer wg.Done() logger.Println("Print from goroutine ", <-ch) }() } logger.Println("Print from main") wg.Wait() } ``` #### 關閉 channel > 若不用 channel 時,可用 close 函式將 channel 關閉。 ``` go package main import "fmt" func main() { ch := make(chan int, 4) ch <- 2 ch <- 4 close(ch) // ch <- 6 // panic, send on closed channel fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch) // closed, returns zero value for element } ``` ### 使用 select 敘述在多個 channel 間做選擇 > 透過 select,我們可以在多個 channel 中做選擇。 ``` go package main import "time" import "fmt" func main() { // For our example we'll select across two channels. c1 := make(chan string) c2 := make(chan string) // Each channel will receive a value after some amount // of time, to simulate e.g. blocking RPC operations // executing in concurrent goroutines. go func() { time.Sleep(time.Second * 1) c1 <- "one" }() go func() { time.Sleep(time.Second * 1) c2 <- "two" }() // We'll use `select` to await both of these values // simultaneously, printing each one as it arrives. for i := 0; i < 2; i++ { select { case msg1 := <-c1: fmt.Println("received", msg1) case msg2 := <-c2: fmt.Println("received", msg2) } } } ``` #### 利用 mutex 將共時性程式同步化 > Go 也提供較傳統的 Mutex;在共時性程式中,mutex 會將某一段程式暫時鎖住,避免 race condition。 ### 參考 [為什麼使用共時性](https://kennyliblog.nctu.me/2019/10/08/Golang-concurrency/#%E7%82%BA%E4%BB%80%E9%BA%BC%E4%BD%BF%E7%94%A8%E5%85%B1%E6%99%82%E6%80%A7)