筆記 - Golang 進階

[筆記] Golang 進階

函式 ( Function )

為甚麼要使用函式

  1. 可重複使用性 ( Reusability ) :

    • 不須重複撰寫功能相似的程式碼
    • 宣告一個函式後可重複使用
  2. 抽象化 ( Abstraction ) :

    • 使用時不須特別了解函式的詳細內容
    • 使用時只需知道需要的參數與此函次的功能
    • 依函式命名,可以清楚了解主程式碼在做甚麼

函式的參數與回傳值

  1. 參數

    1
    2
    3
    4
    func foo (x int, y int)
    {
    fmt.Print(x * y)
    }
    1
    2
    3
    4
    func foo()
    {
    fmt.Print("Hello")
    }
    • 函式可以有參數傳入,也可以不用

      1
      2
      3
      4
      func foo (x, y int)
      {
      fmt.Print(x * y)
      }
    • 如果傳入的參數型別相同,也可以這樣寫

  2. 回傳值

    1
    2
    3
    4
    5
    6
    func foo(x int) int
    {
    return x + 1
    }

    y := foo(1)
    • 在函式後宣告要回傳的值是甚麼型別

      1
      2
      3
      4
      5
      6
      func foo2(x int) (int, int)
      {
      return x, x + 1
      }

      a, b := foo2(3)
    • 跟很多語言不同的是,在 Golang 可以回傳多種型別的值,如果回傳的值用不到,可以用 _,代表丟棄該回傳值

傳值 ( call by value ) 與傳參考 ( call by reference )

  1. 傳值

    • 傳參數時,是將資料複製後再傳給函式
    • 在函式做更動,並不會影響原本的參數
1
2
3
4
5
6
7
8
9
10
func foo(y int)
{
y = y + 1
}
func main()
{
x := 2
foo(x)
fmt.Print(x)
}
  • x 依舊沒變,還是 2

    • 優點 : 在函式內不會影響外層的資料
    • 缺點 : 如果傳入的物件較大,會花費較長的複製時間
  1. 傳參考

    • 傳入函式時,是傳送指標
    • 在呼叫函式時,會直接指派該變數的位置
1
2
3
4
5
6
7
8
9
10
func foo(y *int)
{
*y = *y + 1
}
func main()
{
x := 2
foo(&x)
fmt.Print(x)
}
  • x 會是 3

    • 優點 : 不需要複製參數的時間
    • 缺點 : 會改變外層變數的資料

傳遞 Arrays 與 Slices 參數

1
2
3
4
5
6
7
8
9
func foo(x [3]int) int
{
return x[0] + 1
}
func main()
{
a := [3]int{1, 2, 3}
fmt.Print(foo(a))
}
  • 傳遞 Array,但當 Array 很大時,會導致效能變慢
  • 此時可以使用傳參考的方式,如下 :
1
2
3
4
5
6
7
8
9
10
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,如下 :
1
2
3
4
5
6
7
8
9
10
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. 邏輯錯誤
    • 為了方便除錯

      • 函式必須要有可讀性
      • 資料必須是方便追蹤的,全域變數追蹤就較複雜

撰寫方法

  • 有意義的名稱

    • 一看函式名稱就知道此函式的大概功能
    • 參數命名使人明瞭此參數帶甚麼資料
1
2
func ProcessArray(a []int) float {} //無意義的名稱
func ComputeRMS(samples []int) float {} //有意義的名稱
  • 函式內聚 ( Functional cohesion )

    • 每種函式最好只有一種功能
    • 每個函式要有獨立性
    • 可以不會去影響其他的函式
1
2
PointDist(), DrawCircle(), TriangleArea() //每個函式有自己的功能
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
        4
        func a() 
        {
        <100 lines>
        }
      • 也許可以寫成 :

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        func a()
        {
        b()
        c()
        }

        func b()
        {
        <50 lines>
        }

        func c()
        {
        <50 lines>
        }
  • 降低控制元複雜性 ( Control-flow complexity )

    • 階層函式可以降低此複雜性
    • 假設 :

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      func foo()
      {
      if a == 1
      {
      if b == 1
      {
      ...
      }
      }
      ...
      }
    • 可以寫成 :

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      func 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
      12
      var 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
      13
      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))
      }
    • 可以被作為函式的返回值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      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 )

