化茧成蝶:Go在FreeWheel服务化中的实践
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

正文

既然我们提到了对象,那么首先问一个有趣的问题:Go是一门面向对象的语言吗?官网FAQ的答案是:

Yes and no.

Go有类型(Type)和方法(Method),包括接口(Interface),可以让你使用面向对象的风格进行编程;但Go又缺少很多常见OO语言的特性,如继承、多态等等。

为什么Go会选择这种方式呢?以我的理解,无论是从语言的特性还是编译器实现的角度,简单性都是Go语言的核心设计原则。也许对Go语言的作者来讲,这种设计风格既能保留普通OO语言的核心优点,又能摈弃继承及复杂类型系统带来的额外负担。

好的,接下来我们将从结构体(Struct),函数(Function),方法(Method),接口(Interface),反射(Reflect)等几个方面深入分析Go语言的对象模型。

首先是结构体(Struct)。

结构体(Struct)

结构体与数值、字符串、数组等等类型一样,属于Go语言的基本类型。

结构体在Go语言中扮演着非常重要的角色。由于Go语言中没有常见OO语言中的类(Class),结构体实际承担了类的一部分职责。与常见OO语言类似,结构体的实例可以作为数据容器(Data Container,想象下OO语言中常见的通过Getter/Setter对数据的访问),或是方法接收者(Method Receiver, 后面Method一节中会详细说明)。结构体声明方式如下:

        type Person struct {
            Name string
            Age  int
        }

我们前面提到过,Go没有类,也没有继承,那么Go是如何如常见OO语言般通过继承来实现代码复用的呢?答案是通过组合(Composition)。

        type Employee struct {
            Person
            Job string
        }

结构体Employee内嵌(Embedded)了一个结构体Person。这样,Employee的实例能访问到Person上的所有字段(Field)和方法(Method,我们会在后面的章节中详述),一定程度上实现了类似继承的效果。

        e := &Employee{}
        fmt.Print(e.Person.Name,  e.Person.Age,  e.Job)

注意到我们可以通过e.Person.Name访问到内嵌结构体的字段。如果仅仅这样,那仍然不是特别方便;而实际上Go编译器提供了隐式查找的功能,甚至使我们能通过e.Name访问到内嵌结构体的字段。

        e := &Employee{}
        fmt.Print(e.Name,  e.Age,  e.Job)

这样是不是有点OO语言继承的感觉了?

隐式查找也支持多级嵌套:

        type Manager struct {
            Employee
            Title string
        }
        m := &Manager{}
        fmt.Print(m.Name,  m.Age,  m.Job,  m.Title)

通过以上几例,我们可以看到,Go语言可以通过结构体的组合来实现类似OO继承的概念。但这与真正的继承还是有一定区别,比如Employee的实例并不是(is-a)Person的实例,因此更无法实现OO中的多态(Polymorphism)。

我们定义一个接收Person作为参数的函数来进行说明:

        func playWith(p *Person) {}

然后初始化一个Employee的实例传入:

        e := &Employee{}
        playWith(e)

报错,cannot use type *Employee as type *Person in argument

所以我们无法通过结构体组合来实现OO继承和多态的全部功能。但在后面接口(Interface)的章节中会看到,可以通过接口来实现对象间is-a的关系。

虽然结构体组合相比继承在某些方面有一定的局限性,但由于我们可以内嵌多个结构体,因此得以方便地实现类似多重继承般的代码复用,规避了如多重继承在对象层级上的复杂性。

        type Address struct {
            Number string
            City   string
        }

        type Contact struct {
            Person
            Address
        }
        c := &Contact{}
        fmt.Println(c.Name,  c.City)

很多设计模式的书籍都鼓吹组合优于继承(Composition Over Inheritance),我想Go的作者也是赞同这一看法的,最后以一则小趣闻作为本章的结束:

