在Go語言的世界裡,func init() {}
是一個適合用來初始化整個package的執行者,它會在package 第一次被import時,自動的執行其中之內容,並且它只會被執行一次…
不過使用上也必須特別注意,因為當某個package需要init
時,就代表它有一些package level variable,而這些package level variable 若是被一些內部的function 或 method 使用時,就可能造成一些side effect。
簡而言之,若使用的不好的話,是很容易造成一些anit pattern的…
舉例來說像下面這段範例程碼碼
package foo
type handler interface {
shouldHandle() bool
handleFoo() error
handlerBar() error
}
type handlerImpl struct {}
func (h handlerImpl) shouldHandle() bool {
return true
}
func (h handlerImpl) handleFoo() error {
return nil
}
func (h handlerImpl) handleBar() error {
return nil
}
var defaulHandler handler
func init() {
defaultHandler = handlerImpl{}
}
func HandleFoo() error {
if defaultHandler.shouldHandle() {
return defaultHandler.handleFoo()
}
return errors.New("not allowed")
}
func HandleBar() error {
if defaultHandler.shouldHandle() {
return defaultHandler.handlerBar()
}
return errors.New("not allowed")
}
在上面這個範例中,HandleFoo
, HandleBar
都會先call defaultHandler.shouldHandle
來看是不是需要處理接下來的流程,如果是的話,再執行各自的handle function。
如果要分別測試HandleFoo
, HandleBar
的話,我們就要想辦法mock defaultHandler
。 但因為defaultHandler
是在init
中被指派的,所以就上面的程式碼來看,是沒有直接可以測試的方式…
但如果我們不在init
中指派defaultHandler
,而是改用下面的範例來做的話,就可以進一步解決測試上的問題…
func Init(concreteHandler handler) {
defaultHandler = concreteHandler
}
以這段程式碼來看,如果我們要測試HandleFoo
, HandleBar
的話,我們只需要在每一個unit test之前,先執行Init()
來代入mocked 過的 concreteHandler
就可以測試了。
不過這樣的作法是不是就沒有問題了???
很不幸的,透過Init
來解決測試的問題還是會有一些副作用;由於這兩個HandleFoo
與HandleBar
都同時用到handler.shouldHandle()
,所以這也代表如果我們同時在測試這兩個function時,就會有可能發生下面的race issue:
- test 1: 測試
HandleFoo
時,代入mockDefaultHandler
, 並且把shouldHandle()
,mock成回傳值為true
. - test 2: 測試
HandleBar
時,代入mockDefaultHandler
, 並且把shouldHandle()
,mock成回傳值為false
.
由於go test
預設是會同時用多個cpu cores來執行所有的測試,這樣就會有機率性地遇到上述的問題,並發生測試結果不如預期情況…
當然如果我們直接使用go test -p 1
來執行unit tests 的話就沒有race 的問題了,不過同樣的就造成測試時間變長的問題了。
寫到這邊的一些想法:
- 如果沒有必要的話,盡量不要使用
init
,因為會使用init
的情況通常代表這個package 有些package level variables 需要被初始化。 - 盡量避免在
func
中直接使用package level variables,因為它會造成那些使用的func
不易被測試。