方法

1 什么是方法?

方法其实就是一个函数,在 func 这个关键字和方法名中间加入了一个特殊的接收器类型。接收器可以是结构体类型或者是非结构体类型。接收器是可以在方法的内部访问的。

Go语言中的方法(Method)是一种作用于特定类型变量的函数。这种特定类型变量叫做接收者(Receiver)。接收者的概念就类似于其他语言中的this或者 self。在python中就是绑定给对象的方法,哪个对象来调用,就把自己self传过去。

下面就是创建一个方法的语法。

func (t Type) methodName(parameter list) {
}

func (接收者变量 接收者类型) 方法名(参数列表) (返回参数) {
    函数体
}

其中,

  • 接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名称首字母的小写,而不是selfthis之类的命名。例如,Person类型的接收者变量应该命名为 pConnector类型的接收者变量应该命名为c等。
  • 接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。
  • 方法名、参数列表、返回参数:具体格式与函数定义相同。

举个例子:

package main

import (
    "fmt"
)

//Person 结构体
type Person struct {
    name string
    age  int8
}

//普通函数dream
func dream() {
    fmt.Println("我的梦想是学好Go语言!")
}

//Dream函数指定接收者后就是方法
func (p Person) Dream() {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name) // p类似python中的self,谁调用Dream方法,就把谁传进来(p就是谁)
}

func main() {
    //dream() 调用普通函数 
    var p1 = Person{  //实例化得到p1, p1就可以调用Dream方法
        name:"egon",
        age:18,}
    p1.Dream()    
}

上面程序输出为:egon的梦想是学好Go语言!

方法与函数的区别是,函数不属于任何类型,方法属于特定的类型。

函数谁都可以调用。方法是某个具体的类型才能调用的函数。

 

2 为什么我们已经有函数了还需要方法呢?

我们把上面的程序重写成只使用函数:

package main

import (
    "fmt"
)

type Person struct {
    name string
    age  int
}

func Dream(p Person) {
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := Person{name: "egon", age: 18}
    Dream(p1)
}

在上面的程序中,Dream 方法被转化为一个函数,Person 结构体被当做参数传递给它。这个程序也产生完全相同的输出:egon的梦想是学好Go语言!

既然我们可以使用函数写出相同的程序,那么为什么我们需要方法?这有着几个原因,让我们一个个的看看。

  • Go 不是纯粹的面向对象编程语言,而且Go不支持类。因此,基于类型的方法是一种实现和类相似行为的途径。
  • 相同的名字的方法可以定义在不同的类型上,而相同名字的函数是不被允许的。假设我们有一个 Square 和 Circle 结构体。可以在 Square 和 Circle 上分别定义一个 Area 方法。见下面的程序:
package main

import (
    "fmt"
    "math"
)

type Rectangle struct {
    length int
    width  int
}

type Circle struct {
    radius float64
}

func (r Rectangle) Area() int {
    return r.length * r.width
}

func (c Circle) Area() float64 {
    return math.Pi * c.radius * c.radius
}

func main() {
    r := Rectangle{
        length: 10,
        width:  5,
    }
    fmt.Printf("Area of rectangle %d\n", r.Area())
    c := Circle{
        radius: 12,
    }
    fmt.Printf("Area of circle %f", c.Area())
}

该程序输出:

Area of rectangle 50
Area of circle 452.389342

上面方法的属性被使用在接口中。我们将在接下来的教程中讨论这个问题。

 

3 指针接收者与值接收者

指针类型的接收者由一个结构体的指针组成,由于指针的特性,调用方法时修改接收者指针的任意成员变量,在方法结束后,修改都是有效的。这种方式就十分接近于其他语言中面向对象中的this或者self

package main

import (
    "fmt"
)

type Person struct {
    name string
    age  int
}

func (p *Person) Dream() {
    p.age = 99
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := Person{name: "egon", age: 18}
    (&p1).Dream()
    fmt.Println(p1.age)
}

