方法
Go 没有类。不过你可以为类型定义方法。
方法就是一类带特殊的 接收者 参数的函数。
方法接收者在它自己的参数列表内,位于 func 关键字和方法名之间。
在此例中,Abs 方法拥有一个名字为 v,类型为 Vertex
的接收者。
methods.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package main
import ( "fmt" "math" )
type Vertex struct { X, Y float64 }
func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) }
func main() { v := Vertex{3, 4} fmt.Println(v.Abs()) }
|
方法即函数
方法只是个带接收者参数的函数。 现在这个Abs
的写法就是一个正常的函数,功能并没有什么变化。
为非结构体类型声明方法
method-continued.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package main
import ( "fmt" "math" )
type MyFloat float64
func (f MyFloat) Abs() float64 { if f < 0 { return float64(-f) } return float64(f) }
func main() { f := MyFloat(-math.Sqrt2) fmt.Println(f.Abs()) }
|
在此例中,我们看到了一个带 Abs 方法的数值类型 MyFloat。
你只能为在同一个包中定义的接收者类型声明方法,而不能为其它别的包中定义的类型 (包括 int 之类的内置类型)声明方法。
即 接收者的类型定义和方法声明必须在同一包内。
指针类型的接收者
你可以为指针类型的接收者声明方法。
这意味着对于某类型 T
,接收者的类型可以用 *T
的文法。 (此外,T
本身不能是指针,比如不能是 *int
。)
例如,这里为 *Vertex
定义了 Scale
方法。
指针接收者的方法可以修改接收者指向的值(如这里的 Scale
所示)。 由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。
试着移除第 16 行 Scale
函数声明中的 *
,观察此程序的行为如何变化。
若使用值接收者,那么 Scale
方法会对原始 Vertex
值的副本进行操作。(对于函数的其它参数也是如此。)Scale
方法必须用指针接收者来更改 main
函数中声明的 Vertex
的值。
method-pointers.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| package main
import ( "fmt" "math" )
type Vertex struct { X, Y float64 }
func (v Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) }
func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f }
func main() { v := Vertex{3, 4} v.Scale(10) fmt.Println(v.Abs()) }
|
方法与指针重定向
带指针参数的函数必须接受一个指针
1 2 3
| var v Vertex ScaleFunc(v, 5) ScaleFunc(&v, 5)
|
而接收者为指针的的方法被调用时,接收者既能是值又能是指针:
1 2 3 4
| var v Vertex v.Scale(5) p := &v p.Scale(10)
|
对于语句 v.Scale(5)
来说,即便 v
是一个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale
方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5)
解释为 (&v).Scale(5)
。
indirection.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| package main
import "fmt"
type Vertex struct { X, Y float64 }
func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f }
func ScaleFunc(v *Vertex, f float64) { v.X = v.X * f v.Y = v.Y * f }
func main() { v := Vertex{3, 4} v.Scale(2) ScaleFunc(&v, 10)
p := &Vertex{4, 3} p.Scale(3) ScaleFunc(p, 8)
fmt.Println(v, p) }
|
反之也一样:
接受一个值作为参数的函数必须接受一个指定类型的值:
1 2 3
| var v Vertex fmt.Println(AbsFunc(v)) fmt.Println(AbsFunc(&v))
|
而以值为接收者的方法被调用时,接收者既能为值又能为指针:
1 2 3 4
| var v Vertex fmt.Println(v.Abs()) p := &v fmt.Println(p.Abs())
|
这种情况下,方法调用 p.Abs()
会被解释为 (*p).Abs()
。
选择值或指针作为接收者
使用指针接收者的原因有二:
首先,方法能够修改其接收者指向的值。
其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样会更加高效。
在本例中,Scale
和 Abs
接收者的类型为 *Vertex
,即便 Abs
并不需要修改其接收者。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。
methods-with-pointer-receivers.go1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| package main
import ( "fmt" "math" )
type Vertex struct { X, Y float64 }
func (v *Vertex) Scale(f float64) { v.X = v.X * f v.Y = v.Y * f }
func (v *Vertex) Abs() float64 { return math.Sqrt(v.X*v.X + v.Y*v.Y) }
func main() { v := &Vertex{3, 4} fmt.Printf("缩放前:%+v,绝对值:%v\n", v, v.Abs()) v.Scale(5) fmt.Printf("缩放后:%+v,绝对值:%v\n", v, v.Abs()) }
|