Go语言——没有对象的面向对象编程
本文译自steve francia在oscon 2014的一个ppt,原作请前往:
对我来说,最吸引我的不是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做了一些伟大的事情
上一篇: 简单的PHP留言本实例代码
下一篇: 地图下钻简单实现