Go面向对象详解

今天我们来了解一下 Go 中函数的另一种形态,带有接收者的函数,我们称为 method

method 是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在 func 后面增加了一个 receiver (也就是 method 所依从的主体)。

用 Rob Pike 的话来说就是:

"A method is a function with an implicit first argument, called a receiver."

method 的语法如下:
func (r ReceiverType) funcName(parameters) (results)

例如下面的 method 来实现面积的求和:

package main

import (
    "fmt"
)

type Rectangle struct {
    width, height float64
}

// 这样就实现了method
func (r Rectangle) area() float64 {
    return r.width * r.height
}

func main() {
    r1 := Rectangle{12, 2}
    r2 := Rectangle{9, 4}

    fmt.Println("Area of r1 is: ", r1.area())
    fmt.Println("Area of r2 is: ", r2.area())
}

使用 method 有下面的几个特点:

  1. 虽然 method 的名字一模一样,但是如果接收者不一样,那么 method 就不一样。
  2. method 里面可以访问接收者的字段。
  3. 调用 method 通过 . 访问,就像 struct 里面访问字段一样。

Receiver 还可以是指针,两者的差别在于,指针作为 Receiver 会对实例对象的内容发生操作,而普通类型作为 Receiver 仅仅是以副本作为操作对象,并不对原实例对象发生操作。

后文对此会有详细论述。

method 只能作用在 struct 上面吗?
当然不是,他可以定义在任何你自定义的类型、内置类型、struct 等各种类型上面。

struct 只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现。

type typeName typeLiteral

请看下面这个申明自定义类型的代码。

type ages int
type money float32
type months map[string]int

实际上只是一个定义了一个别名,有点类似于 c 中的 typedef,例如上面 ages 替代了 int

指针作为 receiver

package main

import "fmt"

type Tree struct {
    Value int
    Left  *Tree
    Righ  *Tree
}

func (t Tree) setValue1(v int) {
    t.Value = v
}
func (t *Tree) setValue2(v int) {
    t.Value = v
}

func main() {
    t1 := Tree{
        Value: 0,
        Left:  nil,
        Righ:  nil,
    }
    t1.setValue1(1)
    fmt.Println(t1) // {0 <nil> <nil>} 并没有修改原来的值
    t1.setValue2(2)
    fmt.Println(t1) // {2 <nil> <nil>} 对原来的Tree修改了

    t2 := &Tree{
        Value: 0,
        Left:  nil,
        Righ:  nil,
    }
    t2.setValue1(1)
    fmt.Println(t2) // &{0 <nil> <nil>} // 并没有对原来的值进行修改
    t2.setValue2(2)
    fmt.Println(t2) // &{2 <nil> <nil>}
}

从上面的例子可以看出来:

  1. 如果一个 methodreceiver*T, 你可以在一个 T 类型的实例变量 V 上面调用这个 method,而不需要 &V 去调用这个 method

  2. 如果一个 methodreceiverT,你可以在一个 T 类型的变量 P 上面调用这个 method,而不需要 *P 去调用这个 method

这种特性:让开发者不必关注,调用的指针的 method 还是不是指针的 method,Go 能推断出你的目的,这对于有多年 C/C++ 编程经验的同学来说,真是解决了一个很大的痛苦。

如果你的方法想修改值就将 receiver 改为指针类型,否则就使用实例变量类型。

method 继承

如果匿名字段实现了一个 method,那么包含这个匿名字段的 struct 也能调用该 method

package main

import "fmt"

type Human struct {
    name  string
    age   int
    phone string
}

type Student struct {
    Human  // 匿名字段
    school string
}

type Employee struct {
    Human   // 匿名字段
    company string
}

// 在 human 上面定义了一个 method
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

method 重写
上面的例子中,如果 Employee 想要实现自己的 SayHi, 怎么办?

简单,和匿名字段冲突一样的道理,我们可以在 Employee 上面定义一个 method,重写了匿名字段的方法。

package main
import "fmt"

type Human struct {
    name  string
    age   int
    phone string
}

type Student struct {
    Human  // 匿名字段
    school string
}

type Employee struct {
    Human   // 匿名字段
    company string
}

// Human 定义 method
func (h *Human) SayHi() {
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)
}

// Employee 的 method 重写 Human 的 method
func (e *Employee) SayHi() {
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,
		e.company, e.phone) //Yes you can split into 2 lines here.
}

func main() {
    mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"}
    sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"}

    mark.SayHi()
    sam.SayHi()
}

上面的代码设计的是如此的美妙,让人不自觉的为 Go 的设计惊叹!

通过这些内容,我们可以设计出基本的面向对象的程序了。

但是 Go 里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现 (大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。

关于我
loading
在线编辑器