欢迎来到Golang系列教程的第17部分
什么是方法?
方法只是一个具有特殊接收器类型的函数,该接收器在 func
关键字和方法名称之间编写。接收器即可以是结构体类型,也可以是非结构体类型。接收器可以在方法内部访问。
下面是创建方法的语法
func (t Type) methodName(parameter list) {
}
上面片段创建一个名为 methodName
的方法,它有一个类型为 Type
的接收器。
方法示例
让我们写一个简单的在结构体类型上面创建方法并调用的程序。
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
上创建一个方法 desplaySalary
。方法 displaySalary() 在它内部访问接收器 e Employee
。在第 17 行,我们使用接收器 e
并打印employee的 name,currency 和 salary。
在第 26 行,我们使用语法 emp1.displaySalary()
调用方法。
这个程序打印 Salary of Sam Adolf is $5000
。
我们有函数为什么还要方法?
上面的程序被下面仅使用函数而没有方法重写。
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不是一个纯面向对象的语言,它并不支持类。因此基于类型的方法是一种达到和类相同样行为的方式。
- 拥有相同名字的方法可以在不同的类型上定义,然而拥有相同名字的函数是不被允许的。我们假设我们有一个
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
方法的上述属性用于接口,当我们处理接口,我们将在下一教程讨论。
指针接收器 vs 值接收器
目前我们已经看到仅使用值接收器的方法。可以创建指针接收器的方法。值接收器和指针接收器的不同之外在于,在方法内部修改指针接收器对于调用者来说是可见的,而在值接收器则不是。让我们使用下面的辅助程序来理解它。
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
的 name
字段对于调用者并不可见,因此程序在方法 e.changeName("Michael Andrew")
调用前后打印相同名字。而方法 changeAge
工作在指针接收器 (e *Employee)
上,方法调用 (&e).changeAge(51)
对 age
字段的修改对调用者来说是可见的。程序打印
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)
将被语言解读为 (&e).changeAge(51)
。
下面的程序被使用 e.changeAge(51)
代替 (&e).changeAge(51)
重写,它打印同样的输出
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)
}
何时使用指针接收器和值接收器
一般当在方法内修改接收器应该对调用者可见就使用指针接收器。
指针接收器也可以用在拷贝一个数据结构体代价比较昂贵的地方。考虑到结构体有很多字段。使用这个结构体作为方法的接收器将需要拷贝整个结构体,这将是昂贵的。在这个例子中如果使用指针接收器,结构体将不会拷贝,仅在方法中使用指针。
在所有其他的情况下可以使用值接收器。
匿名字段的方法
调用属于结构体匿名字段的方法就如同调用匿名字段定义时所属的结构体
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
。明确的直接 p.address.fullAddress()
是不必要的。这个程序打印
Full address: Los Angeles, California
方法的值接收器 vs 函数的值参数
这个主题大都是新手。我尝试着尽可能清晰
当一个函数有一个值参数。它将只能接收一个值参数。
当一个方法有一个值接收器,它将同时接收指针和值接收器。
让我们通过一个例子来理解这一点。
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
接收一个值参数而方法 func (r rectangle area()
接收一个值接收器。
在第 25行,我们使用一个值参数 area(r)
调用 area 函数它有效果,同样地我们使用值接收器调用 r.area()
,它也有效果。
我们在 28 行创建指针 p
指向 r
。如果我们试图把这个指针传给仅接收一个值参数的 area 函数,编译器将抱怨。我已经在 33 行这样做注释掉。如果取消注释这行,编译器将抛出错误*compilation error,cannot use p(tye rectange) as type rectangle in argument to area。这正如预期。
现在来到棘手部分,35行的代码 p.area()
使用指针接收器 p
调用方法 area
,该方法仅接收值接收器,这完全有效。原因是 p.area()
这一行,为了方便将被 Go 解读为 (*p).area()
,由于 area
有一个值接收器。
这个程序将输出
Area Function result: 50
Area Method result: 50
Area Method result: 50
方法的指针接收器 vs 函数的指针参数
和值参数一样,拥有指针参数的函数将仅接收指针,而拥有指针接收器的方法可以同时接受值和指针接收器。
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()
。程序将输出
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30
非结构体类型上的方法
目前我们仅在结构体类型上定义方法。也可以在非结构体类型上定义方法但这儿需要牢记。为了在类型上定义方法,方法的接收器类型的定义和方法的定义必须在同一个包。目前为止,所有的结构体和我们定义在结构体上的方法都位于 main
包,因此他们可以工作。
package main
func (a int) add(b int) {
}
func main() {
}
上面程序,在第 3 行 我们试图在内置类型 int
上添加一个名为 add
的方法。这是不允许的,因为方法 add
的定义和类型 int
的定义并不在同一个包中。这个程序将抛汇编错误cannot define new method on non-local type int
让它工作的方法是创建一个内置类型 int 的类型别名。然后以这个类型噣为接收器创建方法。
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
。
我已经创建一个包含所有目前我们讨论的程序,可以在github上获得
下一教程 接口 - I