方法
Q7nl1s admin

方法简介

方法只是一个函数,在func关键字和方法名称之间有一个特殊的接收器类型。接收器可以是一个结构类型,也可以是非结构类型。

下面提供了方法声明的语法。

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

上面的片段创建了一个名为methodName的方法,其接收器类型为Typet被称为接收器,它可以在方法中被访问。

方法示例

让我们写一个简单的程序,在一个结构类型上创建一个方法并调用它。

1
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
package main

import (
"fmt"
)

type Employee struct {
name string
salary int
currency string
}

/*
displaySalary() method has Employee as the receiver type
*/
func (e Employee) displaySalary() {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
emp1 := Employee {
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
emp1.displaySalary() //Calling displaySalary() method of Employee type
}

在上述程序的第16行中,我们在Employee结构类型上创建了一个方法displaySalarydisplaySalary()方法可以访问其内部的接收器e。在第17行,我们使用接收器e,并打印雇员的姓名、货币和工资。

在第26行中,我们使用语法emp1.displaySalary()调用该方法。

这个程序打印Salary of Sam Adolf is $5000

方法与函数

上述程序可以只用函数而不用方法来重写。

1
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
package main

import (
"fmt"
)

type Employee struct {
name string
salary int
currency string
}

/*
displaySalary() method converted to function with Employee as parameter
*/
func displaySalary(e Employee) {
fmt.Printf("Salary of %s is %s%d", e.name, e.currency, e.salary)
}

func main() {
emp1 := Employee{
name: "Sam Adolf",
salary: 5000,
currency: "$",
}
displaySalary(emp1)
}

在上面的程序中,displaySalary方法被转换为一个函数,Employee结构被作为一个参数传递给它。这个程序也产生了完全相同的输出——Salary of Sam Adolf is $5000

那么,既然我们可以用函数编写同样的程序,为什么还要用方法呢?这其中有几个原因。让我们逐一来看一下。

  • Go不是一种纯面向对象的编程语言,它不支持类。因此,类型上的方法是实现类似于类的行为的一种方式。方法允许对与类型相关的行为进行逻辑分组,类似于类。在上面的示例程序中,所有与Employee类型相关的行为都可以通过使用Employee接收器类型来创建方法进行分组。例如,我们可以添加诸如计算养老金、计算休假等方法。
  • 同名的方法可以定义在不同的类型上,而同名的函数是不允许的。让我们假设我们有一个SquareCircle结构。我们可以在SquareCircle上都定义一个名为Area的方法。这在下面的程序中已经完成。
1
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
30
31
32
33
34
35
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())
}

这个程序会打印出来:

1
2
Area of rectangle 50  
Area of circle 452.389342

上述方法的属性是用来实现接口的。我们将在下一个教程中处理接口时详细讨论这个问题。

指针接收器与值接收器

到目前为止,我们只看到了带有值接收器的方法。我们也可以创建带有指针接收器的方法。值接收器和指针接收器之间的区别是,在一个带有指针接收器的方法中所作的改变对调用者来说是可见的,而在值接收器中则不是这样的。让我们借助于一个程序来理解这一点。

1
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
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
)

type Employee struct {
name string
age int
}

/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}

/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}

func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)

fmt.Printf("\n\nEmployee age before change: %d", e.age)
(&e).changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}

在上面的程序中,changeName方法有一个值接收器(e Employee),而changeAge方法有一个指针接收器(e *Employee)。在changeName中对Employee结构的名称字段所做的修改对调用者来说是不可见的,因此程序在第32行调用e.changeName("Michael Andrew")方法之前和之后都打印了相同的名称。由于changeAge方法有一个指针接收器(e *Employee),在调用方法(&e).changeAge(51)之后,age字段的变化将对调用者可见。这个程序会打印:

1
2
3
4
5
Employee name before change: Mark Andrew  
Employee name after change: Mark Andrew

Employee age before change: 50
Employee age after change: 51

在上述程序的第36行中,我们使用(&e).changeAge(51)来调用changeAge方法。由于changeAge有一个指针接收器,我们使用了(&e)来调用该方法。这是不需要的,语言让我们选择直接使用e.changeAge(51)e.changeAge(51)将被Go语言解释为(&e).changeAge(51)

下面的程序被改写为使用e.changeAge(51)而不是(&e).changeAge(51),它打印出相同的输出。

1
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
30
31
32
33
34
35
36
37
38
package main

import (
"fmt"
)

type Employee struct {
name string
age int
}

/*
Method with value receiver
*/
func (e Employee) changeName(newName string) {
e.name = newName
}

