golang语言中的方法是与对象实例绑定的特殊函数,用于维护和展示对象的自身状态。
与函数的区别是方法有前置实例接收参数(receiver),编译器根据receiver来判断该方法属于哪个实例。receiver可以是基础类型,也可以是指针类型,这会关系到是否需要有可以修改对象实例的能力。
在调用方法时,可以使用对象实例值或指针,编译器会根据receiver类型自动在基础类型和指针类型之间转换,比如:
- typerectstruct{
- width,height,areaint
- }
- func(r*rect)pointer(){
- r.width+=2
- r.area=r.width*r.height
- }
- func(rrect)value(){
- r.width+=4
- r.area=r.width*r.height
- }
- funcmain(){
- r:=rect{width:10,height:5}
- r.value()
- fmt.Println(r)
- r.pointer()
- fmt.Println(r)
- /*
- r:=&rect{width:10,height:5}
- r.value()
- fmt.Println(r)
- r.pointer()
- fmt.Println(r)
- */
- }
- 输出:
- {1050}
- {12560}
如果使用指针调用方法时,需要注意不能使用多级指针,且必须使用合法的指针(包括nil)或能取实例地址,比如:
- typeXstruct{}
- func(x*X)test(){
- fmt.Println("hellogopher")
- }
- funcmain(){
- varx*X
- fmt.Println(x)
- x.test()
- &X{}.test()//cannottaketheaddressofXliteral
- }
如何选择方法的receiver类型?
- 如果要修改实例状态,用*T
- 如果不需要修改实例状态的小对象或固定值,建议用T
- 如果是大对象,建议用*T,可以减少复制成本
- 引用类型、字符串、函数等指针包装对象,用T
- 如果对象实例中包含Mutex等同步字段,用*T,以免因复制造成锁无效
- 无法确定需求的情况,都用*T
通过匿名字段的方法访问:
可以像访问匿名字段成员那样调用其方法,由编译器负责查找,比如:
- typepersonstruct{}
- typeManstruct{
- person
- }
- func(pperson)toWork()string{
- return"Tom"
- }
- funcmain(){
- varmMan
- fmt.Println(m.toWork())
- }
- 输出:
- Tom
如果Man结构体也有个同名的toWork方法,此时调用逻辑如下,比如:
- typepersonstruct{}
- typeManstruct{
- person
- }
- func(pperson)toWork()string{
- return"Tomtowork"
- }
- func(mMan)toWork()string{
- return"metowork"
- }
- funcmain(){
- varmMan
- fmt.Println(m.toWork())//metowork
- fmt.Println(m.person.toWork())//Tomtowork
- }
方法集:
GoLang规范中提到了一个与类型相关的方法集(method set),这决定了它是否实现了某个接口。
- 类型*T方法集包含所有receiver T + *T的方法
- typeSstruct{}
- typeTstruct{
- S
- }
- func(S)SVal(){}
- func(*S)SPtr(){}
- func(T)TVal(){}
- func(*T)TPtr(){}
- funcmethodSet(ainterface{}){
- t:=reflect.TypeOf(a)
- fori,n:=0,t.NumMethod();i<n;i++{
- m:=t.Method(i)
- fmt.Println(m.Name,m.Type)
- }
- }
- funcmain(){
- vartT
- methodSet(t)
- println("--------------")
- methodSet(&t)
- }
- 输出:
- SValfunc(main.T)
- TValfunc(main.T)
- --------------
- SPtrfunc(*main.T)
- SValfunc(*main.T)
- TPtrfunc(*main.T)
- TValfunc(*main.T)
很显然, 匿名字段就是为扩展方法集准备的。否则没有必要少写个字段。这种组合没有父子依赖关系,
整体与局部松耦合,可以任意增加来实现扩展。各单元互无关联,实现与维护更加简单。
方法表达式:
方法是一种特殊的函数,除了可以直接调用之外,还可以进行赋值或当作参数传递,下面是Go语言的方法定义格式,比如:
- func(pmytype)funcname(qtype)(r,stype){return0,0}
- instance.method(args)->(type).func(instance,args)
instance 就是Reciever,左边的称为Method Value;右边则是Method Expression,Go推荐使用左边形式。
Method Value是包装后的状态对象,总是与特定的对象实例关联在一起(类似闭包),
而Method Expression会被还原成普通的函数,将Receiver作为第一个显式参数,调用时需额外传递。
二者本质上没有区别,只是Method Value 看起来更像面向对象的格式,且编译器会自动进行类型转换;
Method Expression更直观,更底层,编译器不会进行类型转换,会按照实际表达意义去执行,更易于理解。
Method Expression:
- typePersonstruct{
- Ageint
- Namestring
- }
- func(pPerson)GetAge()int{
- returnp.Age
- }
- func(p*Person)SetAge(iint){
- p.Age=i
- }
- funcmain(){
- p:=Person{20,"Tom"}
- setAge:=(*Person).SetAge//(\*Person)必须用括号,整体相当于func(p*Person)
- setAge(&p,50)//编译器不会进行类型转换
- getAge:=Person.GetAge
- fmt.Println(getAge(p))
- }
- 输出:
- 50
Method Value:
- funcmain(){
- p:=Person{20,"Tom"}
- setAge:=p.SetAge//编译器会自动进行类型转换
- setAge(50)
- getAge:=p.GetAge
- fmt.Println(getAge())
- }
只要receiver参数类型正确,使用nil同样可以执行,比如:
- typeNint
- func(nN)value(){
- println(n)
- }
- func(n*N)pointer(){
- println(n)
- }
- funcmain(){
- varn*N
- n.pointer()
- (*N)(nil).pointer()
- (*N).pointer(nil)
- }
这样写程序并没有什么意义,只是希望你能理解并安全使用Method Value和Method Expression