上面程序中,Dream方法的接收者是一个指针; 实例化得到的结构体p1,需要用指针调用Dream方法,使用&p1语法,Go语言支持直接使用p1.Dream

上面程序输出为:

egon的梦想是学好Go语言!
99

当方法作用于值类型接收者时,Go语言会在代码运行时将接收者的值复制一份。在值类型接收者的方法中可以获取接收者的成员值,但修改操作只是针对副本,无法修改接收者变量本身。

package main

import (
	"fmt"
)

type Person struct {
    name string
    age  int
}

func (p Person) Dream() {
    p.age = 99
    fmt.Printf("%s的梦想是学好Go语言!\n", p.name)
}

func main() {
    p1 := Person{name: "egon", age: 18}
    p1.Dream()
    fmt.Println(p1.age)
}

上面程序输出为:

egon的梦想是学好Go语言!
18

什么时候应该使用指针类型接收者?

  1. 对方法内部的接收者所做的改变应该对调用者可见时。
  2. 当拷贝一个结构体的代价过于昂贵时。考虑下一个结构体有很多的字段。在方法内使用这个结构体做为值接收者需要拷贝整个结构体,这是很昂贵的。在这种情况下使用指针接收者,结构体不会被拷贝,只会传递一个指针到方法内部使用。
  3. 保证一致性,如果有某个方法使用了指针接收者,那么其他的方法也应该使用指针接收者。

 

4 匿名字段的方法

属于结构体的匿名字段的方法可以被直接调用,就好像这些方法是属于定义了匿名字段的结构体一样。

package main

import (
    "fmt"
)

type address struct {
    city  string
    state string
}

func (a address) fullAddress() {
    fmt.Printf("Full address: %s, %s", a.city, a.state)
}

type person struct {
    firstName string
    lastName  string
    address
}

func main() {
    p := person{
        firstName: "Elon",
        lastName:  "Musk",
        address: address {
            city:  "Los Angeles",
            state: "California",
        },
    }

    p.fullAddress() //访问 address 结构体的 fullAddress 方法
}

在上面程序的第 32 行,我们通过使用 p.fullAddress() 来访问 address 结构体的 fullAddress() 方法。明确的调用 p.address.fullAddress() 是没有必要的,p.fullAddress()访问提升字段的方法,就像它是在结构体 p 中声明的一样。该程序输出:

Full address: Los Angeles, California

还是以上面的程序为例,如果person结构体内的方法,和address结构体内的方法重名,又是如何取值呢?

package main

import (
    "fmt"
)

type address struct {
    city  string
    state string
}

func (a address) fullAddress() {
    fmt.Printf("Full address: %s, %s", a.city, a.state)
}

func (p person) fullAddress() {
    p.city = "成都"  //字段提升
    p.state = "四川"
    fmt.Printf("Full address: %s, %s", p.city, p.state)
}

type person struct {
    firstName string
    lastName  string
    address
}

func main() {
    p := person{
        firstName: "Elon",
        lastName:  "Musk",
        address: address{
            city:  "Los Angeles",
            state: "California",
        },
    }

    p.fullAddress()         //访问 person 结构体的 fullAddress 方法
    fmt.Println()
    p.address.fullAddress() //访问 address 结构体的 fullAddress 方法
}

子类和父类方法重名,优先使用自己的,类似于面向对象的派生,子类重写了父类的方法。上面的程序输出为:

Full address: 成都, 四川
Full address: Los Angeles, California

 

5 任意类型添加方法

在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。

前提条件:不能给别的包定义的类型添加方法,也就是说,方法的接收者类型定义和方法的定义应该在同一个包中。比如,intGo语言内置包定义的类型,所以我们不能为int添加方法。

但是我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。

package main

import "fmt"

type myInt int

func (m myInt) add(b myInt) myInt { //接收者是myInt类型, 方法的参数也是myInt类型, 返回值也是myInt类型
    return m + b
}

