方法-学习Go语言

阅读笔记

methods.go

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())
}

笔记

Go 没有类。不过你可以为结构体类型定义方法。

方法就是一类带特殊的 **接收者** 参数的函数。

方法接收者在它自己的参数列表内,位于 `func` 关键字和方法名之间。

在此例中,`Abs` 方法拥有一个名为 `v`,类型为 `Vertex` 的接收者。

1. 这不是面向对象的类的方法吗?是否根据函数名的大小写来区分权限public 和private?

methods-funcs.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(Abs(v))
}

笔记

记住:方法只是个带接收者参数的函数。

methods-continued.go

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 之类的内建类型)的接收者声明方法。

(译注:就是接收者的类型定义和方法声明必须在同一包内;不能为内建类型声明方法。)

  1. MyFloat内部实现还是结构体吧,只是定义一个新的结构体别名
  2. 再为这个结构体添加新的方法。

methods-pointers.go

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())
}

笔记

你可以为指针接收者声明方法。

这意味着对于某类型 `T`,接收者的类型可以用 `*T` 的文法。(此外,`T`不能是像 `*int` 这样的指针。)

例如,这里为 `*Vertex` 定义了 `Scale` 方法。

指针接收者的方法可以修改接收者指向的值(就像 `Scale` 在这做的)。由于方法经常需要修改它的接收者,指针接收者比值接收者更常用。

试着移除第 16 行 `Scale` 函数声明中的 `*`,观察此程序的行为如何变化。

若使用值接收者,那么 `Scale` 方法会对原始 `Vertex` 值的副本进行操作。(对于函数的其它参数也是如此。)`Scale` 方法必须用指针接受者来更改 `main` 函数中声明的 `Vertex` 的值。
  1. 什么是指针接受者?什么是值接受者?
  2. 这里有点类似原型模式了。在JavaScript可以为一个方法(对象)添加原型方法,后续大家都可以调用该方法。
  3. 在这里的函数Scale参数是指针的,修改其值会传导原始的值。

methods-pointers-explained.go

package main

import (
    "fmt"
    "math"
)

type Vertex struct {
    X, Y float64
}

func Abs(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func Scale(v Vertex, f float64) {
    v.X = v.X * f
    v.Y = v.Y * f
}

func main() {
    v := Vertex{3, 4}
    Scale(v, 10)
    fmt.Println(Abs(v))
}

笔记

  1. v *Vertex -> v Vertex
  2. 去掉参数里的*,参数就从指针传递变成值传递,这就意味着是否可以改变原始值
  3. 第2行函数,只是改变了副本的值,并没有改变原始值。

indirection.go

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)
}

笔记

比较前两个程序,你大概会注意到带指针参数的函数必须接受一个指针:

var v Vertex
ScaleFunc(v, 5)  // 编译错误!
ScaleFunc(&v, 5) // OK
而以指针为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
v.Scale(5)  // OK
p := &v
p.Scale(10) // OK
对于语句 v.Scale(5),即便 v 是个值而非指针,带指针接收者的方法也能被直接调用。 也就是说,由于 Scale 方法有一个指针接收者,为方便起见,Go 会将语句 v.Scale(5) 解释为 (&v).Scale(5)。
  1. 感觉这里有坑,go语言自动帮你做了转换。
  2. 如果调用的方法里有一个指针的参数,go自动会取到值的引用当作函数的参数,去调用对应的函数。
  3. 第一个例子,先将值变大了2倍,然后扩大了10倍。
  4. 第二个例子,扩大了3倍。p是一个指针,可以直接传递到函数内部。

indirection-values.go

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 AbsFunc(v Vertex) float64 {
    return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func main() {
    v := Vertex{3, 4}
    fmt.Println(v.Abs())
    fmt.Println(AbsFunc(v))

    p := &Vertex{4, 3}
    fmt.Println(p.Abs())
    fmt.Println(AbsFunc(*p))
}

笔记

同样的事情也发生在相反的方向。

接受一个值作为参数的函数必须接受一个指定类型的值:

var v Vertex
fmt.Println(AbsFunc(v))  // OK
fmt.Println(AbsFunc(&v)) // 编译错误!
而以值为接收者的方法被调用时,接收者既能为值又能为指针:

var v Vertex
fmt.Println(v.Abs()) // OK
p := &v
fmt.Println(p.Abs()) // OK
这种情况下,方法调用 p.Abs() 会被解释为 (*p).Abs()。

思考
1. 什么是同样的事情?
2. 第二个例子里面,p是一个指向Vertex的指针,go自动会将p解释为(*p)。

methods-with-pointer-receivers.go

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("Before scaling: %+v, Abs: %v\n", v, v.Abs())
    v.Scale(5)
    fmt.Printf("After scaling: %+v, Abs: %v\n", v, v.Abs())
}

笔记

使用指针接收者的原因有二:

首先,方法能够修改其接收者指向的值。

其次,这样可以避免在每次调用方法时复制该值。若值的类型为大型结构体时,这样做会更加高效。

在本例中,Scale 和 Abs 接收者的类型为 *Vertex,即便 Abs 并不需要修改其接收者。
  1. 传值和传引用,根据使用情况不同的case进行选择。
  2. 在这个例子中Scale 和Abs都变成结构体的方法,并传递了引用,所以参数值会被改变,并一直传递下去。

参考链接

  1. 方法
  2. 方法即函数
  3. 方法续
  4. 指针接收者
  5. 指针与函数
  6. 方法与指针重定向
  7. 方法与指针重定向(续)
  8. 选择值或指针作为接收者

发表评论

电子邮件地址不会被公开。 必填项已用*标注