大家有沒有想過,如果我要寫一個把數字呈現給使用者的function,我需要考慮到輸入的數字的資料型態之後再餵到function內,像如果我要轉換資料型態為整數、浮點數(小數)兩種,我就需要寫兩個function。

例如下面這個例子:

package main

import "fmt"

func showInt(num int64) {
	fmt.Println(num)
}

func showFloat(num float64) {
	fmt.Println(num)
}

func main() {
	var sum1 int64 = 28
	var sum2 float64 = 29.5

	showInt(sum1)
	showFloat(sum2)
}
  • 執行結果:

但明明裡面執行的內容都差不多,有沒有一個方法可以把他們合起來寫成一個function呢?答案是有的,既然內容差不多,就把他們寫在同一個function吧,為甚麼這麼執著簡化function呢?這就要講到軟體工程領域中有很多工程師寫code寫到變得有程式潔癖XD,就衍生出了一個DRY原則。

DRY原則

在軟體工程領域中有一個DRY原則,我們可以看看維基百科中是怎麼描述DRY的Don’t repeat yourself,全名叫 “Don’t repeat yourself”。

把可能發生變化的重複資訊,用不太可能發生變化的抽象資訊替代,而泛型就是符合DRY的好方法之一。

來簡單化function吧

呈上一段的程式碼,我們可以用泛型把它簡化~ 讓兩個function統整為一個function,程式碼參考至初探 golang 1.18 generics 功能,原來1.18版的golang才把泛型引入XD

泛型方法

package main

import "fmt"

func show[num int64 | float64](s num) {
	fmt.Println(s)
}

func main() {
	var sum1 int64 = 28
	var sum2 float64 = 29.5

	show(sum1)
	show(sum2)
}
  • 執行結果:

你看~是不是乾淨很多!當然在還是有其他招可以取代泛型,來看看另一種方法-斷言 Assertion。

Assertion方法

package main

import "fmt"

func show(num any) {
	switch s := num.(type) {
	case int64:
		fmt.Println(s)
	case float64:
		fmt.Println(s)
	}
}

func main() {
	fmt.Println("go 1.18 Generics Example")

	var sum1 int64 = 28
	var sum2 float64 = 29.5

	show(sum1)
	show(sum2)
}
  • 執行結果:

其實兩種方法都是重構程式碼的好方法,我自己也另外做了壓力測試,看兩著執行速度會不會差很多,以下是執行結果,我把兩種方各執行了三次,每次執行將他們的show function 各使用了10000次,單位為ns/op,意思為一個指令要執行幾奈秒,只看ns/op看起來斷言方法的效能比較好一點…,那我要泛型到底要幹嘛,笑死。好吧也可能是我測試方法有誤,有甚麼因素沒有考慮到XD

Generic(泛型)

(1)
1000000000               0.9029 ns/op
PASS
ok      example1        56.259s
(2)
1000000000               0.8784 ns/op
PASS
ok      example1        64.238s
(3)
1000000000               0.9074 ns/op
PASS
ok      example1        63.307s

Assertion(斷言)

(1)
1000000000               0.8786 ns/op
PASS
ok      example1        65.471s
(2)
1000000000               0.8769 ns/op
PASS
ok      example1        64.095s
(3)
1000000000               0.8761 ns/op
PASS
ok      example1        61.927s

說實話兩者跑的結果是差不多的,選一個喜歡的方法來重構程式碼吧XD。但後來我工作上有個需求也是要重構程式碼,我一開始選擇使用了泛型,但發現泛型若使用物件當參數,竟然不能操作物件,像是使用他的屬性之類的操作,所以只能改用斷言的方式來重構程式碼了…以下為程式碼:

package main

import "fmt"

type apple struct {
	name string
	age  int
}

func show[num int64 | float64 | *apple](s num) {
	fmt.Println(s.name)
        fmt.Println(s.age)
}

func main() {
	apple1 := &apple{name: "andy", age: 26}
	show(apple1)
}
  • 執行結果:

之後改用斷言來解決的程式碼:

package main

import "fmt"

type apple struct {
	name string
	age  int
}

func show(num any) {
	switch s := num.(type) {
	case *apple:
		fmt.Println(s.name)
		fmt.Println(s.age)
	}
}

func main() {
	apple1 := &apple{name: "andy", age: 26}
	show(apple1)
}
  • 執行結果:

重點回顧

  1. Generic是什麼?
  2. Assertion是什麼?
  3. 兩者差別?

總結

結論就是Assertion斷言比較讚,可以接受物件得屬性當參數,用Assertion斷言的方式重構才是聰明人…哈哈哈哈