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