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