余晖落尽暮晚霞,黄昏迟暮远山寻
本站
当前位置:网站首页 > 编程知识 > 正文

从理论、语法、RT、stacktrace论方法是函数go

xiyangw 2022-12-03 13:12 20 浏览 0 评论

https://golang.org/ref/spec#Method_declarations

https://golang.org/ref/spec#Function_declarations

从理论、语法、RT、stacktrace论方法是函数go

https://tour.golang.org/methods/1



任何行业,最简单的技能或知识都是最基础的,基础得你从来没考虑过为什么,就像一种习惯。但这往往是你遇到的坑和解决问题的突破口。

该篇来介绍一下Golang语言的Method,从现象到理论,然后再从不同的角度来深度地解释和证明Method就是Function。


带着问题寻找答案

代码片段1

1 type Foo struct {
2   tag string
3 }
4
5 func (f Foo) change() {
6   f.tag = "bar"
7 }
8
9 func TestMethodUsingAsFunc(t *testing.T) {
10  f := Foo{tag: "foo"}
11  fmt.Printf("Foo initialized: %#v \n", f)
12
13  f.change()
14  fmt.Printf("Foo changed: %#v \n", f)
15 }

  • 定义了一个含有 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

1 type Foo struct {
2   tag string
3 }
4 
5 func (f Foo) change() {
6   f.tag = "bar"
7 }
8 
9 func TestMethodUsingAsFunc(t *testing.T) {
10  f := Foo{tag: "foo"}
11  fmt.Printf("Foo initialized: %#v \n", f)
12
13  (Foo).change(f)
14  fmt.Printf("Foo changed: %#v \n", f)
15 }

在第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的,这就不像其他面向对象的语言那样会直接抛空指针异常的。


代码片段3

1 type Foo struct {
2   tag string
3 }
4
5 func (f *Foo) change() {
6   fmt.Printf("receiver is %#v\n", f)
7 }
8
9 func TestMethodUsingAsFunc(t *testing.T) {
10  var f *Foo
11  fmt.Printf("Foo initialized: %#v \n", f)
12
13  f.change()
14  fmt.Printf("Foo changed: %#v \n", f)
15 }


  • 第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?

1 type Foo struct {
2   tag string
3 }
4
5 func (f *Foo) Change(a int) {
6   f.tag = "bar"
7 }
8
9 func TestMethodUsingAsFunc(t *testing.T) {
10   f := &Foo{tag: "foo"}
11   rv := reflect.TypeOf(f)
12   for i := 0; i < rv.NumMethod(); i++ {
13     fmt.Printf("Foo's method %#v\n", rv.Method(i))
14     fmt.Printf("Change method correspondding func %#v\n", rv.Method(i).Func)
15   }
16 }
  • 我们修改Foo的Change方法为可导出型的,要不通过reflect获取不到方法列表,同时给方法添加了一个整型参数,作为参照一起分析参数列表。
  • 第13行输出类型*Foo的方法,第14行输出某个方法对应的函数定义。

运行输出的结果

1 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}
2 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进一步验证方法就是函数

这里不多说,请大家之前写的 “深度解释 stack trace in Go”的系列文章2,当然还是要结合1看的,从更底层的角度解释方法就是函数。

(2/2)深度解释 stack trace in Go



篇篇更精彩,章章有深度

相关推荐

spring利用spring.handlers解析自定义配置(spring validation 自定义)

一、问题我们在spring的xml配置文件里经常定义各种各样的配置(tx、bean、mvc、bean等等)。以及集成第三方框架时,也会看到一些spring之外的配置,例如dubbo的配置、securi...

「Spring源码分析」AOP源码解析(上篇)(spring源码深度解析(第2版))

前言前面写了六篇文章详细地分析了SpringBean加载流程,这部分完了之后就要进入一个比较困难的部分了,就是AOP的实现原理分析。为了探究AOP实现原理,首先定义几个类,一个Dao接口:1&nbs...

Spring 解析注册BeanDefinition这一篇就Over
Spring 解析注册BeanDefinition这一篇就Over

一、简介:学习过Spring框架的人一定都会听过Spring的IoC(控制反转)、DI(依赖注入)这两个概念,对于初学Spring的人来说,总觉得IoC、...

