1. 程式人生 > >Go語言——沒有物件的面向物件程式設計

Go語言——沒有物件的面向物件程式設計

本文譯自Steve Francia在OSCON 2014的一個PPT,原作請前往:https://spf13.com/presentation/go-for-object-oriented-programmers/


對我來說,最吸引我的不是Go擁有的特徵,而是那些被故意遺漏的特徵。 —— txxxxd

為什麼你要創造一種從理論上來說,並不令人興奮的語言?
因為它非常有用。 —— Rob Pike

Go中的“物件”

要探討Go語言中的物件,我們先搞清楚一個問題:

Go語言有物件嗎?

從語法上來說,

  • Go中沒有類(Classes)
  • Go中沒有“物件”(Objects)

到底什麼是物件?

物件是一種抽象的資料型別,擁有狀態(資料)和行為(程式碼)。 —— Steve Francia

在Go語言中,我們這樣宣告一個型別:

型別宣告(Struct)
type Rect struct {
	width  int
	height int
}
然後我們可以給這個Struct宣告一個方法
func (r *Rect) Area() int {
	return r.width * r.height
}
用起來就像這樣
func main() {
	r := Rect{width: 10, height: 5}
	fmt.Println("area: ", r.Area())
}

我們不光可以宣告結構體型別,我們可以宣告任何型別。比如一個切片:

型別宣告(Slice)
type Rects []*Rect
同樣也可以給這個型別宣告一個方法
func (rs Rects) Area() int {
	var a int
	for _, r := range rs {
		a += r.Area()
	}
	return a
}
用起來
func main() {
	r := &Rect{width: 10, height: 5}
	x := &Rect{width: 7, height: 10}
	rs := Rects{r, x}
	fmt.Println("r's area: ", r.Area())
	fmt.Println("x's area: ", x.Area())
	fmt.Println("total area: ", rs.Area())
}

https://play.golang.org/p/G1OWXPGvc3

我們甚至可以宣告一個函式型別

型別宣告(Func)
type Foo func() int
同樣的,給這個(函式)型別宣告一個方法
func (f Foo) Add(x int) int {
	return f() + x
}
然後用起來
func main() {
	var x Foo

	x = func() int { return 1 }

	fmt.Println(x())
	fmt.Println(x.Add(3))
}

https://play.golang.org/p/YGrdCG3SlI

通過上邊的例子,這樣看來,其實

Go有“物件”

那麼我們來看看

“面向物件”的Go

如果一種語言包含物件的基本功能:標識、屬性和特性,則通常認為它是基於物件的。
如果一種語言是基於物件的,並且具有多型性和繼承性,那麼它被認為是面向物件的。 —— Wikipedia

第一條,我們在上邊的例子看到了,go中的type declaration其實滿足了Go語言是基於物件的。那麼,

Go是基於物件的,它是面向物件的嗎?

我們來看看關於第二條,繼承性和多型性

繼承

  • 提供物件的複用
  • 類是按層級建立的
  • 繼承允許一個類中的結構和方法向下傳遞這種層級

Go中實現繼承的方式

  • Go明確地避免了繼承
  • Go嚴格地遵循了符合繼承原則的組合方式
  • Go中通過嵌入型別來實現組合

組合

  • 提供物件的複用
  • 通過包含其他的物件來宣告一個物件
  • 組合使一個類中的結構和方法被拉進其他類中

繼承把“知識”向下傳遞,組合把“知識”向上拉昇 —— Steve Francia

嵌入型別
type Person struct {
	Name string
	Address
}

type Address struct {
	Number string
	Street string
	City   string
	State  string
	Zip    string
}
給被嵌入的型別宣告一個方法
func (a *Address) String() string {
	return a.Number + " " + a.Street + "\n" + a.City + ", " + a.State + " " + a.Zip + "\n"
}
使用組合字面量宣告一個Struct
func main() {
	p := Person{
		Name: "Steve",
		Address: Address{
			Number: "13",
			Street: "Main",
			City:   "Gotham",
			State:  "NY",
			Zip:    "01313",
		},
	}
}
跑起來試試
func main() {
	p := Person{
		Name: "Steve",
		Address: Address{
			Number: "13",
			Street: "Main",
			City:   "Gotham",
			State:  "NY",
			Zip:    "01313",
		},
	}
	fmt.Println(p.String())
}

https://play.golang.org/p/9beVY9jNlW

升級

  • 升級會檢查一個內部型別是否能滿足需要,並“升級”它
  • 內嵌的資料域和方法會被“升級”
  • 升級發生在執行時而不是宣告時
  • 被升級的方法被認為是符合介面的
升級不是過載
func (a *Address) String() string {
	return a.Number + " " + a.Street + "\n" + a.City + ", " + a.State + " " + a.Zip + "\n"
}

func (p *Person) String() string {
	return p.Name + "\n" + p.Address.String()
}

外部結構的方法和內部結構的方法都是可見的

