本文最初发表在我的个人博客,查看原文,获得更好的阅读体验
上一篇文章中介绍了关于Go的类型体系。在Go中,可以为结构等类型定义方法。方法就是带有接收者
参数的函数。方法接收者位于func
关键字和方法名之间。
一 方法的声明
将方法的接收者指定为某一个类型,该方法即成为指定类型的方法。
例如,以下为Point
类型定义了一个Abs()
方法:
package main
import (
"fmt"
"math"
)
func main() {
p := Point{5, 12}
v := p.Abs()
fmt.Println(v)
}
type Point struct {
X float64
Y float64
}
/* Abs是一个方法 */
func (p Point) Abs() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
上述Abs
方法同样的功能也可以使用函数实现:
package main
import (
"fmt"
"math"
)
func main() {
p := Point{
5,
12,
}
v := Abs(p)
fmt.Println(v)
}
type Point struct {
X float64
Y float64
}
/* Abs是一个函数 */
func Abs(p Point) float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
也可以为非结构体类型声明方法:
package main
import (
"fmt"
)
func main() {
var t TimeUnit = 2
fmt.Println(t.Seconds())
}
type Point struct {
X float64
Y float64
}
type TimeUnit int
func (a TimeUnit) Seconds() int {
return int(a) * 60
}
注意,我们只能为同一个包中定义的类型声明方法,这意味着所有的内置类型我们都不能再为它们声明方法。除此之外,可以为任何已命名类型定义方法(指针或接口除外)。
另外,方法的类型是以接收者作为第一个参数的函数的类型。例如,方法Abs
具有以下类型:
func(p *Point)
但是,以这种方式声明的函数却不是方法。
二 方法与函数
2.1 指针接收者
上边的示例中,接收者均为值类型。我们也可以为指针接收者声明方法。即对于类型T
,接收者的类型可以使用*T
。指针接收者的方法可以修改接收者指向的值:
package main
import (
"fmt"
"math"
)
func main() {
v := Point{5, 12}
fmt.Println(v, v.Abs())
v.Scale(10) // 值调用
fmt.Println(v, v.Abs())
p := &v
p.Scale(10) // 指针调用
fmt.Println(p, p.Abs())
}
type Point struct {
X float64
Y float64
}
// 值接收者方法
func (p Point) Abs() float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
// 指针接收者方法
func (p *Point) Scale(f float64) { // 注意该行接收者的声明有一个 *
p.X = p.X * f
p.Y = p.Y * f
}
2.2 指针与函数
上述方法同样可以改为函数实现:
package main
import (
"fmt"
"math"
)
func main() {
v := Point{5, 12}
fmt.Println(v, Abs(v))
// Scale(v, 10) // 值调用编译出错:cannot use v (type Point) as type *Point in argument to Scale
// fmt.Println(v, Abs(v))
Scale(&v, 10) // 指针调用
fmt.Println(v, Abs(v))
}
type Point struct {
X float64
Y float64
}
// 值函数
func Abs(p Point) float64 {
return math.Sqrt(p.X*p.X + p.Y*p.Y)
}
// 指针函数
func Scale(p *Point, f float64) { // 注意该函数第一个参数的声明有一个 *
p.X = p.X * f
p.Y = p.Y * f
}
尝试去掉上边第12行的注释看看
2.3 方法与指针重定向
从前两个示例中可以看出,指针参数的函数必须接受一个指针类型的值:
v := Point{5, 12}
// Scale(v, 10) // 值调用编译出错
Scale(&v, 10) // 指针调用
但在指针接收者方法中,接收者既可以是值又可以是指针:
v := Point{5, 12}
v.Scale(10) // 值调用
p := &v
p.Scale(10) // 指针调用
对于语句v.Scale(10)
,即便v
是个值而非指针,带指针接收者的方法也能被直接调用。也就是说,由于Scale
方法有一个指针接收者,方便起见,Go会将语句v.Scale(10)
解释为(&v).Scale(10)
。
反过来也是一样。
接受一个值作为参数的函数必须接受一个指定类型的值:
v := Point{5, 12}
fmt.Println(v, Abs(v)) // 正常编译
fmt.Println(v, Abs(&v)) // 编译出错
而以值为接收者的方法被调用时,接收者既能为值又能为指针:
v := Point{5, 12}
fmt.Println(v.Abs()) // 正常编译
p := &v
fmt.Println(p.Abs()) // 正常编译
这种情况下,方法调用p.Abs()
会被解释为(*p).Abs()
。
2.4 指针和值的区别
以指针或值为接收者的区别在于:值方法可通过指针和值调用,而指针方法只能通过指针来调用。
原因是,指针方法可以修改接收者指向的值,尤其是在比较大的数据结构中,这样做会更高效;而值方法会导致接收值的副本(值复制),所以任何修改都会被丢弃。因此,语言层面不允许这种错误出现。但是有一个例外,即:当值是可寻址的时候,语言就会自动插入取址符来处理一般的通过值调用的指针方法。如上述示例中的v
,当使用调用v.Scale(10)
时,语言会自动转换为(&v).Scale(10)
。
通常来说,所有给定类型的方法都应该有值或指针接收者,但并不应该二者混用。
参考:
https://golang.org/ref/spec#Method_expressions
https://golang.org/doc/effective_go.html#pointers_vs_values