函数和方法的区别

函数:函数名(实参列表)
方法:变量.方法(实参列表)

方法

在面向对象编程中,一个对象其实也就是一个简单的值或者一个变量,在这个对象中会包含一些函数,这种带有接收者的函数,我们称为方法(method)。

本质上,一个方法则是一个和特殊类型关联的函数。

在Go语言中,可以给任意自定义类型(包括内置类型,但不包括指针类型)添加相应的方法。

方法总是绑定对象实例,并隐式将实例作为第一实参 (receiver),方法的语法如下:

  1. func (receiver ReceiverType) funcName(parameters) (results)
  • 参数 receiver 可任意命名。如方法中未曾使用,可省略参数名。
  • 参数 receiver 类型可以是 T 或 *T。基类型 T 不能是接口或指针。
  • 不支持重载方法,也就是说,不能定义名字相同但是不同参数的方法。

为类型添加方法

Should I define methods on values or pointers?

  1. func (s *MyStruct) pointerMethod() { } // method on pointer 结构体 数组 切片
  2. func (s MyStruct) valueMethod() { } // method on value 适合基本类型

基础类型 作为接收者

  1. 类型 添加方法
  2. type MyInt int //自定义类型,给int改名为MyInt
  3. // 给MyInt定义一个方法, 返回类型MyInt
  4. func (this MyInt) Add(b MyInt) MyInt {
  5. return this + b
  6. }
  7. // 定义一个函数, 返回类型MyInt
  8. func Add(a, b MyInt) MyInt { //面向过程
  9. return a + b
  10. }
  11. func main() {
  12. var a MyInt = 1
  13. var b MyInt = 1
  14. //方法调用
  15. fmt.Println("a.Add(b) = ", a.Add(b)) //a.Add(b) = 2
  16. //函数调用
  17. fmt.Println("Add(a, b) = ", Add(a, b)) //Add(a, b) = 2
  18. }

注意:虽然方法的名字一模一样,但是如果接收者不一样,那么方法就不一样。

结构体 作为接收者

方法里面可以访问接收者的字段,调用方法通过点( .)访问,就像struct里面访问字段一样:

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. // 带有接收者的函数叫方法
  9. func (tmp Person) PrintInfo() {
  10. fmt.Println("tmp = ", tmp)
  11. }
  12. //通过一个函数,给成员赋值
  13. func (p *Person) SetInfo(n string, s byte, a int) {
  14. p.name = n
  15. p.sex = s
  16. p.age = a
  17. }
  18. func main() {
  19. //定义同时初始化
  20. p := Person{"mike", 'm', 18}
  21. p.PrintInfo() // tmp = {mike 109 18}
  22. //定义一个结构体变量
  23. var p2 Person
  24. (&p2).SetInfo("yoyo", 'f', 22)
  25. p2.PrintInfo() // tmp = {yoyo 102 22}
  26. }

值语义和引用语义

  1. package main
  2. import "fmt"
  3. type person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. //修改成员变量的值
  9. //值作为接收者,值语义,一份拷贝
  10. func (p person) setInfoValue(n string, s byte, a int) {
  11. p.name = n
  12. p.sex = s
  13. p.age = a
  14. fmt.Println("p = ", p) // p = {mike 109 18}
  15. fmt.Printf("setInfoValue &p = %p\n", &p) // setInfoValue &p = 0xc0000044e0
  16. }
  17. //指针作为接收者,引用语义
  18. func (p *person) setInfoPointer(n string, s byte, a int) {
  19. p.name = n
  20. p.sex = s
  21. p.age = a
  22. fmt.Printf("setInfoPointer p = %p\n", p)
  23. }
  24. func main() {
  25. // 初始化
  26. fmt.Println("\n-----------main------------------")
  27. s1 := person{"go", 'm', 22}
  28. fmt.Printf("main函数 s1 = %p\n", &s1)
  29. fmt.Println("\n------------setInfoPointer-----------------")
  30. //引用语义
  31. (&s1).setInfoPointer("mike", 'm', 18)
  32. fmt.Println("s1 = ", s1) // s1 = {mike 109 18}
  33. fmt.Println("\n-----------setInfoValue------------------")
  34. //值语义(不是引用传递) s1 还是初始化的值
  35. s1.setInfoValue("mike", 'm', 18)
  36. fmt.Println("s1 = ", s1) // s1 = {go 109 22}
  37. }
  38. -----------main------------------
  39. main函数 s1 = 0xc000066020
  40. ------------setInfoPointer-----------------
  41. setInfoPointer p = 0xc000066020
  42. s1 = {mike 109 18}
  43. -----------setInfoValue------------------
  44. p = {mike 109 18}
  45. setInfoValue &p = 0xc000066060
  46. s1 = {mike 109 18}

从打印结果中可以看出指针作为接收者传的是引用,值是拷贝一份

方法集

类型 的 方法集 是指可以被 该类型的值 调用的 所有方法的集合。

用实例实例 valuepointer 调用方法(含匿名字段)不受方法集约束,编译器编总是查找全部方法,并自动转换 receiver 实参。

类型 *T 方法集

一个指向自定义类型的值的指针,它的方法集由该类型定义的所有方法组成,无论这些方法接受的是一个值还是一个指针。

如果在指针上调用一个接受值的方法,Go语言会聪明地将该指针解引用,并将指针所指的底层值作为方法的接收者。

类型 *T 方法集包含全部 receiver T + *T 方法:

