Go 的方法

106 阅读6分钟

「这是我参与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 指针接收者

  • 要改变内容必须使用指针接收者
  • 结构过大也考虑使用指针接收者
  • 一致性:如果有指针接收者,最好都是指针接收者