這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
關於本系列
決定開個新坑。
這個系列首先是關於Go語言實踐的。在項目中實際使用Go語言也有段時間了,一個體會就是不論是官方文檔、圖書還是網路資料,關於Go語言慣用法(idiom)的介紹都比較少,基本只能靠看標準庫原始碼自己琢磨,所以我特別想在這方面有一些收集和總結。
然後這個系列也是關於設計模式的。雖然Go語言不是一門物件導向程式設計語言,但是很多物件導向設計模式所要解決的問題是在程式設計中客觀存在的。不管用什麼語言,總是要面對和解決這些問題的,只是解決的思路和途徑會有所不同。所以我想就以經典的設計模式作為切入點來展開這個系列,畢竟大家對設計模式都很熟悉了,可以避免無中生有想出一些蹩腳的應用情境。
本系列的具體主題會比較靈活,計劃主要包括這些方面的話題:
- Go語言慣用法。
- 設計模式的實現。特別是引入了閉包,協程,DuckType等語言特性後帶來的變化。
- 設計模式思想的探討。會有一些吐槽。
不使用迭代器的方案
首先要指出的是,絕大多數情況下Go程式是不需要用迭代器的。因為內建的slice和map兩種容器都可以通過range進行遍曆,並且這兩種容器在效能方面做了足夠的最佳化。只要沒有特殊的需求,通常是直接用這兩種容器解決問題。即使不得不寫了一個自訂容器,我們幾乎總是可以實現一個函數,把所有元素(的引用)拷貝到一個slice之後返回,這樣調用者又可以直接用range進行遍曆了。
當然某些特殊場合迭代器還是有用武之地。比如迭代器的Next()是個耗時操作,不能一口氣拷貝所有元素;再比如某些條件下需要中斷遍曆。
經典實現
經典實現完全採用物件導向的思路。為了簡化問題,下面的例子中容器就是簡單的[]int
,我們在main
函數中使用迭代器進行遍曆操作並列印取到的值,迭代器的介面設計參考java。
package mainimport "fmt"type Ints []intfunc (i Ints) Iterator() *Iterator {return &Iterator{data: i,index: 0,}}type Iterator struct {data Intsindex int}func (i *Iterator) HasNext() bool {return i.index < len(i.data)}func (i *Iterator) Next() (v int) {v = i.data[i.index]i.index++return v}func main() {ints := Ints{1, 2, 3}for it := ints.Iterator(); it.HasNext(); {fmt.Println(it.Next())}}
閉包實現
Go語言支援first class functions、高階函數、閉包、多傳回值函數。用上這些特性可以換種方式實現迭代器。
初看之下閉包實現與經典實現完全不同,其實從本質上來看,二者區別不大。經典實現中把迭代器需要的資料存在struct中,HasNext()
Next()
兩個函數定義為Iterator
的方法從而和資料繫結了起來;閉包實現中迭代器是一個匿名函數,它所需要的資料i Ints
和index
以閉包up value的形式綁定了起來,匿名函數返回的兩個值正好對應經典實現中的Next()
和HasNext()
。
package mainimport "fmt"type Ints []intfunc (i Ints) Iterator() func() (int, bool) {index := 0return func() (val int, ok bool) {if index >= len(i) {return}val, ok = i[index], trueindex++return}}func main() {ints := Ints{1, 2, 3}it := ints.Iterator()for {val, ok := it()if !ok {break}fmt.Println(val)}}
channel實現
這份實現是最go way的,使用了一個channel在新的goroutine中將容器內的元素依次輸出。優點是channel是可以用range接收的,所以調用方代碼很簡潔;缺點是goroutine環境切換會有開銷,這份實現無疑是最低效的,另外調用方必須接收完所有資料,如果只接收一半就中斷掉發送方將永遠阻塞。
依稀記得在郵件清單裡看到說標準庫裡有這個用法的例子,剛才去翻了下沒找到原帖了:-)
順便說一下,“在函數中建立一個channel返回,同時建立一個goroutine往channel中塞資料”這是一個重要的慣用法(雖然我不知道這個應該叫什麼名字=。=),可以用來做序列發生器、fan-out、fan-in等。
package mainimport "fmt"type Ints []intfunc (i Ints) Iterator() <-chan int {c := make(chan int)go func() {for _, v := range i {c <- v}close(c)}()return c}func main() {ints := Ints{1, 2, 3}for v := range ints.Iterator() {fmt.Println(v)}}
Do實現
這份迭代器實現是最簡潔的,代碼也很直白,無須多言。如果想加上中斷迭代的功能,可以將func(int)
改為func(int)bool
,Do中根據傳回值決定是否退出迭代。
標準庫中的container/ring
中有Do()用法的例子。
package mainimport "fmt"type Ints []intfunc (i Ints) Do(fn func(int)) {for _, v := range i {fn(v)}}func main() {ints := Ints{1, 2, 3}ints.Do(func(v int) {fmt.Println(v)})}
總結
- Go語言中沒有class和繼承,不具備完整表達物件導向的能力,不是一門通常意義上的物件導向語言。但是這不妨礙Go語言實現物件導向的思想,利用其語言特性,實現封裝、組合、多態都沒有問題。
- 設計模式的精髓在于思想而不在於類圖。程式設計語言是在不斷進步的,類圖卻一直用幾十年前那一張,拋開類圖重新審視問題,合理利用語言新特性可以得到更簡潔的設計模式實現。