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