1
2
3
4
5
6
7
8
9
10
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 )

    • 被包裹在內層的區塊可以保護自己的變數不被外層取用,相反的外層區塊的變數還是可以被內層區塊使用
  • 引用環境包含,定義函式的區塊內,所定義的名稱

1
2
3
4
5
6
var x int
func foo(y int)
{
z := 1
...
}
  • foo 可以看到 xyz

閉包 ( Closure )

  • 函式 + 引用環境
  • 當函式被傳遞或當成返回值時,他們的引用環境會跟著傳送
1
2
3
4
5
6
7
8
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_xo_y

參數個數可變的函式( Variadic function )

  • 使用 ... 表示此函示可以接受零個或零個以上的參數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 會先被呼叫
1
2
3
4
5
func main()
{
defer fmt.Println("Bye!")
fmt.Println("Hello!")
}
  • 最後才輸出 Bye!
1
2
3
4
5
6
7
func main()
{
i := 1
defer fmt.Println(i + 1)
i++
fmt.Println("Hello!")
}
  • 會先輸出 hello 在輸出 2,因為跟變數運算無關,所以 i++ 是最後執行的

物件導向 ( Object orientation )

Class

1
2
3
4
5
type Point struct
{
x float64
y float64
}
  • Go 裡面並沒有 Class,在 Go 裡必須用 Struct 替代
1
2
3
4
5
6
7
8
9
10
11
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 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 裡,xy 是私有的,首字大寫為公有,小寫為私有
1
2
3
4
5
6
7
8
9
10
11
package main

import data

func main()
{
var p data.Point
p.InitMe(3, 4)
p.Scale(2)
p.PrintMe()
}
  • 可以調用 function,但無法直接 p.xp.y

傳參考的用意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 很大時,傳值的複製會使效能降低
1
2
3
4
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

      1
      2
      3
      4
      5
      type Shape2D interface
      {
      Area() float64
      Perimeter() float64
      }
    • Triangle

      • 此為實體類型 ( Concrete type )
      • 是可以另外增加函數的
      • 在 Go 裡並不需要說明 Triangle 是屬於 Shape2D Interface
      • 只要定義 Interface,定義 Triangle 和 Triangle 的函式,就會自動匹配
      1
      2
      3
      type Triangle {...}
      func (t Triangle) Area() float64 {...}
      func (t Triangle) Perimeter() float64 {...}
    • 使用 Interface

      • 假如有個院子需要蓋一座泳池
      • 泳池形狀不定,但需要有適合的面積與周長
      • FitInYard() 函式裡使用 Interface
      1
      2
      3
      4
      5
      6
      7
      8
      func 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
      13
      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 進行判斷
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      func DrawShape(s Shape2D)
      {
      switch := sh := s.(type)
      {
      case Rectangle:
      DrawRect(sh)
      case Triangle:
      DrawTri(sh)
      }
      }
  • 空介面 ( Empty Interface )

    • 沒有定義任何函式
    • 所有型別都能滿足他
    • 可以用來給接受任何型別的函式使用
1
2
3
4
func PrintMe(val interface{})
{
fmt.Println(val)
}

Interface 與 Concrete type

  • Interface :

    • 為抽象類型
    • 定義了函式簽名
    • 具體實現方法都是抽象的
  • Concrete type :

    • 為實體類型
    • 定義了函式與資料
    • 包含實現函數的完整程式碼

介面變數 ( Interface value )

  • 跟其他變數一樣

    • 可以被指定變數
    • 可以被傳送或傳回
  • 包含兩個元件 :

    • 動態型別 ( Dymamic Type ) : 此 Interface 的 Concrete type
    • 動態變數 ( Dymamic Value ) : 動態型別的變數
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

1
2
3
4
type error interface
{
Error() string
}
  • 此為一個內建的 Interface
  • 正確時,error 為 nil
  • 錯誤時,Error() 會輸出錯誤訊息
1
2
3
4
5
6
f, err := os.Open("/harris/test.txt")
if err != nil
{
fmt.Println(err.Error())
return
}
  • 使用 Error Interface
tags: 筆記 程式語言 Golang
Author: Kenny Li
Link: https://kennyliblog.nctu.me/2019/08/20/Golang-advanced/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.