func main() {
    num1 := myInt(5)       //实例化得到myInt类型的接收者(对象),并初始化值
    num2 := myInt(10)
    sum := num1.add(num2)  //num1调用add方法,把自己传进去
    fmt.Println("Sum is", sum)
}

在上面程序的第5行,我们为 int 创建了一个类型别名 myInt。在第7行,我们定义了一个以 myInt 为接收者的的方法 add

该程序将会打印出 Sum is 15

 

6 在方法中使用值接收者 与 在函数中使用值参数

当一个函数有一个值参数,它只能接受一个值参数。只能传值,不能传指针。

当一个方法有一个值接收者,它可以接受值接收者和指针接收者。可以用值来调,也可以用指针来调。

package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func area(r rectangle) {
    fmt.Printf("Area Function result: %d\n", (r.length * r.width))
}

func (r rectangle) area() {
    fmt.Printf("Area Method result: %d\n", (r.length * r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    area(r)
    r.area()

    p := &r
    /*
       compilation error, cannot use p (type *rectangle) as type rectangle
       in argument to area
    */
    //area(p)

    p.area()//通过指针调用值接收器
}

第 12 行的函数 func area(r rectangle) 接受一个值参数,方法 func (r rectangle) area() 接受一个值接收者。

在第 25 行,我们通过值参数 area(r) 来调用 area 这个函数,这是合法的。同样,我们使用值接收器来调用 area 方法 r.area(),这也是合法的。

在第 28 行,我们创建了一个指向 r 的指针 p。如果我们试图把这个指针传递到只能接受一个值参数的函数 area,编译器将会报错。所以我把代码的第 33 行注释了。如果你把这行的代码注释去掉,编译器将会抛出错误 compilation error, cannot use p (type *rectangle) as type rectangle in argument to area.

在第35行的代码 p.area() 使用指针接收者 p 调用了只接受一个值接收者的方法 area,这是完全有效的。原因是当 area 有一个值接收者时,为了方便Go语言把 p.area() 解释为 (*p).area()。该程序将会输出:

Area Function result: 50
Area Method result: 50
Area Method result: 50

 

7 在方法中使用指针接收者 与 在函数中使用指针参数

和值参数相类似,函数使用指针参数只接受指针,而使用指针接收器的方法可以使用值接收器和指针接收器。

package main

import (
    "fmt"
)

type rectangle struct {
    length int
    width  int
}

func perimeter(r *rectangle) {
    fmt.Println("perimeter function output:", 2*(r.length+r.width))

}

func (r *rectangle) perimeter() {
    fmt.Println("perimeter method output:", 2*(r.length+r.width))
}

func main() {
    r := rectangle{
        length: 10,
        width:  5,
    }
    p := &r //pointer to r
    perimeter(p)
    p.perimeter()

    /*
        cannot use r (type rectangle) as type *rectangle in argument to perimeter
    */
    //perimeter(r)

    r.perimeter()//使用值来调用指针接收器
}

在上面程序的第 12 行,定义了一个接受指针参数的函数 perimeter。第 17 行定义了一个有一个指针接收者的方法。

在第 27 行,我们调用 perimeter 函数时传入了一个指针参数。在第 28 行,我们通过指针接收者调用了 perimeter 方法,都是合法的。

在被注释掉的第 33 行,我们尝试通过传入值参数 r 调用函数 perimeter,这是不被允许的,因为函数的指针参数不接受值传参。如果你把这行的代码注释去掉并把程序运行起来,编译器将会抛出错误 main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.

在第 35 行,我们通过值接收器 r 来调用有指针接收器的方法 perimeter。这是被允许的,为了方便Go语言把代码 r.perimeter() 解释为 (&r).perimeter()。该程序输出:

perimeter function output: 30
perimeter method output: 30
perimeter method output: 30

 

posted @ 2022-11-19 17:59  不会钓鱼的猫  阅读(108)  评论(0编辑  收藏  举报