I once attended a Java user group meeting where James Gosling(Java's inventor)was the featured speaker. During the memorable Q&A session, someone asked him: “If you could do Java over again, what would you change? ” “I'd leave out classes, ” he replied. After the laughter died down, he explained that the real problem wasn't classes per se, but rather implementation inheritance(the extends relationship). Interface inheritance(the implements relationship)is preferable. You should avoid implementation inheritance whenever possible.

函数(Function)

接下来我们讨论函数。

函数同样是Go语言的基本类型。编译器视相同签名(参数和返回值列表)的函数为同一类型。另外值得注意的是,函数是Go语言的第一类公民(First-class citizen)。

        // 声明了一个函数命名类型
        type operator func(int,  int) int

        // 返回值为函数,可以确认函数确实是第一类公民
        func getOperator(op string) operator {
            switch op {
            case "+":
                // 1. 返回一个匿名函数
                // 2. 函数签名一致视为同类型
                return func(x,  y int) int {
                    return x + y
                }
            // 略 ...
            }
        }

调用方式如下:

        // 函数是第一类公民,所以可以通过变量来引用,然后调用
        op := getOperator("+")
        op(1,  2)

        // 当然也可以直接调用
        getOperator("*")(1,  2)

Go语言的函数有一些不太方便的限制,如:

· 不支持同名函数重载(Overload)

· 不支持默认参数

· 不支持模式匹配(Pattern Match, 函数式编程语言常见概念)

        // 如果我们希望扩展上面的函数,定义一个带初始值的版本
        // 因为Go不支持同名函数重载,也不支持默认参数
        // 所以一种可能的解决方案是再重新定义一个函数
        func getOperatorWithValue(op string,  v int) operator {
            switch op {
            case "+":
                return func(x,  y int) int {
                    return v + x + y
                }
            // 略 ...
            }
        }

调用方式如下:

        getOperatorWithValue("+",  1)(2,  3)

但这种解决方案很不优雅。尤其是当参数变化较多时,如果需要重新定义大量的函数尤其显得繁杂。我们接下来会看到更优雅的解决方案。

前面我们说了Go语言函数的一些限制,但它也有自己的特点和优势,如:

· 支持可变长参数

· 支持多返回值,支持命名返回值

· 支持匿名函数和闭包(Closure)

        // 可变长参数函数
        type operator func(...int) int
        // 多返回值,命名返回值。Go的惯例是多返回值中返回error
        func getOperatorWithValue(op string,  v int) (fn operator,  err
        error) {
            switch op {
            case "+":
                // 匿名函数赋值给变量,然后作为返回值
                fn := func(args ...int) int {
                    sum := v
                    for _,  a := range args {
                        sum += a
                    }
                    return sum
                }
                return fn,  nil
            // 略 ...
            default:
                return nil,  errors.New("不支持的操作符")
            }
        }

调用方式如下:

        if op,  err := getOperatorWithValue("+",  1);  err == nil {
            op(2,  3,  4)
        }

再来说明下闭包(Closure)的使用:

        // 稍微修改了之前函数的逻辑,用来说明闭包的概念
        func getOperatorWithValue(op string,  v int) (fn operator,  err
        error) {
            switch op {
            case "+":
                // 返回的函数修改了上层的变量v
                // 这相当于延长了v的生命周期

                // 因此fn是一个闭包
                fn := func(args ...int) int {
                    for _,  a := range args {
                        v += a
                    }
                    return v
                }
                return fn,  nil
            // 略 ...
            default:
                return nil,  errors.New("不支持的操作符")
            }
        }

调用如下:

        if op,  err := getOperatorWithValue("+",  1);  err == nil {
            op(2,  3) // 返回6
            op(2,  3) // 返回11
        }

最后,让我们来看一下如何通过Go特有的设计模式(Design Pattern)来解决语法本身的限制。还是以重载为例。

一种方式是通过函数匹配参数列表,效果如下:

        func Brew(shots int,  variety string,  cups int) []*Coffee {
            // Brew my coffee
        }

        func ALargeCoffee() (int,  string,  int) {
            return 3,  "Robusta",  1
        }

        func ForTheOffice(cups int) (int,  string,  int) {
            return 1,  "Arabica",  cups
        }

        func AnEspresso(shots int) (int,  string,  int) {
            return shots,  "Arabica",  1
        }

        func main() {
            myCoffee := Brew(ALargeCoffee())
            coffeesForEverybody := Brew(ForTheOffice(6))
            wakeUpJuice := Brew(AnEspresso(3))
        }

或者

        func main() {
            t := FillTemplate(FromReader(myReader,  tokens))
            t = FillTemplate(FromFile(filename,  tokens))
            t = FillTemplate(FromURLWithJSON(templateURL,  restServiceURL))
        }

        // FillTemplate will accept a template and a slice of name-values
        and
        // replace the named tokens with the given values and return the
        result.
        func FillTemplate(template string,  tokens map[string]interface{})
        string {
        }

        // 略 ...

另一种方式是通过Functional Options模式,效果如下:

        emptyFile,  err := file.New("/tmp/empty.txt")
        if err ! = nil {
            panic(err)
        }

        fillerFile,  err := file.New("/tmp/file.txt",  file.UID(1000),  file.
        Contents("Lorem Ipsum Dolor Amet"))

        if err ! = nil {
            panic(err)
        }

重载(Overload)等特性的缺失,究其原因,还是如我们在前面提到的设计原则,官网FAQ中也对这个问题进行了回答。通过上面的例子我们可以看到,这些问题一定程度上通过一些模式来进行规避。类似的例子还有Decorator等等。

方法(Method)

方法可以看作特殊的函数。与普通函数不同,方法需要与对象实例绑定,在定义语法上方法有前置的接收者(Receiver)。

可以为除接口和指针外的任何类型定义方法。如:

        type N int

        func (n N) double() int {
            return n*2
        }

        func main() {
            var a N = 5
            println(a.double())
        }

方法同样不支持重载。方法Receiver的类型可以是基础类型或指针类型。这会决定方法调用时对象实例是否被复制。

Go语言中虽然没有类(Class),但它的方法巧妙地达到了与OO语言中方法类似的使用体验和效果。类似于我们在第一部分Struct中介绍过的,甚至可以通过在Struct上定义方法来达到面向对象语言中继承的效果。

我们扩展第一部分的例子:

        type Person struct {

            Name string
            Age  int
        }

        func (p *Person) Talk() {
              ...
        }

        type Employee struct {
              Person
              Job string
        }

        func (e *Employee) Work() {
              ...
        }

接下来,我们可以通过:

        e := &Employee{}
        e.Talk()

来调用被嵌入的Struct上的方法,这也达到了类似于OO中方法继承的效果。但需要注意的是,如同我们在Struct部分提到的,这样的方法“继承”是无法实现OO中的多态的。

最后再来介绍一个比较有意思的特性。如同我们在前面提到过的,方法可以定义在几乎任何的类型上。利用这个特性,我们把方法定义在函数上:

        type HandlerFunc func(ResponseWriter,  *Request)

        // ServeHTTP calls f(w,  r).
        func (f HandlerFunc) ServeHTTP(w ResponseWriter,  r *Request) {
            f(w,  r)
        }

这样,任一满足了func(ResponseWriter, *Request)签名的函数,我都可以把它转化为HandlerFunc类型的对象,然后再调用它的ServeHTTP方法:

        func doSomethingWithHTTP(w ResponseWriter,  r *Request) {
            ...
        }

这样有什么好处呢?这样可以使传入的函数经过类型转化后满足方法参数中的接口,正是我们要在下一部分中介绍的接口:

        type Handler interface {
            ServeHTTP(ResponseWriter,  *Request)
        }

接口(Interface)

Go语言中接口的概念与常见OO语言中接口类似。但在实现角度存在着比较显著的区别。

Go语言中类型实现接口无须显式声明,即只要目标类型方法集内包含接口声明的全部方法,就会被编译器和运行时认为实现了该接口。这种实现机制是比较简洁的,非侵入式的设计也带来了很多便利性。

        type Talker interface {
            Talk()
        }

因为我们之前定义的Person实现了Talk方法,所以它也就实现了Talker接口。

        var t Talker = &Person{}
        t.Talk()

接下来就更有意思了。因为Employee“继承”了Person,所以Employee也实现了Talker接口。

        var t Talker = &Employee{}
        t.Talk()

虽然我们在前面章节中提到过Go语言的“继承”是无法实现多态的,但聪明的读者马上能联想到,我们完全可以利用接口来巧妙地实现Dynamic Dispatch! 这确实是非常有意思的特性!

        func justTalk(t Talker) {
            t.Talk()
        }

        func (e *Employee) Talk() {
            ...
        }

        p := &Person{}
        e := &Employee{}

        justTalk(p)
        justTalk(e)

最后再说明一下,如果接口没有任何方法声明,那么它就是一个空接口interface{}, 类似于OO中的根类型Object,可被赋值为任何类型的对象。如果使用空接口作为方法参数,该参数可以接受任何值。使用空接口作为参数一定要谨慎——这样固然灵活,但失去了类型安全和类型检查等一系列的好处。

反射(Reflect)

反射可以让我们在运行时获取对象的类型信息,这从一定程度上弥补了静态语言相对动态语言在灵活性上的缺失。此外,反射也是实现元编程(Metaprogramming)的重要手段。

和C数据结构一样,Go对象头部没有类型指针,所以无法通过自身获取任何类型相关信息。Go也不像Java、C#那样有元数据,更不像动态语言一样可以在运行时获取类型、方法等各种信息。

Go的反射操作所需的全部信息都来自于接口变量。接口变量除存储自身类型外,还会保存实际对象的类型数据。总体而言,Go的反射具有的功能是较为有限的。

        func TypeOf(i interface()) Type
        func Valueof (i interface()) Value

在使用反射时,尤其需要注意Type和Kind的区别。Type表示对象的类型,Kind表示对象类型所对应的底层类型。如:

        type X int

        func main() {
            var a X = 1
            t := reflect.TypeOf(a)

            fmt.Println(t.Name(),  t.Kind())
        }

则对应的输出应为X int

接下来看一个反射实际使用的范例:

        package main

        import (
            "fmt"
            "reflect"
        )

        type Foo struct {
            FirstName string `tag_name:"tag 1"`
            LastName  string `tag_name:"tag 2"`
            Age       int    `tag_name:"tag 3"`
        }

        func (f *Foo) reflect() {
            val := reflect.ValueOf(f).Elem()

            for i := 0;  i < val.NumField();  i++ {
                valueField := val.Field(i)
                typeField := val.Type().Field(i)

                tag := typeField.Tag

                fmt.Printf("Field Name: %s, \t Field Value: %v, \t Tag Value:
        %s\n",  typeField.Name,  valueField.Interface(),  tag.Get("tag_name"))
            }
        }

        func main() {
            f := &Foo{
                FirstName: "Drew",
                LastName:  "Olson",
                Age:       30,
            }

            f.reflect()
        }

关于反射使用的原则,我们还可以参考The Laws of Reflection:

· Reflection goes from interface value to reflection object.

· Reflection goes from reflection object to interface value.

· To modify a reflection object, the value must be settable.