func main() {
	p := Person{
		Name: "Steve",
		Address: Address{
			Number: "13",
			Street: "Main",
			City:   "Gotham",
			State:  "NY",
			Zip:    "01313",
		},
	}
	fmt.Println(p.String())
	fmt.Println(p.Address.String())
}

https://play.golang.org/p/Aui0nGa5Xi

這兩個型別仍然是兩個不同的型別
func isValidAddress(a Address) bool {
	return a.Street != ""
}

func main() {
	p := Person{
		Name: "Steve",
		Address: Address{
			Number: "13",
			Street: "Main",
			City:   "Gotham",
			State:  "NY",
			Zip:    "01313",
		},
	}

	// 這裡不能用 p (Person型別) 作為 Address型別的IsValidAddress引數
	// cannot use p (type Person) as type Address in argument to isValidAddress
	fmt.Println(isValidAddress(p))
	fmt.Println(isValidAddress(p.Address))
}

https://play.golang.org/p/KYjXZxNBcQ

升級不是子型別

多型

為不同型別的實體提供單一介面

通常通過泛型、過載和/或子型別實現

Go中實現多型的方式

  • Go明確避免了子型別和過載
  • Go尚未提供泛型
  • Go的介面提供了多型功能

介面

  • 介面就是(要實現某種功能所需要提供的)方法的列表
  • 結構上的型別 vs 名義上的型別
  • “如果什麼東西能做這件事,那麼就可以在這使用它”
  • 慣例上就叫它 某種東西

Go語言採用了鴨式辯型,和JavaScript類似。鴨式辯型的思想是,只要一個動物走起路來像鴨子,叫起來像鴨子,那麼就認為它是一隻鴨子。 也就是說,只要一個物件提供了和某個介面同樣(在Go中就是相同簽名)的方法,那麼這個物件就可以當做這個介面來用。並不需要像Java中一樣顯式的實現(implements)這個介面。

介面宣告
type Shaper interface{ 
	Area() int 
}
然後把這個介面作為一個引數型別
func Describe(s Shaper) {
	fmt.Println("Area is: ", s.Area())
}
這樣用
func main() {
	r := &Rect{width: 10, height: 5}
	x := &Rect{width: 7, height: 10}
	rs := &Rects{r, x}
	Describe(r)
	Describe(x)
	Describe(rs)
}

https://play.golang.org/p/WL77LihUwi

“如果你可以重新做一次Java,你會改變什麼?”
“我會去掉類class,” 他回答道。
在笑聲消失後,他解釋道,真正的問題不是類class本身,而是“實現”的繼承(類之間extends的關係)。介面的繼承(implements的關係)是更可取的方式。
只要有可能,你就應該儘可能避免“實現”的繼承。
—— James Gosling(Java之父)

Go的介面是基於實現的,而不是基於宣告的

這也就是上邊所說的鴨式辯型

介面的力量

io.Reader
type Reader interface {
	Read(p []byte) (n int, err error)
}
  • Interface
  • Read方法讀取最多len(p) bytes的資料到位元組陣列p中
  • 返回讀取的位元組數和遇到的任何error
  • 並不規定Read()方法如何實現
  • 被諸如 os.File, bytes.Buffer, net.Conn, http.Request.Body等等使用
io.Writer
type Writer interface {
	Write(p []byte) (n int, err error)
}
  • Interface
  • Write方法寫入最多len(p) bytes的資料到位元組陣列p中
  • 返回寫入的位元組數和遇到的任何error
  • 並不規定Write()方法如何實現
  • 被諸如 os.File, bytes.Buffer, net.Conn, http.Request.Body等等使用
io.Reader 使用
func MarshalGzippedJSON(r io.Reader, v interface{}) error {
	raw, err := gzip.NewReader(r)
	if err != nil {
		return err
	}
	return json.NewDecoder(raw).Decode(&v)
}
讀取一個json.gz檔案
func main() {
	f, err := os.Open("myfile.json.gz")
	if err != nil {
		log.Fatalln(err)
	}
	defer f.Close()
	m := make(map[string]interface{})
	MarshalGzippedJSON(f, &m)
}
實用的互動性
  • Gzip.NewReader(io.Reader) 只需要傳入一個io.Reader介面型別即可
  • 在files, http requests, byte buffers, network connections, ...任何你建立的東西里都能工作
  • 在gzip包裡不需要任何特殊處理。只要簡單地呼叫Read(n),把抽象的部分留給實現者即可
將 http response 寫入檔案
func main() {
	resp, err := http.Get("...")
	if err != nil {
		log.Fatalln(err)
	}
	defer resp.Body.Close()
	out, err := os.Create("filename.ext")
	if err != nil {
		log.Fatalln(err)
	}
	defer out.Close()
	io.Copy(out, resp.Body) // out io.Writer, resp.Body io.Reader 
}

Go

簡單比複雜更難:你必須努力使你的思維清晰,使之簡單。但最終還是值得的,因為一旦你到了那裡,你就可以移山。 —— Steve Jobs

Go簡單,實用,絕妙

Go做了