本文为译文 Methods in Go
一个method就是一个函数在func关键字和method name之间有一个特殊的receiver。这个receiver可以是结构体类型,也可以是非结构体类型。
func (t Type) methodName(parameter list) {
}
上面的代码为Type类型创建了一个methodName方法。其中变量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行,我们在Empolyee类型上创建了一个displaySalary方法。
Methods vs Functions
上面的程序我们也可以单独写成函数的形式。
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)
}
上面的程序和我们使用方法的时候,输出是完全一样的。所以,为啥我们还需要有方法的这个概念呢?然我们一块来梳理一下
-
首先,go不是一个很纯粹的面向对象编程的语言,它不支持类。因此,类型上的方法是一种去实现了类功能差不多的形式。方法允许对相关的行为进行分组。拿上面的例子来说,所有和
Employee类型相关的可以通过方法组织在一起。比如我们可以添加方法calculatePension``calculateLeaves -
有相同名字的方法可以被定义为不同的类型。但是函数就不可以。假设我们有
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)
/**
use e.changeAge(51) instead of (&e).changeAge(51)
and it prints the same output
*/
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
在36行,我们使用(&e).changeAge(51)去调用changeAge方法。其实我们也可以这样写e.changeAge(51)go会自动把这个变成(&e).changeAge(51)
什么时候用指针接收器什么时候用值接收器
- 当你期望当方法内对数据的变化,想同步体现到调用者的时候。
- 当复制数据结构比较昂贵的时候,也推荐用指针接收器。比如一个结构体有很多字段,如果用值接收器则会将整个结构体做复制,如果数据量比较大的话,代价就是很高的。如果使用”指针接收器“则不会有复制的操作了。
剩余的其他情况,都建议使用值接收器。
匿名结构体字段的方法
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
}
In line no. 32 of the program above, we call the fullAddress() method of the address struct using p.fullAddress(). The explicit direction p.address.fullAddress() is not needed.
方法的值接收器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
}
最后一行 **p.area()**能工作的原因,这句话会被go解释为***(p).area()**因为**area**接受的是值类型。
方法的指针接收器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
}
最后一行可以的原因也是go会把**r.perimeter()**转换为**(&r).perimeter()**
Methods with non-struct receivers
以上我们定义了结构体类型的方法。我们也可以定义非结构体的方法。去定义一个类型的方法,接受类型的定义和方法的定义应该存在在相同的包中。
package main
func (a int) add(b int) {
}
func main() {
}
上面的程序会报错,因为方法add和类型内置类型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)
}