「这是我参与2022首次更文挑战的第16天,活动详情查看:2022首次更文挑战」。
概念
首先
Go 语言中同时有函数和方法,函数和方法并不是同一个东西
面向对象的背景
- Go 仅支持封装,不支持继承和多态
- Go 没有 class,只有 struct
- struct 就像是类的一种简化形式,那类方法在哪呢?其实就是 Go 方法
方法的描述
Go 方法是作用在接收者(receiver)上的一个函数
接收者的概念
- 接收者有点实例对象的意思
- 接收者是某种类型的变量,所以方法是一种特殊类型的函数
接收者的类型
- 接收者类型可以是(几乎)任何类型
- 不仅仅是结构体类型,任何类型都可以有方法,甚至可以是函数类型、指针,可以是 int、bool、string 或数组的别名类型
- 重点:接收者不能是一个接口类型,因为接口是一个抽象定义,但是方法是具体实现
- 所有给定类型的方法属于该类型的方法集
和函数的区别
- 方法能给用户自定义的类型添加新的行为
- 它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法
- 接收者可以是值接收者,也可以是指针接收者
- 在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。
- 也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型
- 函数不允许重载,但是方法允许重载,前提是接收者类型不一样
语法格式
// 方法
func (t Type) methodName(parameter list)(return list) {
}
// 函数
func funcName(parameter list)(return list){
}
- 类比面向对象:t 就像实例对象(self),methodName 是它的实例方法
- 所以调用方法的格式是:
t.methodName()
简单的🌰
package main
import "fmt"
type People struct {
name string
age int
}
// 不带参数的方法
func (p People) getNameAndAge() {
fmt.Printf("name is: %v, age is: %v", p.name, p.age)
}
// 带参数的方法
func (p *People) addAge(age int) {
p.age += age
}
func main() {
p := new(People)
p.name = "小菠萝"
p.age = 24
p.addAge(10)
p.getNameAndAge()
}
运行结果
name is: 小菠萝, age is: 24
注意
因为结构体是值类型,如果希望通过方法修改结构体的值,那接收者类型就需要是结构体类型的指针(如:func (p *People) addAge(age int))
函数的写法
将上面的方法用函数来完成
package main
import "fmt"
type People struct {
name string
age int
}
// 不带参数的方法
func getNameAndAge(p *People) {
fmt.Printf("name is: %v, age is: %v", p.name, p.age)
}
// 带参数的方法
func addAge(p *People, age int) {
p.age += age
}
func main() {
p := new(People)
p.name = "小菠萝"
p.age = 24
addAge(p, 10)
getNameAndAge(p)
}
运行结果
name is: 小菠萝, age is: 24
既然函数也能完成相同功能,为什么还要用方法?
- Go不是一种纯粹面向对象的编程语言,它不支持类;因此,类型的方法是一种实现类似于类的行为的方式
- 相同名称的方法可以在不同的类型上定义,而相同名称的函数是不允许的,所以说方法可以重载,但函数不可以重载
方法重载的🌰
重点:接收者类型必须不一样
package main
import (
"fmt"
"math"
)
type Graphics struct {
area float64
}
type Rectangle struct {
Graphics
width, height float64
}
type Circle struct {
Graphics
radius float64
}
func (r *Rectangle) getArea() {
r.area = r.width * r.height
}
func (c *Circle) getArea() {
c.area = c.radius * c.radius * math.Pi
}
func main() {
r1 := Rectangle{Graphics{}, 12, 2}
c1 := Circle{Graphics{}, 25}
r1.getArea()
c1.getArea()
fmt.Println("Area of r1 is: ", r1.area)
fmt.Println("Area of r2 is: ", c1.area)
}
Rectangle、Circle 有继承 Graphics 的意思
运行结果
Area of r1 is: 24
Area of r2 is: 314.1592653589793
等价 Python 的代码
class Graphics:
area: float = 0
class Rectangle(Graphics):
width: int = 0
height: int = 0
def getArea(self):
self.area = self.width * self.height
class Circle(Graphics):
radius: int
def getArea(self):
self.area = self.radius * self.radius * 3.14
假设同一个方法名,但是一个方法的参数是值类型,一个方法的参数是指针类型,会怎么样?
type Cat struct {
}
type Duck interface {
Quack()
}
// 以下两个方法只是参数类型不一样,一个是 Cat 的值类型,另一个 Cat 的指针类型
func (c Cat) Quack() {
}
func (c *Cat) Quack() {
}
会编译不通过
指针作为接收者
和函数参数传递一样,如果接收者的类型不是指针或引用类型(slice、map、channel),那么会直接拷贝整个变量给方法,并不会改变接收者实际的值
package main
import (
"fmt"
)
// 树节点
type TreeNode struct {
value int
// TreeNode 指针
left, right *TreeNode
}
// 使用自定义工厂函数
func newTreeNode(value int) *TreeNode {
// 返回的是局部变量的地址
return &TreeNode{value: value}
}
// 值接收者
func (tree TreeNode) setValue(value int) {
tree.value = value
}
// 遍历树,左中右顺序
func (tree *TreeNode) print() {
if tree == nil {
return
}
tree.left.print()
fmt.Print(tree.value, " ")
tree.right.print()
}
func main() {
var root TreeNode
fmt.Println(root)
root = TreeNode{value: 1}
fmt.Println(root)
// 使用 new 函数创建
root.left = new(TreeNode)
// 通过 & 创建
root.right = &TreeNode{2, nil, nil}
// 通过工厂函数创建
root.right.left = newTreeNode(4)
root.print()
root.setValue(1111)
root.print()
}
运行结果
{0 <nil> <nil>}
{1 <nil> <nil>}
0 1 4 2 0 1 4 2
- 如果 left、right 字段没有赋值,指针零值就是 nil
- setValue 是值接收者,在方法里面修改值,并不会实际改变结构体的字段值
将 setValue 方法的接收者类型改成指针
指针接收者就是指针传递,此时就是将 root 的内存地址复制给 tree 了
func (tree *TreeNode) setValue(value int) {
tree.value = value
}
...
root.print()
root.setValue(1111)
root.print()
这个时候再运行一次
0 1 4 2 0 1111 4 2
方法内的修改会同步修改结构体的字段值
nil 指针也可以调用方法
package main
import (
"fmt"
)
type TreeNode struct {
value int
left, right *TreeNode
}
func newTreeNode(value int) *TreeNode {
return &TreeNode{value: value}
}
func (tree *TreeNode) setValue(value int) {
// 加了个判断是否为指针
if tree == nil {
fmt.Println("tree 是一个 nil 指针")
}
tree.value = value
}
func (tree *TreeNode) print() {
if tree == nil {
return
}
tree.left.print()
fmt.Print(tree.value, " ")
tree.right.print()
}
func main() {
var root TreeNode
fmt.Println(root)
root.left.setValue(123)
}
运行结果
tree 是一个 nil 指针
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x108bccf]
goroutine 1 [running]:
main.(*TreeNode).setValue(0x0, 0x7b)
/Users/1/hello-go/17_方法.go:63 +0x6f
main.main()
/Users/1/hello-go/17_方法.go:95 +0x20d
- nil 指针是成功调用了方法,也进到 if 判断里面了,但最终还是报错,因为 nil 指针并没有 value 这个字段,所以报错了
- 但像 java、python ,如果是 null 值肯定不能调用方法了,所以 go 还是很人性化的嘛
- 为了程序不报错,还是要做下处理的,比如 print 方法
func (tree *TreeNode) print() {
// 如果是 nil 直接 return 就好了,这样就不会报错了
if tree == nil {
return
}
tree.left.print()
fmt.Print(tree.value, " ")
tree.right.print()
}
值接收者 vs 指针接收者
- 要改变内容必须使用指针接收者
- 结构过大也考虑使用指针接收者
- 一致性:如果有指针接收者,最好都是指针接收者