方法简介
方法只是一个函数,在func
关键字和方法名称之间有一个特殊的接收器类型。接收器可以是一个结构类型,也可以是非结构类型。
下面提供了方法声明的语法。
1 | func (t Type) methodName(parameter list) { |
上面的片段创建了一个名为methodName
的方法,其接收器类型为Type
。t
被称为接收器,它可以在方法中被访问。
方法示例
让我们写一个简单的程序,在一个结构类型上创建一个方法并调用它。
1 | package main |
在上述程序的第16行中,我们在Employee
结构类型上创建了一个方法displaySalary
。displaySalary()
方法可以访问其内部的接收器e
。在第17行,我们使用接收器e
,并打印雇员的姓名、货币和工资。
在第26行中,我们使用语法emp1.displaySalary()
调用该方法。
这个程序打印Salary of Sam Adolf is $5000
。
方法与函数
上述程序可以只用函数而不用方法来重写。
1 | package main |
在上面的程序中,displaySalary
方法被转换为一个函数,Employee结构被作为一个参数传递给它。这个程序也产生了完全相同的输出——Salary of Sam Adolf is $5000
。
那么,既然我们可以用函数编写同样的程序,为什么还要用方法呢?这其中有几个原因。让我们逐一来看一下。
- Go不是一种纯面向对象的编程语言,它不支持类。因此,类型上的方法是实现类似于类的行为的一种方式。方法允许对与类型相关的行为进行逻辑分组,类似于类。在上面的示例程序中,所有与
Employee
类型相关的行为都可以通过使用Employee
接收器类型来创建方法进行分组。例如,我们可以添加诸如计算养老金、计算休假等方法。 - 同名的方法可以定义在不同的类型上,而同名的函数是不允许的。让我们假设我们有一个
Square
和Circle
结构。我们可以在Square
和Circle
上都定义一个名为Area
的方法。这在下面的程序中已经完成。
1 | package main |
这个程序会打印出来:
1 | Area of rectangle 50 |
上述方法的属性是用来实现接口的。我们将在下一个教程中处理接口时详细讨论这个问题。
指针接收器与值接收器
到目前为止,我们只看到了带有值接收器的方法。我们也可以创建带有指针接收器的方法。值接收器和指针接收器之间的区别是,在一个带有指针接收器的方法中所作的改变对调用者来说是可见的,而在值接收器中则不是这样的。让我们借助于一个程序来理解这一点。
1 | package main |
在上面的程序中,changeName
方法有一个值接收器(e Employee)
,而changeAge
方法有一个指针接收器(e *Employee)
。在changeName
中对Employee
结构的名称字段所做的修改对调用者来说是不可见的,因此程序在第32行调用e.changeName("Michael Andrew")
方法之前和之后都打印了相同的名称。由于changeAge
方法有一个指针接收器(e *Employee)
,在调用方法(&e).changeAge(51)
之后,age
字段的变化将对调用者可见。这个程序会打印:
1 | Employee name before change: Mark Andrew |
在上述程序的第36行中,我们使用(&e).changeAge(51)
来调用changeAge
方法。由于changeAge
有一个指针接收器,我们使用了(&e)
来调用该方法。这是不需要的,语言让我们选择直接使用e.changeAge(51)
。e.changeAge(51)
将被Go语言解释为(&e).changeAge(51)
。
下面的程序被改写为使用e.changeAge(51)
而不是(&e).changeAge(51)
,它打印出相同的输出。
1 | package main |
何时使用指针接收器,何时使用值接收器
一般来说,当方法内部对接收器的改变对调用者来说应该是可见的,就可以使用指针接收器。
指针接收器也可以用在那些复制数据结构成本很高的地方。考虑一个有许多字段的结构。在方法中使用这个结构作为一个值接收器,需要复制整个结构,这将是很昂贵的。在这种情况下,如果使用一个指针接收器,该结构将不会被复制,在方法中只使用它的一个指针。
在所有其他情况下,可以使用值接收器。
匿名结构字段的方法
属于结构的匿名字段的方法可以被调用,就像它们属于定义匿名字段的结构一样。
1 | package main |
在上述程序的第32行,我们用p.fullAddress()
调用address
结构的fullAddress()
方法。在第32行中,我们使用p.fullAddress()
调用address
结构的fullAddress()
方法。明确的方向p.address.fullAddress()
是不需要的。这个程序打印出
1 | Full address: Los Angeles, California |
方法中的值接收方与函数中的值参数
这个话题让大多数新手头疼不已。我将尽量把它说清楚😀。
当一个函数有一个值参数时,它将只接受一个值参数。
当一个方法有一个值接收器时,它将同时接受指针和值接收器。
让我们通过一个例子来理解这一点。
1 | package main |
第12行的函数func area(r rectangle)
接受一个值参数,第16行的方法func (r rectangle) area()
接受一个值的接收器。
在第25行,我们用一个值参数调用area(r)
函数,它将工作。同样,我们用一个值接收器调用 area 方法 r.area()
,这也会起作用。
我们在第28行创建一个指向r
的指针p
。如果我们试图将这个指针传递给只接受一个值的函数 area
,编译器会报错。我对第33行做了注释。如果你不注释这一行,那么编译器将抛出编译错误,不能将p(type *rectangle)
作为矩形类型的参数传给 area
。这和预期的一样。
现在棘手的部分来了,第35行的代码p.area()
调用了方法area,它只接受一个使用指针接收器p的值接收器,这是完全有效的。原因是p.area()
这一行,为了方便起见,将被Go解释为(*p).area()
,因为 area
有一个值接收器。
1 | Area Function result: 50 |
方法中的指针接收器与函数中的指针参数
与值参数类似,带有指针参数的函数将只接受指针,而带有指针接收器的方法将同时接受指针和值接收器。
1 | package main |
上述程序的第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()
。这个程序将输出。
1 | perimeter function output: 30 |
具有非结构接收器的方法
到目前为止,我们只在结构类型上定义了方法。也可以在非结构类型上定义方法,但有一个问题。要在一个类型上定义方法,接收器类型的定义和方法的定义应该存在于同一个包中。到目前为止,我们定义的所有结构和结构上的方法都位于同一个main
包中,因此它们都能工作。
1 | package main |
在上面的程序中,在第3行,我们试图在内置类型int
上添加一个名为add
的方法。这是不允许的,因为add方法的定义和int
类型的定义不在同一个包里。这个程序将抛出编译错误不能在非本地类型int
上定义新方法。
让这个程序正常工作的方法是为内置类型int
创建一个类型别名,然后用这个类型别名创建一个方法作为接收器。
1 | package main |
在上述程序的第5行,我们已经为int
创建了一个类型别名myInt
。在第7行中,我们定义了一个以myInt
为接收器的add
方法。
这个程序将打印Sum is 15
。
Go中的方法就介绍到这里。祝你有个愉快的一天。