1. 程式人生 > >Go語言設計模式實踐:迭代器(Iterator)

Go語言設計模式實踐:迭代器(Iterator)

關於本系列

決定開個新坑。

這個系列首先是關於Go語言實踐的。在專案中實際使用Go語言也有段時間了,一個體會就是不論是官方文件、圖書還是網路資料,關於Go語言慣用法(idiom)的介紹都比較少,基本只能靠看標準庫原始碼自己琢磨,所以我特別想在這方面有一些收集和總結。

然後這個系列也是關於設計模式的。雖然Go語言不是一門面向物件程式語言,但是很多面向物件設計模式所要解決的問題是在程式設計中客觀存在的。不管用什麼語言,總是要面對和解決這些問題的,只是解決的思路和途徑會有所不同。所以我想就以經典的設計模式作為切入點來展開這個系列,畢竟大家對設計模式都很熟悉了,可以避免無中生有想出一些蹩腳的應用場景。

本系列的具體主題會比較靈活,計劃主要包括這些方面的話題:

  1. Go語言慣用法。
  2. 設計模式的實現。特別是引入了閉包,協程,DuckType等語言特性後帶來的變化。
  3. 設計模式思想的探討。會有一些吐槽。

不使用迭代器的方案

首先要指出的是,絕大多數情況下Go程式是不需要用迭代器的。因為內建的slice和map兩種容器都可以通過range進行遍歷,並且這兩種容器在效能方面做了足夠的優化。只要沒有特殊的需求,通常是直接用這兩種容器解決問題。即使不得不寫了一個自定義容器,我們幾乎總是可以實現一個函式,把所有元素(的引用)拷貝到一個slice之後返回,這樣呼叫者又可以直接用range進行遍歷了。

當然某些特殊場合迭代器還是有用武之地。比如迭代器的Next()是個耗時操作,不能一口氣拷貝所有元素;再比如某些條件下需要中斷遍歷。

經典實現

經典實現完全採用面向物件的思路。為了簡化問題,下面的例子中容器就是簡單的[]int,我們在main函式中使用迭代器進行遍歷操作並列印取到的值,迭代器的介面設計參考java。

package main

import "fmt"

type Ints []int

func (i Ints) First() Iterator {
	return Iterator{
		data:  i,
		index: 0,
	}
}

type Iterator struct {
	data  Ints
	index int
}

func (i Iterator) Get() int {
	return
i.data[i.index] } func (i Iterator) HasNext() bool { return i.index < len(i.data) } func (i Iterator) Next() Iterator { return Iterator{ data: i.data, index: i.index + 1, } } func main() { ints := Ints{1, 2, 3} for it := ints.First(); it.HasNext(); it = it.Next() { fmt.Println(it.Get()) } }

閉包實現

Go語言支援first class functions、高階函式、閉包、多返回值函式。用上這些特性可以換種方式實現迭代器。

初看之下閉包實現與經典實現完全不同,其實從本質上來看,二者區別不大。經典實現中把迭代器需要的資料存在struct中,Get() HasNext() Next()三個函式定義為Iterator的方法從而和資料綁定了起來;閉包實現中迭代器是一個匿名函式,它所需要的資料i Intsindex以閉包up value的形式綁定了起來,匿名函式返回的兩個值正好對應經典實現中的Get()HasNext()

package main

import "fmt"

type Ints []int

func (i Ints) Iterator() func() (int, bool) {
	index := 0
	return func() (val int, ok bool) {
		if index >= len(i) {
			return
		}

		val, ok = i[index], true
		index++
		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 main

import "fmt"

type Ints []int

func (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/list中有Do()用法的例子。

package main

import "fmt"

type Ints []int

func (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)
	})
}

總結

  1. Go語言中沒有class和繼承,不具備完整表達面向物件的能力,不是一門通常意義上的面嚮物件語言。但是這不妨礙Go語言實現面向物件的思想,利用其語言特性,實現封裝、組合、多型都沒有問題。
  2. 設計模式的精髓在於思想而不在於類圖。程式語言是在不斷進步的,類圖卻一直用幾十年前那一張,拋開類圖重新審視問題,合理利用語言新特性可以得到更簡潔的設計模式實現。