https://golang.org/ref/spec#Method_declarations
https://golang.org/ref/spec#Function_declarations
https://tour.golang.org/methods/1
Official Golang document
任何行业,最简单的技能或知识都是最基础的,基础得你从来没考虑过为什么,就像一种习惯。但这往往是你遇到的坑和解决问题的突破口。
该篇来介绍一下Golang语言的Method,从现象到理论,然后再从不同的角度来深度地解释和证明Method就是Function。
带着问题寻找答案
代码片段1
type Foo struct { tag string}func (f Foo) change() { f.tag = "bar"}func TestMethodUsingAsFunc(t *testing.T) { f := Foo{tag: "foo"} fmt.Printf("Foo initialized: %#v \n", f) f.change() fmt.Printf("Foo changed: %#v \n", f)}
定义了一个含有 string 属性 tag 的结构体 Foo 和一个对应方法 change。
在测试函数里面第10行初始化一个 Foo 类型的变量 f,设置 tag 属性为 "foo",接着打印初始化的 f 值。然后第13行调用 Foo 的方法 change,修改 tag 属性为"bar",接着打印 f 值。
从代码看,预期的结果应该是 f 的属性 tag 会变成 "bar",但是运行代码,真实的结果如下
Foo initialized: test.Foo{tag:"foo"} Foo changed: test.Foo{tag:"foo"}
至此,先不着急解释为什么,现在将代码片段1中第5行 change 方法的接收者类型修改为引用型
func (f *Foo) change()
再次运行代码,看下结果
Foo initialized: test.Foo{tag:"foo"} Foo changed: test.Foo{tag:"bar"}
这次才真正符合我们的预期,通过调用 change 方法确实修改了 tag 属性。
接下来我们一点点从概念、理论、使用、运行时反射、panic stack trace来解释。
什么是函数函数就是一个代码块,能接收0或多个参数,且能返回0或多个返回值。函数的声明如下
FunctionDecl = "func" FunctionName Signature [ FunctionBody ] .FunctionName = identifier .FunctionBody = Block .// eg:func sum(a,b int) int{ return a+b}
什么是方法方法就是一个带有接收者 Receiver 的函数,方法的声明如下
MethodDecl = "func" Receiver MethodName Signature [ FunctionBody ] .Receiver = Parameters .
从声明的格式上看,接收者本身也是一个参数,位于 "func" 和方法名中间,且必须是一个。
另外如果你定义了一个接收者,但是在方法体内并没有用到这个接收者,那就没必要声明方法了,直接声明函数即可。
方法就是将接收者作为第一个参数的函数
看一个代码片段
func (p *Point) Scale(factor float64) { p.x *= factor p.y *= factor}
其实第1行方法的写法对应函数的写法是
func Scale(p *Point, factor float64) { p.x *= factor p.y *= factor}
就是把方法 Scale 的接收者 p,作为函数 Scale 的第一个参数。
从调用的语法上也是支持这样的,也许是个语法糖而已,便于阅读罢了。但是反过来不行,即你把函数当作方法来调用,是不可以的。
Go里面方法的写法更像Objective-c或Swift函数的写法
//objective-c的函数- (void)selectRowAtIndexPath:(NSIndexPath *)indexPath animated:(BOOL)animated scrollPosition:(UITableViewScrollPosition)scrollPosition;
Objective-c函数的参数是分割在函数名的不同地方,一个很明显的好处就是这个函数是可以按照自然语言直接读和理解的。
把方法当作函数调用现在我们就修改代码片段1,把方法当作函数来调用代码片段2
type Foo struct { tag string}func (f Foo) change() { f.tag = "bar"}func TestMethodUsingAsFunc(t *testing.T) { f := Foo{tag: "foo"} fmt.Printf("Foo initialized: %#v \n", f) (Foo).change(f) fmt.Printf("Foo changed: %#v \n", f)}
在第13行就是将方法的调用f.change() 修改为函数的调用(Foo).change(f),是不是将方法的接收者f,作为函数change的第一个参数。
另外为什么第13行函数前需要加上"Foo.",这个一会在反射reflect再解释。
运行结果和代码片段1是一样的
Foo initialized: test.Foo{tag:"foo"} Foo changed: test.Foo{tag:"foo"}
把方法当作函数来调用就能很清晰的明白为什么是这样的运行结果了,因为在Go中所有的参数传递、赋值都是值传递,即会copy一份当前传的值。上面的写法就是声明是一个Foo,在调用的方法体内的接收者是一个新的且和声明的那个Foo一模一样的Foo,所以在方法体内修改属性tag是不会影响声明的那个Foo的。但是将接收者修改为引用类型就不然了,虽然在调用方法时也是copy了一份当前传的引用,但引用最终指的真实值的内存地址都是一个。
方法的接收者是nil,但是并没有panic比如有时对方法定义不规范,在方法体内并没有引用接收者,此时把接收者定义为nil,然后调用方法,是不会panic的,这就不像其他面向对象的语言那样会直接抛空指针异常的。 代码片段3type Foo struct { tag string}func (f *Foo) change() { fmt.Printf("receiver is %#v\n", f)}func TestMethodUsingAsFunc(t *testing.T) { var f *Foo fmt.Printf("Foo initialized: %#v \n", f) f.change() fmt.Printf("Foo changed: %#v \n", f)}
第5行将方法的接收者类型定义为Foo的引用类型,在方法体内不引用f,仅仅一条打印语句。
第10行声明一个Foo引用类型f,但初始值是nil.
第13行调用方法change
运行代码的结果,正如上面所说,并没有panic异常,而是正常调用。
Foo initialized: (*test.Foo)(nil) receiver is (*test.Foo)(nil)Foo changed: (*test.Foo)(nil)
通过运行时的reflect验证方法就是函数
方法在reflect中的定义如下
type Method struct { // Name is the method name. // PkgPath is the package path that qualifies a lower case (unexported) // method name. It is empty for upper case (exported) method names. // The combination of PkgPath and Name uniquely identifies a method // in a method set. // See https://golang.org/ref/spec#Uniqueness_of_identifiers Name string PkgPath string Type Type // method type Func Value // func with receiver as first argument Index int // index for Type.Method}
除了其他基本信息之外,我们看Func属性,他是一个把接收者作为第一个参数的函数。现在就主要研究Func到底是什么,当然都是一个Value。
说千道万不如撸一下~代码
代码片段4
type Foo struct { tag string}func (f *Foo) Change(a int) { f.tag = "bar"}func TestMethodUsingAsFunc(t *testing.T) { f := &Foo{tag: "foo"} rv := reflect.TypeOf(f) for i := 0; i < rv.NumMethod(); i++ { fmt.Printf("Foo's method %#v\n", rv.Method(i)) fmt.Printf("Change method correspondding func %#v\n", rv.Method(i).Func) }}
我们修改Foo的Change方法为可导出型的,要不通过reflect获取不到方法列表,同时给方法添加了一个整型参数,作为参照一起分析参数列表。
第13行输出类型*Foo的方法,第14行输出某个方法对应的函数定义。
运行输出的结果
Foo's method reflect.Method{Name:"Change", PkgPath:"", Type:(*reflect.rtype)(0xc000116180), Func:reflect.Value{typ:(*reflect.rtype)(0xc000116180), ptr:(unsafe.Pointer)(0xc000114040), flag:0x13}, Index:0}Change method correspondding func (func(*test.Foo, int))(0x1125e50)
我们只看第2行的 func(*test.Foo, int),一下子就更明白了,在代码片段4中定义Change方法的接收者作为函数的第一个参数。
其实方法 func (f *Foo) Change(a int)最终是一个函数。
通过panic stack trace进一步验证方法就是函数
这里不多说,请大家之前写的 “司空见惯的panic异常栈stack traces,你看得懂吗?”的系列文章2,当然还是要结合1看的,从更底层的角度解释方法就是函数。
司空见惯的panic异常栈stack traces,你看得懂吗? 程序员届的小学生,公众号:New2coder(2/2)司空见惯的panic异常栈stack traces,你看得懂吗?
篇篇更精彩,章章有深度