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做了一些偉大的事情


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM