Go語言——沒有對象的面向對象編程

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


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

爲何你要創造一種從理論上來講,並不使人興奮的語言?
由於它很是有用。 —— Rob Pikejson

Go中的「對象」

要探討Go語言中的對象,咱們先搞清楚一個問題:數組

Go語言有對象嗎?

從語法上來講,函數

  • Go中沒有類(Classes)
  • Go中沒有「對象」(Objects)

到底什麼是對象?

對象是一種抽象的數據類型,擁有狀態(數據)和行爲(代碼)。 —— Steve Franciaui

在Go語言中,咱們這樣聲明一個類型:code

類型聲明(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/YGrdCG3SlIip

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

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

相關文章
相關標籤/搜索