Structs And Interfaces「结构体与接口」
尽管我们可以使用Go自带的数据类型来写程序,但在有些时候它会非常的枯燥。考虑一个与形状交互的程序:
package main
import ("fmt"; "math")
func distance(x1, y1, x2, y2 float64) float64 {
a := x2 - x1
b := y2 - y1
return math.Sqrt(a*a + b*b)
}
func rectangleArea(x1, y1, x2, y2 float64) float64 {
l := distance(x1, y1, x1, y2)
w := distance(x1, y1, x2, y1)
return l * w
}
func circleArea(x, y, r float64) float64 {
return math.Pi * r*r
}
func main() {
var rx1, ry1 float64 = 0, 0
var rx2, ry2 float64 = 10, 10
var cx, cy, cr float64 = 0, 0, 5
fmt.Println(rectangleArea(rx1, ry1, rx2, ry2))
fmt.Println(circleArea(cx, cy, cr))
}
追踪所有的坐标会使得我们很难看到程序在做什么,并且可能会导致错误。
1. Structs
一种让这段程序更好的简单方式就是使用一个结构体struct
。结构体是一种包含名字字段的类型。例如我们可以像这样表达一个圆:
type Circle struct {
x float64
y float64
r float64
}
type
关键字引入了一种新类型。它的后面是类型的名称(圆Circle
),关键字struct
表示我们正在定义一个结构体struct
类型和花括号内的字段列表。每一个字段都有一个名字和类型。像函数一样,我们可以折叠相同类型的字段:
type Circle struct {
x, y, r float64
}
1.1 Initialization「初始化」
我们可以通过多种方式创建新的Circle类型的实例:
var c Circle
像其他的数据类型一样,这将会创建一个默认值为0的局部Circle变量。
对一个struct
来说,意味着字段中的每一个被设置为它们对应的0值(0
对应int
,0.0
对应float
,""
对应string
,nil
对应指针,…)我们也可以使用new
函数:
c := new(Circle)
new
会给所有的字段分配内存,给它们设置对应的0值,并返回一个指针。(*Circle
)通常我们想给每个字段一个值。我们可以用两种方式实现,如下:
c := Circle{x: 0, y: 0, r: 5}
或者也可以省略它们的名字,假如我们知道它们定义的顺序:
c := Circle{0, 0, 5}
1.2 Fields「字段」
我们可以使用.
操作来访问字段。
fmt.Println(c.x, c.y, c.r)
c.x = 10
c.y = 5
让我们修改一下circleArea
函数以便于它可以使用Circle
:
func circleArea(c Circle) float64 {
return math.Pi * c.r * c.r
}
main
函数中,我们写:
c := Circle{0, 0, 5}
fmt.Println(circleArea(c))
要记住的一点是,在Go中参数总是被复制的。如果我们尝试去修改在circleArea
函数里面的字段的其中一个,并不会改动它的原始值。鉴于此,我们通常会这么写函数:
func circleArea(c *Circle) float64 {
return math.Pi * c.r*c.r
}
然后修改一下main
函数:
c := Circle{0, 0, 5}
fmt.Println(circleArea(&c))
1.3 Methods「方法」
尽管这比第一版的代码要好得多,我们仍旧可以通过使用函数中方法method
这一特殊的类型来显著的改善它:
func (c *Circle) area() float64 {
return math.Pi * c.r*c.r
}
在func
关键字和函数名字我们已经添加了一个接收器"receiver"
。接收器就像一个参数-它有名字和类型-但是通过这种方式创建的函数可以允许我们使用.
操作来调用函数:
fmt.Println(c.area())
这变的更易读,我们不再需要&
操作符(Go可以通过这种方法自动知道传给circle的是一个指针),而且因为这个函数只能随着Circle
用,我们可以重命名该函数为area
即可:
让我们对长方形做同样的事情:
type Rectangle struct {
x1, y1, x2, y2 float64
}
func (r *Rectangle) area() float64 {
l := distance(r.x1, r.y1, r.x1, r.y2)
w := distance(r.x1, r.y1, r.x2, r.y1)
return l * w
}
main
中:
r := Rectangle{0, 0, 10, 10}
fmt.Println(r.area())
1.4 Embedded Types「嵌入式类型」
一个结构体的字段通常表示has-a的关系。例如,Circle
有radius
。假设我们有一个person结构体:
type Person struct {
Name string
}
func (p *Person) Talk() {
fmt.Println("Hi, my name is", p.Name)
}
然后我们想创建一个Android
结构体。我们可以这么做:
type Android struct {
Person Person
Model string
}
这样是有效的,但是我们更愿意说Android
是一个Person
,而不是Android
有一个Person
。Go通过使用嵌入式类型来支持这样的关系。像已知的匿名字段一样,嵌入式字段像这样:
type Android struct {
Person
Model string
}
我们使用(Person
)类型,并没有给它名字。当定义了这种方式,Person
结构体可以通过类型名字来访问:
a := new(Android)
a.Person.Talk()
但是我们也可以在Android
中更直接的调用Person
任意方法:
a := new(Android)
a.Talk()
is-a
关系是这样直观地运作的:Person
可以讲话,android是一个person,所以android可以讲话。
2. Interfaces「接口」
你可能已经注意到我们可以将Rectangle
的area
方法命名为为与Circle
的area
方法相同的方法。这并不是偶然,在现实生活和编程中,像这样的关系是司空见惯的。Go有一种方法可以通过一种称为接口的类型来显化这些偶然的相似之处。这有一个Shape
接口的例子:
type Shape interface {
area() float64
}
类似一个结构体,接口的创建使用了type
关键字,后面紧跟着名字和关键字interface
。但是并不是定义字段,我们定义'方法集'
。方法集是一个类型为了“实现”接口而必须拥有的方法列表。
在我们的例子中Rectangle
和Circle
都拥有一个返回float64
的area
方法,所以两种类型都是为了实现Shape
接口。这本身并不是特别有用,但是我们使用接口类型来作为函数的实参:
func totalArea(shapes ...Shape) float64 {
var area float64
for _, s := range shapes {
area += s.area()
}
return area
}
我们可以这样调用这个函数:
fmt.Println(totalArea(&c, &r))
接口也可以被用来作为字段:
type MultiShape struct {
shapes []Shape
}
我们甚至可以通过area
方法使MultiShape
本身变成一个Shape
。
func (m *MultiShape) area() float64 {
var area float64
for _, s := range m.shapes {
area += s.area()
}
return area
}
现在MultiShape
可以包含Circle
、Rectangle
甚至其它的MultiShape
了。
本作品采用《CC 协议》,转载必须注明作者和本文链接