Go语言 方法

导言

  • 原文链接: 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

同名方法能定义在不同类型上。而对于函数,它们不能同名。

举个例子,假设我们有 SquareCircle结构,我们能为它们添加同名的方法 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 下载。

优质内容来之不易,您可以通过该 链接 为我捐赠。


最后

感谢原作者的优质内容。

欢迎指出文中的任何错误。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值