这篇文章将描述 Go 中函数和方法之间的主要区别,以及何时使用它们最好。
函数和方法都在 Go 中被广泛使用,以提供抽象并使我们的程序更易于阅读和理解。从表面上看,函数和方法看起来很相似,但存在一些重要的语义差异,这些差异会对代码的可读性产生很大影响。
句法
声明语法
通过指定参数类型、返回值和函数体来声明函数:
type Person struct {
Name string
Age int
}
// 这个函数返回一个新的 `Person` 实例
func NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}
而方法则是通过额外指定“接收者”(即 OOP 中该方法所属的“类”)来声明:
// `Person` 指针类型是 `isAdult` 方法的接收者
func (p *Person) isAdult() bool {
return p.Age > 18
}
在上面的方法声明中,我们在 *Person
类型上声明了 isAdult
方法。
执行语法
函数使用指定的参数独立调用,方法则根据其接收者的类型调用:
p := NewPerson("John", 21)
fmt.Println(p.isAdult())
// true
互换性
函数和方法理论上可以互换。例如,我们可以将 isAdult
方法变成一个函数,并将 NewPerson
函数作为一个方法:
type PersonFactory struct {}
// `NewPerson` 现在是 `PersonFactory` 结构的一个方法
func (p *PersonFactory) NewPerson(name string, age int) *Person {
return &Person{
Name: name,
Age: age,
}
}
// `isAdult` 现在是一个函数,我们将 `Person` 作为参数传递
func isAdult(p *Person) bool {
return p.Age > 18
}
这种情况下的执行语法看起来有点奇怪:
factory := &PersonFactory{}
p := factory.NewPerson("John", 21)
fmt.Println(isAdult(p))
// true
使用场景
让我们来看看 Go 应用程序中遇到的一些常见用例,以及用于每个用例的适当抽象(函数或方法):
方法链
方法的一个非常有用的特性是能够将它们链接在一起,同时仍然保持代码干净。让我们举一个使用方法链设置 Person
的一些属性的例子:
type Person struct {
Name string
Age int
}
func (p *Person) withName(name string) *Person {
p.Name = name
return p
}
func (p *Person) withAge(age int) *Person {
p.Age = age
return p
}
func main() {
p := &Person{}
p = p.withName("John").withAge(21)
fmt.Println(*p)
// {John 21}
}
如果我们使用函数来做同样的事,它看起来会很糟糕:
p = withName(withAge(p, 18), "John")
有状态与无状态执行
这里的“无状态”意味着任何一段代码总是为相同的输入返回相同的输出
在互换性示例中,我们看到了使用 PersonFactory
对象来创建 person
的新实例。事实证明,这是一种反模式,应该避免。
对于无状态执行,最好使用之前声明的 NewPerson
之类的函数。
而对于有状态执行,如果发现一个函数读取并修改了许多特定类型的值,那么它可能就该使用该类型的方法。
语义
语义是指代码的阅读方式。如果您用口语大声朗读代码,那么哪种方式更有意义?
让我们看一下 isAdult
的函数和方法实现:
customer := NewPerson("John", 21)
// 方法
customer.isAdult()
// 函数
isAdult(customer)
对于询问“客户是成年人吗?”这里的 isAdult(customer)
与 customer.isAdult()
相比,读起来更好。