一个对象其实也就是一个简单的值或者一个变量, 在这个对象中会包含一些方法, 而一个方法则是一个一个和特殊类型关联的函数。 一个面向对象的程序会用方法来表达其属性和对应的操作, 这样使用这个对象的用户就不需要直接去操作对象, 而是借助方法来做这些事情
如前面自定义的Celsius类型的String方法
// func 参数 方法名 返回值类型 函数体
func (c Celsius) String() string { return fmt.Sprintf("%g°C", c) }
方法声明
在函数声明时, 在其名字之前放上一个变量, 即是一个方法。 这个附加的参数会将该函数附加到这种类型上, 即相当于为这种类型定义了一个独占的方法,如下面的函数,
- 首先创建了一个
Point
类 - 第一个
Distance
是package
级别的 - 第二个
Distance
是属于Point
类的方法,其中(p Point)
是方法接收器(类似于python
的self
参数),Go
中的名称没有固定的,但是习惯用类的首字母表示 main
函数中,首先创建了两个Point
的实例- 分别调用了
package
级别和Point
类内的Distance
```go import “math”
type Point struct{X, Y float64}
func Distance(p, q Point) float64 { return math.Hypot(q.X - p.X, q.Y - p.Y) }
func (p Point) Distance(q Point) float64 { return math.Hypot(q.X - p.X, q.Y - p.Y) }
func main() { p1 := Point{1,2} p2 := Point{3,5} fmt.Println(Distance(p1, p2))
fmt.Println(p2.Distance(p1))
fmt.Println(p1.Distance(p2))
} // out // 3.6055512754639896 // 3.6055512754639896 // 3.6055512754639896
比如取名为`self`(仅举例子) 语法和`py`差不多
```go
// ... //
func (self Point) Distance(q Point) float64 {
return math.Hypot(q.X - self.X, q.Y - self.Y)
}
func main() {
p1 := Point{1,2}
p2 := Point{3,5}
fmt.Println(p2.Distance(p1))
fmt.Println(p1.Distance(p2))
}
这种p1.Distance
的表达式叫做选择器, 因为他会选择合适的对应p1
这个对象的Distance
方法来执行。 选择器也会被用来选择一个struct
类型的字段, 比如p1.X
。 由于方法和字段都是在同一命名空间, 所以如果我们在这里声明一个X
方法的话, 编译器会报错, 因为在调用p1.X
时会有歧义
func (p Point) X() {
fmt.Println("")
}
// out
// src/f.go:9:20: invalid operation: operator - not defined on q.X (value of type func())
// src/f.go:12:19: field and method with the same name X
// src/f.go:6:19: other declaration of X
// src/f.go:17:20: invalid operation: operator - not defined on q.X (value of type func())
基于指针对象的方法
当调用一个函数时, 会对其每一个参数值进行拷贝, 如果一个函数需要更新一个变量, 或者函数的其中一个参数实在太大我们希望能够避免进行这种默认的拷贝, 这种情况下我们就需要用到指针了
type Point struct {X, Y float64}
func (p Point) Dis(q Point) float64{
return math.Hypot(q.X - p.X, q.Y - p.Y)
}
func (p *Point) Scale(factor float64){
p.X *= factor
p.Y *= factor
}
func main() {
p := Point{1, 24}
q := Point{2, 34}
// 3种调用方法
qpr := &q
qpr.Scale(3.14)
(&q).Scale(3.14)
// 如果接收器p是一个Point类型的变量, 并且其方法需要一个Point指针作为接收器
// 可以用下面这种简短的写法(会隐式执行(&q).Scale(3.14))
q.Scale(3.14)
fmt.Println(q.Dis(p))
}
第三种简写方法只适用于“变量”, 包括struct
里的字段比如p.X
, 以及array
和slice
内的元素比如perim[0]
。 不能通过一个无法取到地址的接收器来调用指针方法, 比如临时变量的内存地址就无法获取得到
Point{1, 2}.ScaleBy(2) // compile error: can't take address of Point literal
为了避免歧义, 在声明方法时, 如果一个类型名本身是一个指针的话, 是不允许其出现在接收器中的
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
但是可以用一个 *Point
这样的接收器来调用Point
的方法, 因为我们可以通过地址来找到这个变量, 只要用解引用符号 *
来取到该变量即可。 编译器在这里也会给我们隐式地插入*
这个操作符, 所以下面这两种写法等价的:
qpr.Dis(q)
(*qpr).Dis(q)
非指针类型方法
fmt.Println(Point{2, 34}.Dis(p))
调用类的方法总结
- 不管你的
method
的receiver
是指针类型还是非指针类型, 都是可以通过指针/非指针类型进行调用的, 编译器会帮你做类型转换 - 在声明一个method的receiver该是指针还是非指针类型时, 你需要考虑两方面的内部,
- 第一方面是这个对象本身是不是特别大, 如果声明为非指针变量时, 调用会产生一次拷贝;
- 第二方面是如果你用指针类型作为
**receiver**
, 那么你一定要注意, 这种指针类型指向的始终是一块内存地址, 就算你对其进行了拷贝通过嵌入结构体来扩展类型
内嵌可以使我们定义字段特别多的复杂类型, 我们可以将字段先按小类型分组, 然后定义小类型的方法, 之后再把它们组合起来
下面的Colorpoint
类型 ```go import “image/color”
type Point struct{ X, Y float64 } // 继承了Point type ColoredPoint struct { Point Color color.RGBA }
内嵌可以使定义`ColoredPoint`时得到一种句法上的简写形式, 并使其包含`Point`类型所具有的一切字段,然后再定义一些自己的。通过嵌入的字段就是ColoredPoint自身的字段, 而完全不需要在调用时指出`Point`
```go
var cp ColoredPoint
cp.X = 1
fmt.Println(cp.Point.X) // "1"
cp.Point.Y = 2
fmt.Println(cp.Y) // "2"
可以把ColoredPoint
类型当作接收器来调用Point
里的方法, 即使ColoredPoint
里没有声明这些方法
red := color.RGBA{255, 0, 0, 255}
blue := color.RGBA{0, 0, 255, 255}
var p = ColoredPoint{Point{1, 1}, red}
var q = ColoredPoint{Point{5, 4}, blue}
fmt.Println(p.Distance(q.Point)) // "5"
p.ScaleBy(2)
q.ScaleBy(2)
fmt.Println(p.Distance(q.Point)) // "10"
将Point
看作一个基类, 而ColoredPoint
看作其子类或者继承类, 或者将ColoredPoint
看作**"is a" Point**
类型。 但这是错误的理解, Distance
有一个参数是Point
类型, 但q
并不是一个Point
类, 所以尽管q
有着Point
这个内嵌类型, 我们也必须要显式地选择它,
一个ColoredPoint
并不是一个Point
, 但他**"has a"Point**
, 并且它有从Point
类里引入的Distance
和ScaleBy
方法
p.Distance(q.Point) // "5"
p.Distance(q) // compile error: cannot use q (ColoredPoint) as Point(ColoredPoint并不属于Point)