21

【Go语言入门系列】(七)如何使用Go的方法?

 3 years ago
source link: https://segmentfault.com/a/1190000023838316
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【Go语言入门系列】 前面的文章:

本文介绍Go语言的方法的使用。

1. 声明

如果你用过面向对象的语言,比如Java,那你肯定对类、对象、成员变量、方法等很熟悉。

比如说,狗(类),名字(属性),叫(方法),哮天犬(对象)。

但是Go语言中并没有类,自然也没有面向对象中的成员变量和成员方法。但是Go语言中有类似的概念——结构体,结构体中的字段可以看做类中成员属性。

Go中也有类似于面向对象中方法的概念,也叫方法( method ),这种方法其实是一种特殊的函数( function )——带有接收者( receiver )的函数。

方法的声明方式如下:

func (接受者) funcName(参数们) (返回值们)

可以看出方法的声明方式和函数的声明方式差不多,但是多了一个接收者,该接收者是一个结构体类型。下面是一个实例:

package main

import "fmt"

type dog struct {
    name string
}

func (d dog) say() {//方法
    fmt.Println(d.name + " 汪汪汪。。。方法")
}

func main() {
    d := dog{"哮天犬"}
    d.watchDoor()
}

运行:

哮天犬 汪汪汪。。。方法

say() 是一个方法, d 是接收者,是一个结构体类型参数,方法里可以访问接收者的字段:

fmt.Println(d.name + " 汪汪汪。。。方法")

通过 . 可以调用方法:

d.say()

2. 方法和函数

方法 method 是具有接收者 receiver 的特殊函数 function 。下面的例子展示了 methodfunction 之间的区别。

package main

import "fmt"

type dog struct {
    name string
}

func (d dog) say() {
    fmt.Println(d.name + " 汪汪汪。。。方法")
}

func say(d dog) {
    fmt.Println(d.name + " 汪汪汪。。。函数")
}

func main() {
    d := dog{"哮天犬"}
    d.watchDoor()
    watchDoor(d)
}

运行:

哮天犬 汪汪汪。。。方法
哮天犬 汪汪汪。。。函数

你可能会问,在这个例子中,既然方法和函数的运行结果一样,那使用方法岂不是多此一举,为何不继续使用函数?

换一个场景:现在有狗、猫、兔子等动物,他们都会叫,只是叫声不同:

package main

import "fmt"

type dog struct {
    name string
}

type cat struct {
    name string
}

type rabbit struct {
    name string
}

func dogSay(d dog) {
    fmt.Println(d.name + " 汪汪汪。。。函数")
}

func catSay(c cat)  {
    fmt.Println(c.name + " 喵喵喵。。。函数")
}

func rabbitSay(r rabbit) {
    fmt.Println(r.name + " 吱吱吱。。。函数")
}
func main() {
    d := dog{"哮天犬"}
    c := cat{"加菲猫"}
    r := rabbit{"玉兔"}
    dogSay(d)
    catSay(c)
    rabbitSay(r)
}

运行:

哮天犬 汪汪汪。。。函数
加菲猫 喵喵喵。。。函数
玉兔 吱吱吱。。。函数

上面的三个函数有什么不妥之处呢?

首先,这三个函数都是用来表示 这一行为,一般来说函数名都会叫 say() ,但因为不同的动物,函数名不能相同,为了做区别而做出了改变。

其次, 这个行为应该属于动物,二者在概念上不能分开。比如,说话这个行为是每个人都具有的,但是说话并不能离开人而独自存在。

此时,方法 method 的优点就体现了出来:

package main

import "fmt"

type dog struct {
    name string
}

type cat struct {
    name string
}

type rabbit struct {
    name string
}

func (d dog) say() {
    fmt.Println(d.name + " 汪汪汪。。。方法")
}

func (c cat) say()  {
    fmt.Println(c.name + " 喵喵喵。。。方法")
}

func (r rabbit) say() {
    fmt.Println(r.name + " 吱吱吱。。。方法")
}