2023-03-20 14:53 xiyangw

域、模块、空间、闭包,你真的懂了吗?(模块控制域与作用域的关系)

Javascript有一个特性叫做域。尽管对于初学者来说理解域是有难度的,但我会尽力用最简单的方式让你理解域。理解域能让你的代码更优秀,减少错误,及有助于你做出更强大的模式设计。什么是域域是在运行时,...

这一次搞懂Spring自定义标签以及注解解析原理
这一次搞懂Spring自定义标签以及注解解析原理

前言在上一篇文章中分析了Spring是如何解析默认标签的,并封装为BeanDefinition注册到缓存中,这一篇就来看看对于像context这种自定义标签是如...

2023-03-20 14:53 xiyangw

前端基础进阶(七)-前端工程师最容易出错的问题-this关键字
前端基础进阶(七)-前端工程师最容易出错的问题-this关键字

我们在学习JavaScript的时候,因为对一些概念不是很清楚,但是又会通过一些简洁的方式把它给记下来,那么这样自己记下来的概念和真正的概念产生了很强的偏差.当...

2023-03-20 14:52 xiyangw

深入K8s:守护进程DaemonSet及其源码分析(k8s 进程)
深入K8s:守护进程DaemonSet及其源码分析(k8s 进程)

建议学习:膜拜!阿里内部都在强推的K8S(kubernetes)学习指南,不能再详细了最近也一直在加班,处理项目中的事情,发现问题越多越是感觉自己的能力不足,...

2023-03-20 14:52 xiyangw

Spring 是如何解析 bean 标签的?(spring beans标签)
Spring 是如何解析 bean 标签的?(spring beans标签)

前情回顾上回「SpringIoC容器初始化(2)」说到了Spring如何解析我们定义的<bean>标签,代码跟进了一层又一层,跋山涉水,...

2023-03-20 14:52 xiyangw

快速了解JavaScript文本框操作(javascript文本框代码)
快速了解JavaScript文本框操作(javascript文本框代码)

HTML中使用<input>元素表示单行输入框和<textarea>元素表示多行文本框。HTML中使用的<input&...

2023-03-20 14:51 xiyangw

荐读|30道JavaOOP面试题,可以和面试官扯皮了
荐读|30道JavaOOP面试题,可以和面试官扯皮了

面试是我们每个人都要经历的事情,大部分人且不止一次,今天给大家准备了30道JavaOOP面试题,希望能够帮助到对Java感兴趣的同学,让大家在找工作的时候能够...

2023-03-20 14:51 xiyangw

源码系列——mybatis源码刨析总结,下(mybatis源码分析)
源码系列——mybatis源码刨析总结,下(mybatis源码分析)

接上文简答题一.1.Mybatis动态sql是做什么的?1.动态sql就是根据条件标签动态的拼接sql,包括判空,循环,拼接等2.哪些动态sql?动态sql大...

2023-03-20 14:50 xiyangw

Java面试题(第二弹)(java面试题及答案整理)
Java面试题(第二弹)(java面试题及答案整理)

1.抽象类和接口的区别?接口可以被多重implements,抽象类只能被单一extends接口只有定义,抽象类可以有定义和实现接口的字段定义默认为:public...

2023-03-20 14:50 xiyangw

mybatis3 源码深度解析-动态 sql 实现原理(sql数据库基础知识)
mybatis3 源码深度解析-动态 sql 实现原理(sql数据库基础知识)

大纲动态sql使用示例SqlSource和BoundSql以及实现类LanguageDriver以及实现类SqlNode以及实现类动态sql解...

2023-03-20 14:50 xiyangw

第43节 Text、Comment及CDATASection(第43节 Text、Comment及CDATASection)
第43节 Text、Comment及CDATASection(第43节 Text、Comment及CDATASection)

本内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。文本节点用Text类型表示,包含的是可以按字面解释...

2023-03-20 14:49 xiyangw

Qt读写三种文件(qt读取文件数据并赋值给变量)

第一种INI配置文件.ini文件是InitializationFile的缩写,即初始化文件。除了windows现在很多其他操作系统下面的应用软件也有.ini文件,用来配置应用软件以实现不同用户的要...

取消回复欢迎 发表评论: