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