func main() {
    d := dog{"哮天犬"}
    c := cat{"加菲猫"}
    r := rabbit{"玉兔"}

    d.say() //调用
    c.say()
    r.say()
}

运行:

哮天犬 汪汪汪。。。方法
加菲猫 喵喵喵。。。方法
玉兔 吱吱吱。。。方法

三个方法的方法名都一样,每个方法都有一个接受者 receiver ,这个 receiver 使方法在概念上属于结构体,就像结构体的字段一样,但是没有写在结构体内。

从这三个方法中可以看出: 只要方法的接收者不同,即使方法名相同,方法也不相同

3. 指针和接收者

接收者可以使用指针,和函数的参数使用指针一样(参考 Go语言入门系列(六)之再探函数 ),接收者使用指针传的是引用,不使用指针传的是值拷贝。看下面一个例子:

package main

import "fmt"

type dog struct {
    name string
}

func (d *dog) rename(name string) {
    d.name = name
    fmt.Println("方法内:" + d.name)
}

func (d dog) rename1(name string)  {
    d.name = name
    fmt.Println("方法内:" + d.name)
}

renamerename1 都是改变名字的方法,一个传引用,一个传值。只有 rename 能真正改变名字。

func main() {
    d := dog{"哮天犬"}
    d.rename("小黑黑")
    fmt.Println(d.name)
}

运行:

方法内:小黑黑
小黑黑

rename 把“哮天犬”改为了“小黑黑”。

func main() {
    d := dog{"哮天犬"}
    d.rename1("小红红")
    fmt.Println(d.name)
}

运行:

方法内:小红红
哮天犬

rename1 只在方法内改变了名字,并没有真正改变“哮天犬”。因为 rename1 接收的是 d 的一个拷贝。

方法的指针接收者可以进行重定向,什么意思呢?下面用四段代码来说明。

如果函数的参数是一个指针参数,那么该函数就必须接收一个指针才行,如果是值则报错:

package main

import "fmt"

func double(x *int) {
    *x = *x * 2
}

func main() {
    i := 2
    double(&i) //编译正确
    double(i) //报错
    fmt.Println(i)
}

而如果方法的接收者是一个指针,那么该方法被调用时,接收者既可以是指针,又可以是值:

package main

import "fmt"

func (d *dog) rename(name string) {
    d.name = name
    fmt.Println("方法内:" + d.name)
}

func main() {
    d := dog{"哮天犬"}
    d.rename("小黑黑") //接收者是值,编译正确
    //(&d).rename("小黑黑") //接收者是指针,编译正确
    fmt.Println(d.name)
}

对于指针接收者来说, d.rename("小黑黑") 被解释为 (&d).rename("小黑黑") ,如此一来,我们就不需要在意调用方法的接收者是否为指针类型,因为Go会进行“重定向”。

同理,反过来也可以。

如果函数的参数是值,而不是指针,那么该函数必须接受值,否则会报错:

package main

import "fmt"

func double(x int) {
    x = x * 2
}

func main() {
    i := 2
    p := &i
    double(*p) //参数是值,编译正确
    //double(p) //参数是指针,报错
    fmt.Println(i)
}

而如果方法的接收者是一个值,那么该方法被调用时,接收者既可以是值,又可以是指针:

package main

import "fmt"

func (d dog) rename1(name string)  {
    d.name = name
    fmt.Println("方法内:" + d.name)
}

func main() {
    d := dog{"哮天犬"}
    p := &d
    p.rename1("小红红") //接收者是指针,编译正确
    //(*p).rename1("小红红") //接收者是值,编译正确
    fmt.Println(d.name)
}

对于值接收者来说, p.rename1("小红红") 被解释为 (*p).rename1("小红红") ,如此一来,我们就不需要在意调用方法的接收者是否为值,因为Go会进行“重定向”。

作者简介

我是 行小观 ,我会在公众号『 行人观学 』中持续更新Java、Go、数据结构和算法、计算机基础等相关文章。

本文章属于系列文章「 Go语言入门系列 」,本系列从Go语言基础开始介绍,适合从零开始的初学者。

欢迎关注,我们一起踏上行程。

如有错误,还请指正。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK