# [筆記] 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`