# [筆記] Golang 進階 ### 函式 ( Function ) #### 為甚麼要使用函式 1. 可重複使用性 ( Reusability ) : * 不須重複撰寫功能相似的程式碼 * 宣告一個函式後可重複使用 2. 抽象化 ( Abstraction ) : * 使用時不須特別了解函式的詳細內容 * 使用時只需知道需要的參數與此函次的功能 * 依函式命名,可以清楚了解主程式碼在做甚麼 #### 函式的參數與回傳值 1. 參數 ```go= func foo (x int, y int) { fmt.Print(x * y) } ``` ```go= func foo() { fmt.Print("Hello") } ``` * 函式可以有參數傳入,也可以不用 ```go= func foo (x, y int) { fmt.Print(x * y) } ``` * 如果傳入的參數型別相同,也可以這樣寫 2. 回傳值 ```go= func foo(x int) int { return x + 1 } y := foo(1) ``` * 在函式後宣告要回傳的值是甚麼型別 ```go= func foo2(x int) (int, int) { return x, x + 1 } a, b := foo2(3) ``` * 跟很多語言不同的是,在 Golang 可以回傳多種型別的值,如果回傳的值用不到,可以用 `_`,代表丟棄該回傳值 #### 傳值 ( call by value ) 與傳參考 ( call by reference ) 1. 傳值 * 傳參數時,是將資料複製後再傳給函式 * 在函式做更動,並不會影響原本的參數 ```go= func foo(y int) { y = y + 1 } func main() { x := 2 foo(x) fmt.Print(x) } ``` * `x` 依舊沒變,還是 2 * 優點 : 在函式內不會影響外層的資料 * 缺點 : 如果傳入的物件較大,會花費較長的複製時間 2. 傳參考 * 傳入函式時,是傳送指標 * 在呼叫函式時,會直接指派該變數的位置 ```go= func foo(y *int) { *y = *y + 1 } func main() { x := 2 foo(&x) fmt.Print(x) } ``` * `x` 會是 3 * 優點 : 不需要複製參數的時間 * 缺點 : 會改變外層變數的資料 #### 傳遞 Arrays 與 Slices 參數 ```go= func foo(x [3]int) int { return x[0] + 1 } func main() { a := [3]int{1, 2, 3} fmt.Print(foo(a)) } ``` * 傳遞 Array,但當 Array 很大時,會導致效能變慢 * 此時可以使用傳參考的方式,如下 : ```go= func foo(*x [3]int) int { (*x)[0] = (*x)[0] + 1 } func main() { a := [3]int{1, 2, 3} foo(&a) fmt.Print(a) } ``` * 但在 Go 這方法是麻煩且不必要的 * 所以在需要傳遞 Array 時,盡量改成傳遞 Slice,如下 : ```go= func foo(sli []int) int { sli[0] = sli[0] + 1 } func main() { a := []int{1, 2, 3} foo(a) fmt.Print(a) } ``` ### 改善撰寫函式的方法 #### 好的函式 * 可讀性 ( Understandability ) * 能快速找到某功能的程式碼 * 能知道變數、資料從哪裡來 * 除錯原則 ( Debugging principls ) * 兩種錯誤原因 1. 語法錯誤 2. 邏輯錯誤 * 為了方便除錯 * 函式必須要有可讀性 * 資料必須是方便追蹤的,全域變數追蹤就較複雜 #### 撰寫方法 * 有意義的名稱 * 一看函式名稱就知道此函式的大概功能 * 參數命名使人明瞭此參數帶甚麼資料 ```go= func ProcessArray(a []int) float {} //無意義的名稱 func ComputeRMS(samples []int) float {} //有意義的名稱 ``` * 函式內聚 ( Functional cohesion ) * 每種函式最好只有一種功能 * 每個函式要有獨立性 * 可以不會去影響其他的函式 ```go= PointDist(), DrawCircle(), TriangleArea() //每個函式有自己的功能 DrawCircle() + TriangleArea() //寫在一起不是好的寫法,容易互相影響 ``` * 函式耦合 ( Functional coupling ) * 兩個函數有關係 ( 使用全域變數或接受另一個函數傳入的參數 ) 就稱為耦合 * 耦合度高容易牽一髮動全身,影響原本功能正常的函數 * 所以要保持一種原則 : **提高內聚力,降低偶合度** * 少量的參數 * 在追蹤資料來源時方便許多 * 可能是函式內聚不優所導致,`DrawCircle() + TriangleArea()` 就需要不同的參數 * 降低參數數量 * 可以用 struct 組合起來 * 以 `TriangleArea()` 為例 * 需要以 3 點來描述三角形 * 而每一點在 3D 裡擁有 3 個座標 ( x, y, z ) * 這樣就必須傳遞 9 個參數 ```go= type Point struct {x, y, z float} ``` * 假如這樣組合起來,一個 `Point` 裡包含 3 個座標 * 那就只需要傳遞 3 個參數就好 * 降低函式複雜性 ( Function complexity ) * 函式長度盡量減短 * 階層函式 ( Function call hierarchy ) * 假設有個 `a` 函式 ```go= func a() { <100 lines> } ``` * 也許可以寫成 : ```go= func a() { b() c() } func b() { <50 lines> } func c() { <50 lines> } ``` * 降低控制元複雜性 ( Control-flow complexity ) * 階層函式可以降低此複雜性 * 假設 : ```go= func foo() { if a == 1 { if b == 1 { ... } } ... } ``` * 可以寫成 : ```go= func foo() { if a == 1 { CheckB() } ... } func CheckB() { if b == 1 { ... } } ``` ### 函式型別 ( Function type ) * 為第一類物件 ( First-class value ) * 變數可被當成函式型別來宣告 * 可以被存入變數或其他結構 ```go= var funcVar func(int) int func incFn(x int) int { return x + 1 } func main() { funcVar = incFn fmt.Print(funcVar(1)) } ``` * 可以被作為參數傳遞給其他函式 ```go= func applyIt(afunct func(int) int, val int) int { return afunct(val) } func incFn(x int) int {return x + 1} func decFn(x int) int {return x - 1} func main() { fmt.Println(applyIt(incFn, 2)) fmt.Println(applyIt(decFn, 2)) } ``` * 可以被作為函式的返回值 ```go= func MakeDistOrigin(o_x, o_y float64) func (float64, float64) float64 { fn := func(x, y float64) float64 { return math.Sqrt(math.Pow(x - o_x, 2) + math.Pow(y - o_y, 2)) } return fn } func main() { Dist1 := MakeDistOrigin(0, 0) Dist2 := MakeDistOrigin(2, 2) fmt.Println(Dist1(2, 2)) fmt.Println(Dist2(2, 2)) } ``` * 可被動態建立 #### 匿名函式 ( Anonymous function ) ```go= func applyIt(afunct func(int) int, val int) int { return afunct(val) } func main() { v := applyIt(func(x int) int {return x + 1}, 2) fmt.Println(v) } ``` * 不必為函式取名 #### 函式的引用環境 ( Environment of a function ) * 函式內所有有效名稱 * 函式內定義的名稱 * 語彙範疇 ( Lexical Scope ) * 被包裹在內層的區塊可以保護自己的變數不被外層取用,相反的外層區塊的變數還是可以被內層區塊使用 * 引用環境包含,定義函式的區塊內,所定義的名稱 ```go= var x int func foo(y int) { z := 1 ... } ``` * `foo` 可以看到 `x`、`y`、`z` #### 閉包 ( Closure ) * 函式 + 引用環境 * 當函式被傳遞或當成返回值時,他們的引用環境會跟著傳送 ```go= func MakeDistOrigin(o_x, o_y float64) func (float64, float64) float64 { fn := func(x, y float64) float64 { return math.Sqrt(math.Pow(x - o_x, 2) + math.Pow(y - o_y, 2)) } return fn } ``` * `fn()` 的閉包擁有 `o_x`、`o_y` #### 參數個數可變的函式( Variadic function ) * 使用 `...` 表示此函示可以接受零個或零個以上的參數 ```go= func getMax(vals ...int) int { maxV := -1 for _, v := range vals { if v > maxV { maxV = v } } return maxV } func main() { fmt.Println(getMax(1, 3, 6, 4)) vslice := []int{1, 3, 6, 4} fmt.Println(getMax(vslice...)) } ``` * 可傳送零個或零個以上的參數,也能傳送 Slice,須加上 `...` #### Deferred function * 呼叫會延遲直到其他函式呼叫完畢 * 通常用於釋放資源 * 如有兩個以上的`defer`,越後面的 `defer` 會先被呼叫 ```go= func main() { defer fmt.Println("Bye!") fmt.Println("Hello!") } ``` * 最後才輸出 `Bye!` ```go= func main() { i := 1 defer fmt.Println(i + 1) i++ fmt.Println("Hello!") } ``` * 會先輸出 `hello` 在輸出 `2`,因為跟變數運算無關,所以 `i++` 是最後執行的 ### 物件導向 ( Object orientation ) #### Class ```go= type Point struct { x float64 y float64 } ``` * Go 裡面並沒有 Class,在 Go 裡必須用 Struct 替代 ```go= func (p Point) DistToOrig() { t := math.Pow(p.x, 2) + math.Pow(p.y, 2) return math.Sqrt(t) } func main() { p1 := Point(3, 4) fmt.Println(p1.DisToOrig()) } ``` * Struct 配合 function #### 封裝 ( Encapsulation ) ```go= package data type Point struct { x float64 y float64 } func (p *Point) InitMe(xn, yn float64) { p.x = xn p.y = yn } func (p *Point) Scale(v float64) { p.x = p.x * v p.y = p.y * v } func (p *Point) PrintMe() { fmt.Println(p.x, p.y) } ``` * 在 package data 裡,`x`、`y` 是私有的,首字大寫為公有,小寫為私有 ```go= package main import data func main() { var p data.Point p.InitMe(3, 4) p.Scale(2) p.PrintMe() } ``` * 可以調用 function,但無法直接 `p.x`、`p.y` #### 傳參考的用意 ```go= type Point struct { x float64 y float64 } func (p Point) OffsetX(v float64) { p.x = p.x + v } func main() { p1 := Point(3, 4) p1.OffsetX(5) } ``` * 這樣傳值是無法將 `p1.x` 改成 8 的,因為傳值是將變數複製後傳入的 * 另外如果當 struct 很大時,傳值的複製會使效能降低 ```go= func (p *Point) OffsetX(v float64) { p.x = p.x + v } ``` * 如果需要修改到物件的值,就必須傳參考了 * 所以有兩個原因來使用此 Point receiver * 需要修改值 * 當 struct 很大時,防止每次拷貝降低效能 #### 多型 ( Polymorphism ) * 指相同的函式呼叫介面,傳送給一個物件變數,可以有不同的行為,例如 : * `Area()` 函式 * Rectangle : base * height * Triangle : 0.5 * base * height * 通常用繼承來實現 #### 繼承 ( Inheritance ) * Go 裡面並沒有繼承 * 指子類別 ( subclass ) 會繼承父類別 ( superclass ) 的函式與資料,例如 : * 父類別 : Speaker 擁有 `Speak()` 函式 * 子類別 : Cat、Dog 也一樣擁有 `Speak()` 函式 #### 覆寫 ( Overriding ) * 指子類別可以重新定義從父類別繼承過來的函式,會以子類別為主,例如 : * 父類別 : Speaker 的 `Speak()` 函式 * `prints "<noise>"` * 子類別 : Cat、Dog 的 `Speak()` 函式 * `prints "meow"` * `prints "woof"` * 此時 `Speak()` 就是多型的特性 ### 介面 ( Interface ) * 可以實現多型、繼承、覆寫 * 為抽象類型 * 定義了只有函式簽名 ( method signatures ) 的函式,並沒有實現功能的程式碼 * 函式簽名 : 是一個函式的函式名、參數列表、返回類型的統稱 * 例如 : * Interface : Shape2D ```go= type Shape2D interface { Area() float64 Perimeter() float64 } ``` * Triangle * 此為實體類型 ( Concrete type ) * 是可以另外增加函數的 * **在 Go 裡並不需要說明 Triangle 是屬於 Shape2D Interface** * 只要定義 Interface,定義 Triangle 和 Triangle 的函式,就會自動匹配 ```go= type Triangle {...} func (t Triangle) Area() float64 {...} func (t Triangle) Perimeter() float64 {...} ``` * 使用 Interface * 假如有個院子需要蓋一座泳池 * 泳池形狀不定,但需要有適合的面積與周長 * 在 `FitInYard()` 函式裡使用 Interface ```go= func FitInYard(s Shape2D) bool { if(s.Area() > 100 && s.Perimeter() > 100) { return true } return false } ``` * 斷言型別 ( Type Assertion ) * 假如要實現一個畫出圖形的函式 `DrawShape()` * 而我們必須判斷這個 Interface 是什麼型別 ```go= func DrawShape(s Shape2D) { rect, ok := s.(Rectangle) //若 Interface 有此 Concrete type,ok 就會是 true if ok { DrawRect(rect) } tri, ok := s.(Triangle) if ok { DrawTri(tri) } } ``` * Type Switch * 配合 Switch 進行判斷 ```go= func DrawShape(s Shape2D) { switch := sh := s.(type) { case Rectangle: DrawRect(sh) case Triangle: DrawTri(sh) } } ``` * 空介面 ( Empty Interface ) * 沒有定義任何函式 * 所有型別都能滿足他 * 可以用來給接受任何型別的函式使用 ```go= func PrintMe(val interface{}) { fmt.Println(val) } ``` #### Interface 與 Concrete type * Interface : * 為抽象類型 * 定義了函式簽名 * 具體實現方法都是抽象的 * Concrete type : * 為實體類型 * 定義了函式與資料 * 包含實現函數的完整程式碼 #### 介面變數 ( Interface value ) * 跟其他變數一樣 * 可以被指定變數 * 可以被傳送或傳回 * 包含兩個元件 : * 動態型別 ( Dymamic Type ) : 此 Interface 的 Concrete type * 動態變數 ( Dymamic Value ) : 動態型別的變數 ```go= type Speaker interface {Speak()} type Dog struct {name string} func (d Dog) Speak() { fmt.Println(d.name) } func main() { var s1 Speaker var d1 Dog{"Brian"} s1 = d1 s1.Speak() } ``` * 動態型別為 `Dog`,動態變數為 `d1` ```go= func (d *Dog) Speak() { if d == nil { fmt.Println("<noise>") } else { fmt.Println(d.name) } } func main() { var s1 Speaker var d1 *Dog s1 = d1 s1.Speak() } ``` * 介面變數在沒有動態變數的情況下也是可以呼叫的 * 但在函式中必須做判斷,不然會報錯 * 可以沒有動態變數,但在沒有動態型別的情況下是不能呼叫的 #### Error Interface ```go= type error interface { Error() string } ``` * 此為一個內建的 Interface * 正確時,`error` 為 nil * 錯誤時,`Error()` 會輸出錯誤訊息 ```go= f, err := os.Open("/harris/test.txt") if err != nil { fmt.Println(err.Error()) return } ``` * 使用 Error Interface ###### tags: `筆記` `程式語言` `Golang`