同步 Syncronization
對這部分沒有基礎概念的讀者,可以先參考清大作業系統課程,這邊就不多做解釋。Go 提供 Race Condition 的檢測,只要在執行指令後面加上 -race,執行程式時就會進行分析,如果有問題就會跳訊息出來。
go run -race main.go
我們以簡單的 Count++ 來舉例:
var cnt intfunc addCount() {
cnt++
}func getGount() int {
return cnt
}func main() {
for i := 0; i < 500; i++ {
go addCount()
}
time.Sleep(1 * time.Second)
fmt.Println("cnt = ", getGount())
} // print out: cnt = 469
很明顯多執行幾次的結果回傳都不是 500,因為他們發生搶資源的狀況。該如何解決呢?可以使用 Mutex Lock。
互斥鎖定 Mutex Lock
Mutex = Mutual Exclusion,在 Go 使用方式為宣告 sync.Mutex 變數。我們將剛才的例子,新增 mutex_lock 變數 (line 9),並在函式部分進行修改成函式執行完才解鎖,即可避免 race condition 的狀況發生,他的執行結果會印出 cnt = 500。
此外,有另一種 Lock 叫做讀寫互斥鎖定,適合用在讀資料比寫資料多的情境下。使用方式為宣告 sync.RWMutex 變數。多了唯讀鎖定,可讓多個 Go-Routine 同時讀取資料時不會造成阻塞。
如果只是操作 counter 簡單的數字加加減減,有更有效率的作法,就是使用 sync/atomic 這個套件來做原子操作 Atomic Manipulation。但因為這個東西只有在簡單任務的狀態下使用,個人認為不如好好學會使用更萬能 Mutex Lock,所以這邊就沒有放範例。
等待群組 Wait Group
其實上面的飯粒都在 main 後面加了 time.Sleep,是為了能完整看到 Go-Routine 的執行。如果把這行拿掉,會發現 cnt = 0,因為新建的 Go-Routine 都還沒開始跑,主函式就 Done 了。為了避免這樣的狀況,我們可以使用 WaitGroup 來實現,內部有一個計數器可以等待所有任務完成,宣告 sync.WaitGroup 變數即可使用。話不多說,直接把剛才的舉例調整一下。
看到這裡,可能會發現 defer 真的很重要,它可以讓我們的程式碼變得很有彈性,如果還不是很理解他的操作,建議回到 Part3 或是找其他文章來釐清觀念。
反射 Reflect
反射是指在程式執行期間獲取實體的型別資訊,在多型的環境下就能用反射來識別實體型別,以及存取資料和方法。Go 可自訂型別,反射獲取的型別資訊可以找出 Name & Kind,Name 是自訂型別名稱,而 Kind 是他真實的型別種類。此外,在 StructField 中有個欄位 StructTag,像是 struct’s meta data,可以在定義 struct 時一同定義,且可在反射時取出。
先透過 reflect.TypeOf(variable_name) 定義反射變數,接下來透過這個反射變數來操作,用 .Name() 取得他的型別名稱,.Kind() 取得他真實的型別種類。對 struct 的變數來說,用 .NumField() 知道有幾個成員變數。如果想了解他裡面有什麼欄位,用 .Field(idx) 取出,後面再加上 .Name 或是 .Tag,得到這個欄位的變數名稱以及其 Meta Data。可參照以下範例。
(To be honest, 反射這部分只是淺談,一班情況下似乎用不太到,因此沒有深入了解!)
單元測試 Unit Test
這個部分相當重要!學校不會教但在業界開發卻很常用的。Go 有一個很強大的地方是 Unit Test 框架已經內建在 package,不需要再選擇框架或是安裝其他套件,真 · 21世紀的程式語言。Unit Test 是指對軟體最小受測範圍的測試,以 System 的角度來看,單元指的是功能單元。若以 Code 的角度來看,單元指的是 function 或 class。Go 使用 go test 來執行 test code,而 test code 會寫以 _test 結尾的測試文件中 (e.g. sample_test.go)。 一個測試文件是由一或多個測試函式組成,每個測試函式須以 Test 開頭。
先寫出我們想要測試的東西到 test-eg.go 檔案。
接著,寫我們的測試用程式碼到 test-eg_test.go 檔案。
接著在 Teminal 上你的 Directory 輸入以下指令,即可開始測試!
go test -v -cover=true test-eg_test.go test-eg.go
他會跑出以下結果:
=== RUN TestAdd
eg_test.go:8: Add Success
--- PASS: TestAdd (0.00s)
=== RUN TestFound
eg_test.go:15: Found success
--- PASS: TestFound (0.00s)
=== RUN TestNotFound
eg_test.go:24: Found success
--- PASS: TestNotFound (0.00s)
PASS
coverage: 100.0% of statements
ok command-line-arguments 0.523s coverage: 100.0% of statements
當然,如果你是開發者,可能會覺得自己一直手動寫 test 很麻煩。沒關係,有一個套件可以幫忙你:gotests,有興趣的可以再深入了解~
性能測試 Benchmark Test
性能測試,即測試一段程式或一個函式的執行效能,包含 CPU 計算時間、記憶體佔用比例等。Go 內建性能測試工具,用法和單元測試工具相似,test code 也是放在 _test
結尾的檔案裡,建議不要和單元測試混在一起。性能測試函式必須以 Benchmark
開頭,且參數為 b *testing.B
,b.N
為性能測試框架提供的測試次數,不能假設它的範圍。
接著在 Teminal 上你的 Directory 輸入以下指令,即可開始測試!
go test -v -bench=. benchmark_test.go
他會跑出以下結果:
goos: darwin
goarch: amd64
cpu: VirtualApple @ 2.50GHz
Benchmark_DynamicAllocation
benchmark_test.go:9: 1
benchmark_test.go:9: 100
benchmark_test.go:9: 10000
benchmark_test.go:9: 1000000
benchmark_test.go:9: 7061678
Benchmark_DynamicAllocation-8 7061678 271.3 ns/op
PASS
ok command-line-arguments 2.522s
以上大概是對於 Go 語言的初步認識,之後工作上如果有遇到新奇的東西,值得探討的,會再與大家分享,謝謝!順帶一提,我認為程式語言的學習必須透過實際敲過程式碼才能有顯著進步,不仿透過 Leetcode 來練練感覺吧!萬事起頭難,先從 #1. Two Sum 開始~共勉之!
參考資料 / Reference
1. A Tour of Go
2. Golang 30 天
3. Golang Crash Course
4. Learn Go Programming — Golang Tutorial for Beginners
5. 小馬技術
6. Unit Test iTHelp
This article will be updated at any time! Thanks for your reading. If you like the content, please click the “clap” button. You can also press the follow button to track new articles at any time. Feel free to contact me via LinkedIn or email.