类型 *T 方法集 (指针变量的方法集)

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. func (p Person) SetInfoValue() {
  9. fmt.Println("SetInfoValue")
  10. }
  11. func (p *Person) SetInfoPointer() {
  12. fmt.Println("SetInfoPointer")
  13. }
  14. func main() {
  15. //结构体变量是一个指针变量,它能够调用哪些方法,这些方法就是一个集合,简称方法集
  16. p := &Person{"mike", 'm', 18}
  17. //p.SetInfoPointer() //func (p *Person) SetInfoPointer()
  18. (*p).SetInfoPointer() //把(*p)转换层p后再调用,等价于上面
  19. //内部做的转换, 先把指针p, 转成*p后再调用
  20. //(*p).SetInfoValue()
  21. p.SetInfoValue()
  22. }

类型 T 方法集 (普通变量的方法集)

一个自定义类型值的方法集则由为该类型定义的接收者类型为值类型的方法组成,但是不包含那些接收者类型为指针的方法。

但这种限制通常并不像这里所说的那样,因为如果我们只有一个值,仍然可以调用一个接收者为指针类型的方法,这可以借助于Go语言传值的地址能力实现。

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. func (p Person) SetInfoValue() {
  9. fmt.Println("SetInfoValue")
  10. }
  11. func (p *Person) SetInfoPointer() {
  12. fmt.Println("SetInfoPointer")
  13. }
  14. func main() {
  15. p := Person{"mike", 'm', 18}
  16. p.SetInfoPointer() //func (p *Person) SetInfoPointer()
  17. //内部,先把p, 转为为&p再调用, (&p).SetInfoPointer()
  18. p.SetInfoValue()
  19. }

匿名字段

方法的继承

如果匿名字段 实现了一个方法,那么包含这个匿名字段的struct也能调用该方法。

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. //Person类型,实现了一个方法
  9. func (tmp *Person) PrintInfo() {
  10. fmt.Printf("name=%s, sex=%c, age=%d\n", tmp.name, tmp.sex, tmp.age)
  11. }
  12. //有个学生,继承Person字段,成员和方法都继承了
  13. type Student struct {
  14. Person //匿名字段
  15. id int
  16. addr string
  17. }
  18. func main() {
  19. s := Student{Person{"mike", 'm', 18}, 666, "bj"}
  20. s.PrintInfo() // name=mike, sex=m, age=18
  21. }

方法的重写

两个结构体类型定义了同名方法

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. //Person类型,实现了一个方法
  9. func (tmp *Person) PrintInfo() {
  10. fmt.Printf("name=%s, sex=%c, age=%d\n", tmp.name, tmp.sex, tmp.age)
  11. }
  12. //有个学生,继承Person字段,成员和方法都继承了
  13. type Student struct {
  14. Person //匿名字段
  15. id int
  16. addr string
  17. }
  18. //Student也实现了一个方法,这个方法和Person方法同名,这种方法叫重写
  19. func (tmp *Student) PrintInfo() {
  20. fmt.Println("Student: tmp = ", tmp)
  21. }
  22. func main() {
  23. s := Student{Person{"mike", 'm', 18}, 666, "bj"}
  24. //就近原则:先找本作用域的方法,找不到再用继承的方法
  25. s.PrintInfo() //到底调用的是Person, 还是Student, 结论是Student
  26. //显式调用继承的方法
  27. s.Person.PrintInfo()
  28. }

表达式

类似于我们可以对函数进行赋值和传递一样,方法也可以进行赋值和传递。

根据调用者不同,方法分为两种表现形式:

  • 方法值
  • 方法表达式。

两者都可像普通函数那样赋值和传参,区别在于

  • 方法值绑定实例,
  • 方法表达式则须显式传参

方法值

隐藏了接收者

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. func (p Person) SetInfoValue() {
  9. fmt.Printf("SetInfoValue: %p, %v\n", &p, p)
  10. }
  11. func (p *Person) SetInfoPointer() {
  12. fmt.Printf("SetInfoPointer: %p, %v\n", p, p)
  13. }
  14. func main() {
  15. p := Person{"mike", 'm', 18}
  16. fmt.Printf("main: %p, %v\n", &p, p)
  17. p.SetInfoPointer() //传统调用方式
  18. //保存方式入口地址
  19. pFunc := p.SetInfoPointer //这个就是方法值,调用函数时,无需再传递接收者,隐藏了接收者
  20. pFunc() //等价于 p.SetInfoPointer()
  21. vFunc := p.SetInfoValue
  22. vFunc() //等价于 p.SetInfoValue()
  23. }

方法表达式

  1. package main
  2. import "fmt"
  3. type Person struct {
  4. name string //名字
  5. sex byte //性别, 字符类型
  6. age int //年龄
  7. }
  8. func (p Person) SetInfoValue() {
  9. fmt.Printf("SetInfoValue: %p, %v\n", &p, p)
  10. }
  11. func (p *Person) SetInfoPointer() {
  12. fmt.Printf("SetInfoPointer: %p, %v\n", p, p)
  13. }
  14. func main() {
  15. p := Person{"mike", 'm', 18}
  16. fmt.Printf("main: %p, %v\n", &p, p)
  17. //方法值 f := p.SetInfoPointer //隐藏了接收者
  18. //方法表达式
  19. f := (*Person).SetInfoPointer
  20. f(&p) //显式把接收者传递过去 ====》 p.SetInfoPointer()
  21. f2 := (Person).SetInfoValue
  22. f2(p) //显式把接收者传递过去 ====》 p.SetInfoValue()
  23. }