在pascal中function和procedure被称为Method,实现某一功能,无非就是有返回值和无返回值。在Golang中,method有特定的意义。
概述
golang 中的方法只不过是一个带有接收器的函数。接收器是某些特定类型(如结构)的实例,但它可以是任何其他自定义类型的实例。所以基本上当你把一个函数附加到一个类型时,该函数就会成为该类型的方法。该方法将有权访问接收器的属性,并可以调用接收器的其他方法。
为什么选择方法
由于方法允许您在类型上定义函数,因此它允许您在 Golang 中编写面向对象的代码。还有一些其他的好处,例如两个不同的方法可以在同一包中具有相同的名称,这是函数无法实现的
方法的格式
下面是方法的格式
func (receiver receiver_type) some_func_name(arguments) return_values
方法接收器和接收器类型显示在 func 关键字和函数名称之间。return_values在最后到来。
另外,让我们了解函数和方法之间的更多区别。它们之间存在一些重要差异。下面是一个函数的签名
函数:
func some_func_name(arguments) return_values
从上面的签名中可以清楚地看出该方法具有接收器参数。这是函数和方法之间的唯一区别,但因此它们在提供的功能方面有所不同
- 函数可以用作一阶对象,并且可以在方法不能传递时传递。
- 方法可用于接收器上的链接,而函数不能用于相同的链接。
- 可以存在具有相同名称的不同方法和不同的接收器,但在同一包中不能存在两个具有相同名称的不同函数。
结构体上的方法
Golang 不是一种面向对象的语言。它不支持类型继承,但它确实允许我们在任何自定义类型(包括结构)上定义方法。由于结构是字段的命名集合,因此也可以在其上定义方法。因此,golang中的结构可以与面向对象语言中的类进行比较。
让我们看一个结构体方法的例子
package main
import "fmt"
type employee struct {
name string
age int
salary int
}
func (e employee) details() {
fmt.Printf("Name: %s\n", e.name)
fmt.Printf("Age: %d\n", e.age)
}
func (e employee) getSalary() int {
return e.salary
}
func main() {
emp := employee{name: "Sam", age: 31, salary: 2000}
emp.details()
fmt.Printf("Salary %d\n", emp.getSalary())
}
输出
Name: Sam
Age: 31
Salary 2000
请注意,接收器在方法中可用,接收器的字段可以在方法内部访问。
接收器的字段也可以在方法内部更改吗?
让我们看看
package main
import "fmt"
type employee struct {
name string
age int
salary int
}
func (e employee) setNewName(newName string) {
e.name = newName
}
func main() {
emp := employee{name: "Sam", age: 31, salary: 2000}
emp.setNewName("John")
fmt.Printf("Name: %s\n", emp.name)
}
输出
Name: Sam
在上面的代码中,在员工结构上定义了一个方法集NewName。在这种方法中,我们像这样更新员工的姓名
e.name = newName
当我们在 main 函数中再次打印员工姓名时设置新名称后,我们看到打印的是旧名称“Sam”而不是“John”。发生这种情况是因为方法是在值接收器上定义的
func (e employee) setNewName(newName string)
由于该方法是在值接收器上定义的,因此当该方法被称为方法时,将创建接收器的副本,并且接收器的副本在方法中可用。由于它是副本,因此对值接收器所做的任何更改对调用方都不可见。这就是为什么它打印旧名称“山姆”而不是“约翰”。现在想到的问题是否有任何方法可以解决这个问题。答案是肯定的,这就是指针接收器出现的地方。
指针接收器上的方法
在上面的例子中,我们看到了一个值接收器上的方法。对值接收器所做的任何更改对调用方都不可见。也可以在指针接收器上定义方法。对指针接收器所做的任何更改都将对调用方可见。让我们看一个例子
package main
import "fmt"
type employee struct {
name string
age int
salary int
}
func (e *employee) setNewName(newName string) {
e.name = newName
}
func main() {
emp := &employee{name: "Sam", age: 31, salary: 2000}
emp.setNewName("John")
fmt.Printf("Name: %s\n", emp.name)
}
输出
Name: John
在上面的程序中,我们在指针接收器上定义了方法setNewName。
func (e *employee) setNewName(newName string)
然后,我们创建了一个员工指针,并在其上调用了 setNewName 方法。我们看到,对 setNewName 内的员工指针所做的更改对调用方可见,并且它会打印新名称。
是否需要创建员工指针才能使用指针接收器调用方法?不,不是。可以在员工实例上调用该方法,语言将负责它以将其作为指向该方法的指针正确传递。这种灵活性是由语言提供的。
让我们看一个例子
package main
import "fmt"
type employee struct {
name string
age int
salary int
}
func (e *employee) setNewName(newName string) {
e.name = newName
}
func main() {
emp := employee{name: "Sam", age: 31, salary: 2000}
emp.setNewName("John")
fmt.Printf("Name: %s\n", emp.name)
(&emp).setNewName("Mike")
fmt.Printf("Name: %s\n", emp.name)
}
输出
Name: John
Name: Mike
我们在上面的程序中看到,即使在指针接收器上定义了方法,但我们使用非指针雇员实例调用该方法
emp.setNewName("John")
但是语言将接收方作为指针传递,因此更改对调用方可见。
即使这种调用方式也是有效的
(&emp).setNewName("Mike")
现在,反过来呢。如果在值接收器上定义了方法,是否可以使用接收器的指针调用该方法?
是的,即使这是有效的,并且无论该方法是在指针上还是在普通结构上调用,该语言都会负责将参数作为值接收器正确传递。
让我们看一个例子
package main
import "fmt"
type employee struct {
name string
age int
salary int
}
func (e employee) setNewName(newName string) {
e.name = newName
}
func main() {
emp := employee{name: "Sam", age: 31, salary: 2000}
emp.setNewName("John")
fmt.Printf("Name: %s\n", emp.name)
(&emp).setNewName("Mike")
fmt.Printf("Name: %s\n", emp.name)
emp2 := &employee{name: "Sam", age: 31, salary: 2000}
emp2.setNewName("John")
fmt.Printf("Name: %s\n", emp2.name)
}
输出
Name: Sam
Name: Sam
Name: Sam
请注意,因为在所有三种情况下,setNewName 方法都有一个值接收器,因此当值作为副本传递时,调用方看不到更改。它在所有三种情况下都打印旧名称
总结一下我们上面学到的东西
- 如果方法具有值接收器,则支持同时使用值接收器和指针接收器调用该方法
- 如果一个方法有一个指针接收器,那么它也支持同时使用值和指针接收器调用该方法
这与函数不同,其中如果
- 如果一个函数有一个指针参数,那么它只会接受一个指针作为参数
- 如果一个函数有一个值参数,那么它只接受一个值作为参数
何时使用指针接收器
- 当在方法内部对接收器所做的更改必须对调用方可见时。
- 当结构很大时,最好使用指针接收器,否则每次调用方法时都会制作结构的副本,这将很昂贵
关于方法的更多注意事项
- 接收器类型必须在与方法定义相同的包中定义。在接收器上定义存在于不同包中的方法时,将引发以下错误。
ERROR: cannot define new methods on non-local types
- 到目前为止,我们已经看到了使用点运算符的方法调用。还有另一种方法可以调用方法,如以下示例所示
package main
import "fmt"
type employee struct {
name string
age int
salary int
}
func (e employee) details() {
fmt.Printf("Name: %s\n", e.name)
fmt.Printf("Age: %d\n", e.age)
}
func (e *employee) setName(newName string) {
e.name = newName
}
func main() {
emp := employee{name: "Sam", age: 31, salary: 2000}
employee.details(emp)
(*employee).setName(&emp, "John")
fmt.Printf("Name: %s\n", emp.name)
}
输出
Name: Sam
Age: 31
Name: John
在上面的例子中,我们看到了一种不同的方法来调用方法。有两种情况
- 当方法具有值接收器时,可以按如下所示调用它,它是结构名称,后跟方法名称。第一个参数是值接收器本身。
employee.details(emp)
- 当方法具有指针接收器时,可以按如下所示调用它,该指针指向结构名称,后跟方法名称。第一个参数是指针接收器。
(*employee).setName(&emp, "John")
另请注意,该方法的参数从上面的 setName 函数的第二个参数开始:
(*employee).setName(&emp, "John")
您很少会看到使用这种样式,我们之前讨论的点表示法样式是推荐的,也是最常见的方法。
匿名嵌套结构字段的方法
让我们看一个程序
package main
import "fmt"
type employee struct {
name string
age int
salary int
address
}
type address struct {
city string
country string
}
func (a address) details() {
fmt.Printf("City: %s\n", a.city)
fmt.Printf("Country: %s\n", a.country)
}
func main() {
address := address{city: "London", country: "UK"}
emp := employee{name: "Sam", age: 31, salary: 2000, address: address}
emp.details()
emp.address.details()
}
输出
City: London
Country: UK
City: London
Country: UK
请注意,在上面的程序中,可以通过两种方式访问地址结构的详细方法
emp.details()
emp.address.details()
因此,在匿名嵌套结构的情况下,可以直接访问该结构的方法。
导出方法
Go 没有任何公共、私有或受保护的关键字。控制包外部可见性的唯一机制是使用大写和非大写格式
- 将导出大写标识符。大写字母表示这是导出的标识符,可在包外部使用。
- 不导出非大写标识符。小写表示标识符未导出,只能从同一包中访问。
因此,任何以大写字母开头的结构都会导出到其他包中。同样,任何以资本开头的结构字段都会被导出,否则不会导出。同样,任何以大写字母开头的结构方法都会被导出。让我们看一个示例,该示例显示了结构、结构字段和方法的导出和非导出。请参阅下面的 model.go 和 test.go。两者都属于主包。
- 结构
- 导出结构体人员
- 结构公司是非出口的
- 结构领域
- 导出人员结构字段名称
- 不导出人员结构字段年龄,但导出名称
- 结构的方法
- 人导出结构体的方法 GetAge()
- 人结构体的方法 getName() 未导出
model.go
package main
import "fmt"
//Person struct
type Person struct {
Name string
age int
}
//GetAge of person
func (p *Person) GetAge() int {
return p.age
}
func (p *Person) getName() string {
return p.Name
}
type company struct {
}
test.go
package main
import "fmt"
//Test function
func Test() {
//STRUCTURE IDENTIFIER
p := &Person{
Name: "test",
age: 21,
}
fmt.Println(p)
c := &company{}
fmt.Println(c)
//STRUCTURE'S FIELDS
fmt.Println(p.Name)
fmt.Println(p.age)
//STRUCTURE'S METHOD
fmt.Println(p.GetAge())
fmt.Println(p.getName())
}
输出
&{test 21}
&{}
test
21
21
test
如果我们移动文件 model.go 到另一个名为 model 的包。 现在运行“go build”的输出将给出编译错误。所有编译错误都是因为主包中的test.go无法引用模型包中model.go的未导出字段。
编译错误将是
p.getName undefined (cannot refer to unexported field or method model.(*Person).getName)
方法链接
为了使方法链接成为可能,链中的方法应返回接收器。返回链中最后一个方法的接收器是可选的。
让我们看一个方法链接的示例。
package main
import "fmt"
type employee struct {
name string
age int
salary int
}
func (e employee) printName() employee {
fmt.Printf("Name: %s\n", e.name)
return e
}
func (e employee) printAge() employee {
fmt.Printf("Age: %d\n", e.age)
return e
}
func (e employee) printSalary() {
fmt.Printf("Salary: %d\n", e.salary)
}
func main() {
emp := employee{name: "Sam", age: 31, salary: 2000}
emp.printName().printAge().printSalary()
}
输出
Name: Sam
Age: 31
Salary: 2000
非结构类型的方法
还可以在非结构自定义类型上定义方法。非结构自定义类型可以通过类型定义创建。以下是创建新自定义类型的格式
type {type_name} {built_in_type}
例如,我们可以命名为float64类型的自定义类型myFloat
type myFloat float64
可以在命名的自定义类型上定义方法。请参阅以下示例:
package main
import (
"fmt"
"math"
)
type myFloat float64
func (m myFloat) ceil() float64 {
return math.Ceil(float64(m))
}
func main() {
num := myFloat(1.4)
fmt.Println(num.ceil())
}
输出
2
结论
go中的method,近似于delphi里面record所定义的function和procedure,strcuct和record都不可继承,go里面没有overload的概念,所以method不可重名,二者的method即插即用,不需构造函数。