个人主页:个人主页
系列专栏:Golang基础
Go(又称Golang)是由Google开发的开源编程语言。它结合了静态类型的安全性和动态语言的灵活性,拥有高效的并发编程能力和简洁的语法。Go被设计用于构建可扩展、高性能的软件系统,具有优秀的内存管理和快速的编译速度,适用于Web开发、系统编程和云计算等领域。
函数的另一种形态,带有接收者的函数,称为 method
现在假设有这么一个场景,定义了一个 struct 叫做长方形,现在想要计算他的面积,那么按照一般的思路应该会用下面的方式来实现:
package main
import "fmt"
type Rectangle struct {
width, height float64
}
func area(r Rectangle) float64 {
return r.width*r.height
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
fmt.Println("Area of r1 is: ", area(r1))
fmt.Println("Area of r2 is: ", area(r2))
}
这段代码可以计算出来长方形的面积,但是 area()
不是作为 Rectangle 的方法实现的(类似面向对象里面的方法),而是将Rectangle 的对象(如 r1,r2 )作为参数传入函数计算面积的。
这样实现当然没有问题,但是当需要增加圆形、正方形、五边形甚至其它多边形的时候,想计算他们的面积的时候怎么办?
那就只能增加新的函数,但是函数名就必须要跟着换了,变成 area_rectangle
, area_circle
, area_triangle
…
椭圆代表函数, 而这些函数并不从属于 struct (或者以面向对象的术语来说,并不属于 class ),他们是单独存在于 struct 外围,而非在概念上属于某个 struct 的。
很显然,这样的实现并不优雅,并且从概念上来说"面积"是"形状"的一个属性,它是属于这个特定的形状的,就像长方形的长和宽一样。
基于上面的原因所以就有了 method 的概念, method 是附属在一个给定的类型上的,他的语法和函数的声明语法几乎一样,只是在 func 后面增加了一个 receiver (也就是 method 所依从的主体)。
用上面提到的形状的例子来说,method area() 是依赖于某个形状(比如说 Rectangle )来发生作用的。
Rectangle.area() 的发出者是 Rectangle , area() 是属于 Rectangle 的方法,而非一个外围函数。
更具体地说,Rectangle 存在字段 height 和 width, 同时存在方法 area() , 这些字段和方法都属于 Rectangle。
用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"
"math"
)
type Rectangle struct {
width, height float64
}
type Circle struct {
radius float64
}
func (r Rectangle) area() float64 {
return r.width*r.height
}
func (c Circle) area() float64 {
return c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{12, 2}
r2 := Rectangle{9, 4}
c1 := Circle{10}
c2 := Circle{25}
fmt.Println("Area of r1 is: ", r1.area())
fmt.Println("Area of r2 is: ", r2.area())
fmt.Println("Area of c1 is: ", c1.area())
fmt.Println("Area of c2 is: ", c2.area())
}
在使用 method 的时候重要注意几点:
.
访问,就像struct里面访问字段一样在上例,method area()
分别属于 Rectangle 和 Circle , 于是他们的 Receiver 就变成了 Rectangle 和 Circle, 或者说,这个 area()
方法 是由 Rectangle/Circle 发出的。
值得说明的一点是,此处方法的 Receiver 是以值传递,而非引用传递,是的,Receiver 还可以是指针, 两者的差别在于, 指针作为 Receiver 会对实例对象的内容发生操作,而普通类型作为 Receiver 仅仅是以副本作为操作对象,并不对原实例对象发生操作,后文对此会有详细论述。
那是不是 method 只能作用在 struct 上面呢?当然不是,他可以定义在任何 自定义的类型、内置类型、struct 等各种类型上面。
什么叫自定义类型,自定义类型不就是 struct ,其实不是这样的,struct只是自定义类型里面一种比较特殊的类型而已,还有其他自定义类型申明,可以通过如下这样的申明来实现:
type typeName typeLiteral
请看下面这个申明自定义类型的代码:
type ages int
type money float32
type months map[string]int
m := months {
"January":31,
"February":28,
...
"December":31,
}
这样就可以在自己的代码里面定义有意义的类型了,实际上只是一个定义了一个别名,有点类似于c中的 typedef,例如上面 ages 替代了 int,回到 method 可以在任何的自定义类型中定义任意多的 method ,接下来让看一个复杂一点的例子:
package main
import "fmt"
const(
WHITE = iota
BLACK
BLUE
RED
YELLOW
)
type Color byte
type Box struct {
width, height, depth float64
color Color
}
type BoxList []Box //a slice of boxes
func (b Box) Volume() float64 {
return b.width * b.height * b.depth
}
func (b *Box) SetColor(c Color) {
b.color = c
}
func (bl BoxList) BiggestColor() Color {
v := 0.00
k := Color(WHITE)
for _, b := range bl {
if bv := b.Volume(); bv > v {
v = bv
k = b.color
}
}
return k
}
func (bl BoxList) PaintItBlack() {
for i := range bl {
bl[i].SetColor(BLACK)
}
}
func (c Color) String() string {
strings := []string {"WHITE", "BLACK", "BLUE", "RED", "YELLOW"}
return strings[c]
}
func main() {
boxes := BoxList {
Box{4, 4, 4, RED},
Box{10, 10, 1, YELLOW},
Box{1, 1, 20, BLACK},
Box{10, 10, 1, BLUE},
Box{10, 30, 1, WHITE},
Box{20, 20, 20, YELLOW},
}
fmt.Printf("We have %d boxes in our set\n", len(boxes))
fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³")
fmt.Println("The color of the last one is",boxes[len(boxes)-1].color.String())
fmt.Println("The biggest one is", boxes.BiggestColor().String())
fmt.Println("Let's paint them all black")
boxes.PaintItBlack()
fmt.Println("The color of the second one is", boxes[1].color.String())
fmt.Println("Obviously, now, the biggest one is",
boxes.BiggestColor().String())
}
上面的代码通过 const 定义了一些常量,然后定义了一些自定义类型
然后以上面的自定义类型为接收者定义了一些 method
上面的代码通过文字描述出来之后是不是很简单?
一般解决问题都是通过问题的描述,去写相应的代码实现。
现在让回过头来看看 SetColor 这个 method ,它的 receiver 是一个指向 Box 的指针,可以使用 *Box。
定义 SetColor 的真正目的是想改变这个 Box 的颜色,如果不传 Box 的指针,那么 SetColor 接受的其实是
Box 的一个copy,也就是说 method 内对于颜色值的修改,其实只作用于 Box 的copy,而不是真正的 Box。
所以需要传入指针。
这里可以把 receiver 当作 method 的第一个参数来看,然后结合前面函数讲解的传值和传引用就不难理解。
这里也许会问 SetColor 函数里面应该这样定义 *b.Color=c
,而不是 b.Color=c
,需要读取到指针相应的
值。
其实Go里面这两种方式都是正确的,当用指针去访问相应的字段时(虽然指针没有任何的字段),Go知道要通过指针去获取这个值。PaintItBlack 里面调用 SetColor 的时候是不是应该写成 (&bl[i]).SetColor(BLACK)
,因为 SetColor 的 receiver 是 *Box,而不是 Box。
这两种方式都可以,因为Go知道 receiver 是指针,他自动转了。
也就是说如果一个 method 的 receiver 是 *T ,可以在一个 T类型 的实例变量 V 上面调用这个 method,而不需要 &V 去调用这个 method。
类似的如果一个 method 的 receiver 是 T,可以在一个 *T类型 的变量 P 上面调用这个 method ,而不需要 *P去调用这个 method
所以不用担心是调用的指针的 method 还是不是指针的 method,Go知道要做的一切。
通过字段的继承的学习,发现Go的一个神奇之处,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()
}
上面的例子中,如果 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里面的面向对象是如此的简单,没有任何的私有、公有关键字,通过大小写来实现(大写开头的为公有,小写开头的为私有),方法也同样适用这个原则。