/*
Method with pointer receiver
*/
func (e *Employee) changeAge(newAge int) {
e.age = newAge
}

func main() {
e := Employee{
name: "Mark Andrew",
age: 50,
}
fmt.Printf("Employee name before change: %s", e.name)
e.changeName("Michael Andrew")
fmt.Printf("\nEmployee name after change: %s", e.name)

fmt.Printf("\n\nEmployee age before change: %d", e.age)
e.changeAge(51)
fmt.Printf("\nEmployee age after change: %d", e.age)
}

何时使用指针接收器,何时使用值接收器

一般来说,当方法内部对接收器的改变对调用者来说应该是可见的,就可以使用指针接收器。

指针接收器也可以用在那些复制数据结构成本很高的地方。考虑一个有许多字段的结构。在方法中使用这个结构作为一个值接收器,需要复制整个结构,这将是很昂贵的。在这种情况下,如果使用一个指针接收器,该结构将不会被复制,在方法中只使用它的一个指针。

在所有其他情况下,可以使用值接收器。

匿名结构字段的方法

属于结构的匿名字段的方法可以被调用,就像它们属于定义匿名字段的结构一样。

1
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
30
31
32
33
34
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() //accessing fullAddress method of address struct

}

在上述程序的第32行,我们用p.fullAddress()调用address结构的fullAddress()方法。在第32行中,我们使用p.fullAddress()调用address结构的fullAddress()方法。明确的方向p.address.fullAddress()是不需要的。这个程序打印出

1
Full address: Los Angeles, California  

方法中的值接收方与函数中的值参数

这个话题让大多数新手头疼不已。我将尽量把它说清楚😀。

当一个函数有一个值参数时,它将只接受一个值参数。

当一个方法有一个值接收器时,它将同时接受指针和值接收器。

让我们通过一个例子来理解这一点。

1
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
30
31
32
33
34
35
36
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()//calling value receiver with a pointer
}

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

在第25行,我们用一个值参数调用area(r)函数,它将工作。同样,我们用一个值接收器调用 area 方法 r.area(),这也会起作用。

我们在第28行创建一个指向r的指针p。如果我们试图将这个指针传递给只接受一个值的函数 area,编译器会报错。我对第33行做了注释。如果你不注释这一行,那么编译器将抛出编译错误,不能将p(type *rectangle)作为矩形类型的参数传给 area。这和预期的一样。

现在棘手的部分来了,第35行的代码p.area()调用了方法area,它只接受一个使用指针接收器p的值接收器,这是完全有效的。原因是p.area()这一行,为了方便起见,将被Go解释为(*p).area(),因为 area 有一个值接收器。

1
2
3
Area Function result: 50  
Area Method result: 50
Area Method result: 50

方法中的指针接收器与函数中的指针参数

与值参数类似,带有指针参数的函数将只接受指针,而带有指针接收器的方法将同时接受指针和值接收器。

1
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
30
31
32
33
34
35
36
37
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()//calling pointer receiver with a value

}

上述程序的第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,这是允许的,为了方便起见,这行代码r.perimeter()将被语言解释为(&r).perimeter()。这个程序将输出。

1
2
3
perimeter function output: 30  
perimeter method output: 30
perimeter method output: 30

具有非结构接收器的方法

到目前为止,我们只在结构类型上定义了方法。也可以在非结构类型上定义方法,但有一个问题。要在一个类型上定义方法,接收器类型的定义和方法的定义应该存在于同一个包中。到目前为止,我们定义的所有结构和结构上的方法都位于同一个main包中,因此它们都能工作。

1
2
3
4
5
6
7
8
package main

func (a int) add(b int) {
}

func main() {

}

在上面的程序中,在第3行,我们试图在内置类型int上添加一个名为add的方法。这是不允许的,因为add方法的定义和int类型的定义不在同一个包里。这个程序将抛出编译错误不能在非本地类型int上定义新方法。

让这个程序正常工作的方法是为内置类型int创建一个类型别名,然后用这个类型别名创建一个方法作为接收器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

type myInt int

func (a myInt) add(b myInt) myInt {
return a + b
}

func main() {
num1 := myInt(5)
num2 := myInt(10)
sum := num1.add(num2)
fmt.Println("Sum is", sum)
}

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

这个程序将打印Sum is 15

Go中的方法就介绍到这里。祝你有个愉快的一天。

 Comments
Comment plugin failed to load
Loading comment plugin
Powered by Hexo & Theme Keep
Unique Visitor Page View