文章目录
导言
- 原文链接: Part 17: Methods in Go
- If translation is not allowed, please leave me in the comment area and I will delete it as soon as possible.
方法
介绍
方法是个有接收器的函数,接收器可以是结构体类型,也可以是非结构体类型。
方法的声明句式如下:
func (t Type) methodName(parameter list) {
// method body
}
上面的代码段,创建了一个名为 methodName
、接收器类型为 Type
的方法。
t
被叫作接收器,能在方法体内。
方法实例
接下来,我们在结构体上创建方法。
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
结构 上,创建了 displaySalary
方法。
在第 17
行,displaySalary
方法 访问了 接收器e
。
在第 26
行,我们使用 emp1.displaySalary()
,调用了 displaySalary
方法。
程序输出如下:
Salary of Sam Adolf is $5000.
方法 vs 函数
接下来,我们使用函数,重写上面的程序:
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
方法 被改为 displaySalary
函数,该函数参数是 Employee
类型 的结构体。
程序同样会输出:
Salary of Sam Adolf is $5000
此时你可能有疑问:方法能做的,函数都能做,为何我们还要方法呢?
下面就来说说原因。
原因1
Go
不是纯粹面向对象的语言,它不支持类。因此,通过在类型上创建方法,我们可以创建出一个"类"。
在上面的程序中,通过在 Employee
结构 上创建方法,Employee
类型 有了一组特定的行为,此时它相当于一个"类"。
原因2
同名方法能定义在不同类型上。而对于函数,它们不能同名。
举个例子,假设我们有 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
。而使用 changeAge
方法,我们可以改变 Employee
结构 的 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
拥有一个指针接收器。
实际上,Go
语言 为我们提供了一些语法帮助:我们可以使用 e.Change(51)
去代替 (&e).changeAge(51)
,因为 Go
语言 会帮我们把 e.Change(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)
}
程序同样会输出:
Employee name before change: Mark Andrew
Employee name after change: Mark Andrew
Employee age before change: 50
Employee age after change: 51
值接收器与指针接收器的使用时机
我已经在下面列出使用时机了:
-
当希望方法内对接收器的修改,在方法外可见时,我们使用指针接收器。
-
当拷贝一个数据结构花费很高时,我们也可以使用指针接收器。
想一下,假如有个结构体,它有很多字段。如果此时采用值接收器,势必导致数据拷贝,代价十分高昂。而如果采用指针接收器,我们可以将指向该数据结构的指针,传入方法体,从而防止拷贝的发生。
-
其他情况下,我们使用值接收器。
方法提升
假如 结构a
内嵌了 匿名结构b
,此时我们就可以通过 a
类型 的变量,调用属于 b
结构 的方法。
可以参考 Go语言 结构体 提升字段部分。
下面用代码解释:
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.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)
函数,有个值参数。
第 16
行的 func (r rectangle) area()
,有个值接收器。
在第 25
行,通过 area(r)
,我们调用了 area
函数。在下一行,通过 r.area()
,我们调用了 area
方法。 (r
是值)
在第 28
行,我们创建了一个指针 p
,它指向 r
。
此时,如果我们将 p
传递给 area
函数,编译器会报错,报错信息为:
compilation error, cannot use p (type *rectangle) as type rectangle in argument to area
现在,来看看棘手的部分。
在第 35
行,通过 p.area()
,我们成功调用了 area
方法。
这看似有点不合常理 — area
方法 是值接收器,而 p
是指针。
但这是完全合法的,因为 Go
编译器 会帮我们把 p.area()
解释成 (*p).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
函数,它有 1
个指针参数。
在第 17
行,我们定义了 perimeter
方法,它有 1
个 指针接收器。
在第 27
行,通过 perimeter(p)
,我们调用了 perimeter
函数。
在第 28
行,通过 p.perimeter()
,我们调用了 perimeter
方法。
一切正常运作~
在被注释的第 33
行,我们试着使用 值参数r
,调用 perimeter
函数。
这并不可行,因为 perimester
函数 是指针参数,而 r
是值。
如果你执意运行,编译器会抛出错误:
main.go:33: cannot use r (type rectangle) as type *rectangle in argument to perimeter.
在第 35
行,通过 r.perimeter()
,我们调用了 perimeter
方法,
这看似不合常理 — perimeter
方法 是指针接收器,而 r
是值。
但这是合法的,因为 Go
编译器 会帮我们把 r.perimeter()
解释成 (&r).perimeter()
。
程序输出如下:
perimeter function output: 30
perimeter method output: 30
perimeter method output: 30
在非结构体类型上创建方法
到目前为止,我们都只是在 结构类型 上创建方法。但其实,我们也可以给 非结构类型 创建方法,但有个注意点:类型的定义、该类型上方法的定义 必须在同一包内。
package main
func (a int) add(b int) {
}
func main() {
}
在上面程序的第 3
行,我们试着为 内建类型int
,添加 add
方法。
但这是不允许的,因为 add
方法 的定义 与 int
类型 的定义并不在同一包内。
int
类型 在builtin
包 定义,而该例子的add
方法 在main
包 定义。
运行上面的程序,编译器会抛出如下错误:
cannot define new methods 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
行,我们创建了一个 类型myInt
,它来自 int
。在第 7
行,我们为 myInt
定义了一个 add
方法。
程序输出如下:
This program will print Sum is 15.
这就是方法了~
祝你腰好腿好身体好~
原作者留言
我已经把上面的概念整合到了一个程序,你可以在 github 下载。
优质内容来之不易,您可以通过该 链接 为我捐赠。
最后
感谢原作者的优质内容。
欢迎指出文中的任何错误。