方法
概述: 在面向对象编程, 一个对象其实也就是一个简单的值或者一个变量, 在这个对象中会包含一些函数, 这种带有接收者的函数, 我们称为为方法(method)。
本质上, 一个方法则是一个和特殊类型关联的函数。
一个面向对象的程序会用方法来表达其属性和对应的操作, 这样使用这个对象的用户就不需要直接去操作对象, 而是借助方法来做这些事情。
在Go语言中, 可以给任意自定义类型(包括内置类型, 但不包括指针类型)添加相应的方法。
方法总是绑定对象实例, 并隐式将实例作为第一实参(receiver), 方法的语法如下:
func (receiver ReceiverType) funcName(paramters) (results)
参数receiver可任意命名。 如方法中未曾使用, 可省略参数名。 receiver: 接收者 [rɪˈsiːvə(r)]
参数receiver类型可以是 T 或 *T。 基类型 T 不能是接口或指针
不支持重载方法, 也就是说, 不能定义名字相同但是不同参数的方法。
注意:
每个方法只能有一个 receiver 参数, 不支持多 receiver 参数列表或变长 receiver 参数。
一个方法只能绑定一个基类型, Go 语言不支持同时绑定多个类型的方法。
receiver 参数的基类型本身不能是指针类型或接口类型。
如: 下面的 receiver参数是无效的
type MyInt *intfunc (r MyInt) String() string { // invalid receiver type MyInt (MyInt is a pointer type) return fmt.Sprintf("%d", *(*int)(r))}type MyReader io.Readerfunc (r MyReader) Read(p []byte) (int, error) { // invalid receiver type MyReader (MyReader is an interface type) return r.Read(p)}
Go 方法具有如下特点:
方法名的首字母是否大写决定了该方法是否是导出方法 ;
方法定义要与类型定义放在同一个包内。
由于方法定义与类型定义必须放在同一个包下面, 因此我们可以推论得到:我们不能为原生类型(诸如:int、float64、map 等)添加方法, 只能为自定义类型定义方法。
错误的作法:
func (i int) String() string { // cannot define new methods on non-local type int return fmt.Sprintf("%d", i)}正确的作法:type MyInt intfunc (i MyInt) String() string { return fmt.Sprintf("%d", int(i))}
常见的问题: cannot define new methods on non-local type int
原因: go语言不允许为简单的内置类型添加方法, 一般用于自定义结构体 struct, 其他数据类型不推荐使用;
为类型添加方法
基础类型作为接收者
package main //必须有个main包import "fmt"type MyInt int //自定义类型, 给int改名为MyInt, 定义方法必须使用自定义类型或者结构体//在函数定义时, 在其名字之前放上一个变量, 即是一个方法func (a MyInt) Add(b MyInt) MyInt { //面向对象 return a + b}//传统方法定义func Add(a, b MyInt) MyInt { return a + b}func main(){ var a MyInt = 1 var b MyInt = 1 //调用func (a MyInt) Add(b MyInt) fmt.Println("a.Add(b) = ", a.Add(b)) //a.Add(b) == 2 //调用func Add(a, b MyInt) fmt.Println("Add(a, b) = ", Add(a, b)) //Add(a, b) = 2}
通过上面的例子可以看出, 面向对象只是换了一种语法形式来表达。方法是函数的语法糖, 因为receiver其实就是方法所接收的第1个参数。
注意: 虽然方法的名字一模一样, 但是如果接收者不一样, 那么方法就不一样。
package main //必须有个main包import "fmt"type MyInt int //自定义类型, 给int改名为MyInt, 定义方法必须使用自定义类型或者结构体//在函数定义时, 在其名字之前放上一个变量, 即是一个方法func (a MyInt) Add(b MyInt, c MyInt) MyInt { //面向对象 参数可以多出一个, 不会报bug return a + b}//传统方法定义func Add(a, b, c MyInt) MyInt { // 参数可以多出一个, 不会报bug return a + b}func main() { var a MyInt = 1 var b MyInt = 1 //调用func (a MyInt) Add(b MyInt) fmt.Println("a.Add(b) = ", a.Add(b, 10)) //a.Add(b) == 2 //调用func Add(a, b MyInt) fmt.Println("Add(a, b) = ", Add(a, b, 10)) //Add(a, b) = 2}
注意: 函数和方法多出的参数没有使用, 不会报bug(如: 参数c);
结构体作为接收者
方法里面可以访问接收者的字段, 调用方法通过点(.)访问, 就像struct里面访问字段一样:
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}func (p Person) PrintInfo(){ fmt.Println(p.name, p.sex, p.age)}func main(){ p := Person{"mike", 'm', 18} //初始化 p.PrintInfo() //调用func (p Person) PrintInfo()}
输出结果:
mike 109 18
值语义和引用语义
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}//指针作为接收者, 引用语义func (p *Person) SetInfoPointer(){ //给成员赋值 (*p).name = "yoyo" p.sex = 'f' p.age = 22}//值作为接收者, 值语义func (p Person) SetInfoValue(){ //给成员赋值 p.name = "yoyo" p.sex = 'f' p.age = 22}func main(){ //指针作为接收者, 引用语义 p1 := Person{"mike", 'm', 18} //初始化 fmt.Println("函数调用前 = ", p1) //函数调用前 = {mike 109 18} p1.SetInfoPointer() // 这里没有使用指针不会改变接收者(receiver)是引用语义, p 会转换成 &p fmt.Println("函数调用后 = ", p1) //函数调用后 = {yoyo 102 22} (&p1).SetInfoPointer() fmt.Println("函数调用后 = ", p1) //函数调用后 = {yoyo 102 22} fmt.Println("==========") //值类型作为接收者, 值语义 p2 := Person{"mike", 'm', 18} //初始化 fmt.Println("函数调用前 = ", p2) //函数调用前 = {mike 109 18} p2.SetInfoValue() fmt.Println("函数调用后 = ", p2) //函数调用后 = {mike 109 18} 值语义不会改变原有的值 (&p2).SetInfoValue() // 这里的 "&" 不会改变接收者(receiver)是值语义, &p 会转换成 p fmt.Println("函数调用后 = ", p2) //函数调用后 = {mike 109 18} 值语义不会改变原有的值}
由此可见: 决定值语义还是引用语义, 在于定义方法是否作为指针作为接收者;
接收者(receiver)究竟是指针(引用语义)还是值(值语义)类型, 是在定义时候决定, 而不是在使用的时候决定;
值语义和引用语义的区别:
Go 语言中的大多数类型都是值语义。值语义和引用的区别在于赋值之后, 重新赋值, 是否会改变原值。
如果不改变原值, 则是值语义。否则是引用语义, 引用语义比值语义拥有更复杂的存储结构。比如分配内存、指针、长度、容量等。
总结:
值接收者(值语义) vs 指针接收者(引用语义)
要改变内容必须使用指针接收者
结构过大也考虑使用指针接收者
一致性: 如有指针接收者, 最好都是指针接收者
值接收者是go语言特有
值/指针接收者均可接收值/指针
方法集
类型的方法集是指可以被该类型的值调用的所有方法的集合。
用实例 value 和 pointer 调用方法(含匿名字段)不受方法集约束, 编译器总是查找全部方法, 并自动转换 receiver 实参。
类型 *T 方法集
一个指向自定义类型的值的指针, 它的方法集由该类型定义的所有方法组成, 无论这些方法接受的是一个值还是一个指针。
如果在指针上调用一个接受值的方法, Go语言会聪明地将该指针解引用, 并将指针所指的底层值作为方法的接收者。
类型 *T 方法法集包含全部 receiver T + *T 方法:
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}//指针作为接收者, 引用语义func (p *Person) SetInfoPointer() { (*p).name = "yoyo" p.sex = 'f' p.age = 22 fmt.Println("SetInfoPointer")}//值作为接收者, 值语义func (p Person) SetInfoValue(){ p.name = "xxx" p.sex = 'm' p.age = 33 fmt.Println("SetInfoValue")}func main(){ //p为指针类型 var p *Person = &Person{"mike", 'm', 18} // 此时的"*"代表指针类型 p.SetInfoPointer() //SetInfoPointer 内部将p转化为*p, 再调用 (*p).SetInfoPointer() //SetInfoPointer 等同于 p.SetInfoPointer(), 但是效率高, 此时的"*"代表操作符, 因为 p 传递的是指针数据类型 p.SetInfoValue() //SetInfoValue 同于 (*p).SetInfoValue(), 但是效率高 (*p).SetInfoValue() //SetInfoValue 内部将*p转化为p, 再调用}
类型 T 方法集
一个自定义类型值的方法集则由为该类型定义的接收者类型为值类型的方法组成, 但是不包含那些接收者类型为指针的方法。
但这种限制通常并不像这里所说的那样, 因为如果我们只有一个值, 仍然可以调用一个接收者为指针类型的方法, 这可以借助于Go语言传值的地址能力实现。
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}//指针作为接收者, 引用语义func (p *Person) SetInfoPointer() { (*p).name = "yoyo" // * 代表操作符 p.sex = 'f' p.age = 22 fmt.Println("SetInfoPointer")}//值作为接收者, 值语义func (p Person) SetInfoValue(){ p.name = "xxx" p.sex = 'm' p.age = 33 fmt.Println("SetInfoValue")}func main(){ //p为普通类型 p := Person{"mike", 'm', 18} (&p).SetInfoPointer() //SetInfoPointer //代表传址 (p).SetInfoPointer() //SetInfoPointer 内部先把p转化为&p后, 再调用 p.SetInfoValue() //SetInfoValue (&p).SetInfoValue() //SetInfoValue 内部先把&p转化为p后, 再调用 //(*p).SetInfoPointer() //err, invalid indirect of s (type Person) //(*s).SetInfoValue() //err, invalid indirect of s (type Person) fmt.Println(p.name, p.age, p.sex) //yoyo 22 102}
匿名字段
方法的继承
如果匿名字段实现了一个方法, 那么包含这个匿名字段的struct也能调用这个方法
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}//Person定义了方法func (p *Person) PrintInfo() { fmt.Printf("%s, %c, %d\n", p.name, p.sex, p.age)}type Student struct { Person //匿名字段, 那么 Student 包含了 Person 的所有字段 id int addr string}func main(){ p := Person{"mike", 'm', 18} p.PrintInfo() //mike, m, 18 s := Student{Person{"yoyo", 'f', 20}, 2, "sz"} s.PrintInfo() //yoyo, f, 20}
方法的重写
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}//Person定义了方法func (p *Person) PrintInfo() { fmt.Printf("Person: %s, %c, %d\n", p.name, p.sex, p.age)}type Student struct { Person //匿名字段, 那么 Student 包含了 Person 的所有字段\ id int addr string}func (s *Student) PrintInfo() { fmt.Printf("Student: %s, %c, %d\n", s.name, s.sex, s.age)}func main(){ p := Person{"mike", 'm', 18} p.PrintInfo() //Person: mike, m, 18 s := Student{Person{"yoyo", 'f', 20}, 2, "sz"} s.PrintInfo() //Student: yoyo, f, 20 s.Person.PrintInfo() //Person: yoyo, f, 20}
表达式
类似于我们可以对函数进行赋值和传递一样, 方法也可以进行赋值和传递。
根据调用者不同, 方法分为两种表现形式, 方法值和方法表达式。 两者都可像普通函数那样赋值和传参, 区别在于方法值绑定实例, 而方法表达式则须显式传参。
方法值
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}//Person定义了方法func (p *Person) PrintInfoPointer() { fmt.Printf("%p, %v\n", p, p)}func (p Person) PrintInfoValue() { fmt.Printf("%p, %v\n", &p, p)}func main(){ p := Person{"mike", 'm', 18} p.PrintInfoPointer() //0xc0000640c0, &{mike 109 18} pFunc1 := p.PrintInfoPointer //方法值, 隐式传递receiver pFunc1() //0xc0000640c0, &{mike 109 18} pFunc2 := p.PrintInfoValue pFunc2() //0xc000064140, {mike 109 18}}
方法表达式
package main //必须有个main包import "fmt"type Person struct { name string sex byte age int}//Person定义了方法func (p *Person) PrintInfoPointer() { fmt.Printf("%p, %v\n", p, p)}func (p Person) PrintInfoValue() { fmt.Printf("%p, %v\n", &p, p)}func main(){ p := Person{"mike", 'm', 18} p.PrintInfoPointer() //0xc0000640c0, &{mike 109 18} //方法表达式, 须显式传参 pFunc1 := (*Person).PrintInfoPointer pFunc1(&p) //0xc000004460, &{mike 109 18} pFunc2 := Person.PrintInfoValue pFunc2(p) //0xc0000044e0, {mike 109 18}}
总结:
结构体的方法集和非结构体的方法集区别
结构体的方法集
package mainimport ( "fmt")type T struct { int}func (t T) test() { fmt.Println("类型 T 方法集包含全部 receiver T 方法。")}func main() { t1 := T{1} fmt.Printf("t1 is : %v\n", t1) t1.test()}
非结构体的方法集
package mainimport ("fmt")type T intfunc (t T) test() { fmt.Println("类型 T 方法集包含全部 receiver T 方法。")}func main() { var t1 T t1 = 10 fmt.Printf("t1 is : %v\n", t1) t1.test()}
隐式传递和显示传递区别
package mainimport "fmt"type User struct { id int name string}func (self *User) Test() { fmt.Printf("%p, %v\n", self, self)}func main() { u := User{1, "Tom"} u.Test() mValue := u.Test mValue() // 隐式传递 receiver mExpression := (*User).Test mExpression(&u) // 显式传递 receiver}
立即复制 receiver, 因为不是指针类型, 不受后续修改影响。
package mainimport "fmt"type User struct { id int name string}func (self User) Test() { fmt.Println(self)}func main() { u := User{1, "Tom"} mValue := u.Test // 立即复制 receiver,因为不是指针类型,不受后续修改影响。 u.id, u.name = 2, "Jack" u.Test() mValue()}
Go 方法的本质:一个以方法所绑定类型实例为第一个参数的普通函数;
Go 语法甜头使得我们通过类型实例调用类型方法时无需考虑实例类型与 receiver 参数类型是否一致, 编译器会为我们做自动转换;
receiver 参数类型选择时要看是否要对类型实例进行修改; 如有修改需求, 则选择*T; 如无修改需求, T 类型 receiver 传值的性能损耗也是考量因素之一。