开发者学堂课程【Go 语言核心编程 - 面向对象、文件、单元测试、反射、TCP 编程:方法使用的深度剖析(1)】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/626/detail/9678
方法使用的深度剖析(1)
内容介绍:
一、方法的声明(定义)
二、方法注意事项和细节讨论(1)
三、方法注意事项和细节讨论(2)
一、方法的声明(定义)
1、语法:
func (recevier type) methodName(参数列表)(返回值列表){
方法体
return返回值}
关键词 func 后的括号用来确定方法与何种数据类型完成绑定。receiver 是 type 类型的一个变量,比如:Person结构体的一个变量;methodName是方法的名字,根据前面所讲的标识符的命名规则来写;参数列表类似于函数的形参列表,表示方法输入;返回值列表,方法如果有返回值,返回值就会写入返回值列表中;方法体表示该方法要完成的任务是什么,比如输入 return 表示返回值。recevier type:表示该方法和 type 类型进行绑定,或者说该方法作用于 type 类型,type 可以是结构体,也可以其它的自定义类型;返回值列表表示返回的值,可以是多个;方法主体:表示为了实现某一功能代码块;return 语句不是必须的。
2、具体说明:
1)参数列表
表示方法输入,有什么输入就在参数列表中写清楚。比如前面所举的例子,得到两个数的和,n1和n2就是输入的参数。
2) recevier type
表示这个方法和 type 这个类型进行绑定,或者说该方法作用于 type 类型,当然type 的类型根据实际情况而定。例如前面所讲的 person,或者是 student,就表示方法分别与 person、student 进行绑定。
3) receiver type
type 可以是结构体,也可以其它的普通自定义类型。例如 type integer int,也可以给 integer 进行绑定方法
4) Receiver
Receiver就是 type 类型的一个变量(实例),如果是结构体变量,有些程序员还是习惯于称作实例。比如:Person 结构体的一个变量(实例)
5) 返回值列表
表示返回的值,可以是多个
6) 方法主体
表示为了实现某一功能的代码块,根据实际需求来写,可以实现计算、连接数据库、添加数据等功能。
7) return 语句
Return 语句不是必预的,如果没有 return 语句,返回值列表也就不存在了;反之,如果有返回值列表,那么 return 语句一定存在。
二、方法注意事项和细节讨论(1)
结构体类型是值类型,在方法调用中,遵守值类型的传递机制,即值拷贝传递方式。
该注意事项在之前的内容中已经做了详细的解释和说明,因此在此不再进行赘述了。
三、方法注意事项和细节讨论(2)
如程序员希望在方法中修改结构体变量的值,可以通过结构体指针的方式来处理。
实际上,更多的是绑定结构体指针类型,而不是结构体本身,因为这样可以提高传输的速度、效率。如果是结构体类型的绑定,在传输的时候,进行值的拷贝;而如果是与结构体的指针进行绑定,拷贝的时候只进行地址的拷贝
举例说明:指针类型,而不是结构体
func (c *circle)area2() f1oat64 {
c.radius =10
return 3.14*(*c).radius*(*c).radius
)
方法与 circle 绑定时,需要完成值的拷贝,效率比较低。因此为了提高效率,通常会将方法与结构体的指针类型绑定,此时传入的是地址。因为c是指针,因此访问字段的标准方式是(*c).radius,(*c).radius 等价于 c.radius,因此可以输入为 return 3.14* c.radius*c.radius
进行调用时,首先创建一个circle的变量,传入数值7,标准调用方式如下:
var c circle
c.radius =5.0
res2 :=(&c). area()
fmt.Print1n("面积=",res2)
因为是c指针,所有在调用时,需要添加一个&,同样的道理,在取值的时候,要对值进行加*处理。但是这种调用方式比较麻烦,因此编译器底层做了优化,会自动的给变量添加上&,从而实现了 c.area() 等价为 (&c).area2()。调用代码可以写作:
var c circle
c.radius =5.0
res2 :=c. area()
fmt.Print1n("面积=",res2)
可以更改变量值检验上述的等价操作是否正确,将传入值7.0改为6.0后,运行查看效果,输入 cd.exercise go run main.go,可以看到输出的结果由78.5变成了113.83999……,值发生了改变,与理论值效果一致。虽然两种调用方式都可以得到正确的返回值,但是两者的机制完全不同。如果写作 c.radius=10,外侧c的值通过 fmt.Println(c.radius=",c.radius)
查看有没有发生变化,运行后发现c的值变成了10,因为在进行传输的时候是指针类型,此时c拷贝的不是所有的内容,只是拷贝了地址,因此指针直接指向了 main 函数中的c,所以在函数里对函数进行修改,外侧的值也会发生改变
内存分析图
说明分析
主函数即代表了一个主栈,它是一个独立的数据空间。主栈中存有c变量,c是一个结构体,它会指向一个数据空间,数据空间里存有字段 radius,值为7.0。紧接着会进行c.area2 的操作,会创建一个新的独立的栈——area2 栈,area2 也是一个独立的数据空间,此时不再时将值进行拷贝。当c与指针完成绑定时,仍然存在c变量,但此时c变量只相当于地址。main 栈本身也含有地址,假设 main 栈中的地址为0X11122,即会在 area2 栈就会将地址拷贝进来area栈中的c是一个指针,所以 area2 栈中c指向main 栈中的地址。按照原理来说,由于c是地址,所以不可以直接访问 radius 的值,按照标准应该是以(*)的格式进行访问,但是由于编译器的优化,会将(*)在底层进行添加,因此此时也是可以直接访问的。接下来进行运算 return 3.14*(*c).radius*(*c).radius,此时的值就是在 main 栈中获取的,这里与之前所讲的都不一样,没有所谓的结构体的数据空间。
为了打消顾虑,可以检验一下计算所需要的值是从哪里获取的,先将主栈里c的值取出,输入 fmt.Printf("main c 结构体变量地址 =%\n",&c),添加\n是为了便于查看。因为c本身就是一个指针,所以应该将它的内容取出,代码为 fmt.Printf("c是*Circle 指向的地址 =%p",c),不需要再利用&了,因为此时c本身就是一个地址,若此时通过上述代码取出的地址和已知的地址一致,即可证明上述结论正确。运行后发现,输出的地址地区与所知地址一样。