Go语言基础(一)之函数调用、传参、反射机制
1.1 函数调用
package main
func myFunction(a,b int)(int,int){
return a+b,a-b
}
func main(){
myFunction(66,77)
}
使用编译命令go tool compile -SNl main.go ,得到汇编指令,根据此汇编指令分析调用myFunction之前的栈情况:
- 第一步,main函数通过SUBQ $40,sp 指令一共在栈上分配了40bytes的内存空间,并先压入栈基址指针。从栈顶到栈底空间代号为SP~SP16(16个字节,保存myFunction函数的俩参数)、SP16~SP32(16个字节,保存myFunction函数的俩返回值)、SP32~basepointer(8个字节,保存main函数的栈基址指针)
- 第二步,压入参数。myFunction函数的压参顺序:从右到左,此处即77位于SP8~SP16,66位于SP~SP8
- 第三步,调用函数myFunction。对应的汇编指令为CALL “”.myFunction
- 第四步,执行myFunction的汇编指令。首先会初始化返回地址并压入栈中,此时它为栈顶SP0~SP8,然后移动SP指针获取相对的参数地址(也就是之前的SP0~SP16,现在是SP8~SP24)执行业务操作,最终将返回值栈的指定地址空间即之前的SP16~SP32,现在的SP24~SP36.剩余的4个字节依然是basepointer。
总结:go使用栈来调用或传递参数及保存返回值,所以只需根据实际情况在栈上多申请内存就可以返回多个值
与C的区别:
C使用寄存器和栈来传递参数,特点在于:
- cpu访问栈的开销比寄存器大多了,所以相比go减少了很多开销
- 需单独处理参数过多的情况,增加了复杂度
GO使用栈来传递参数,特点在于:
- 不需考虑不同架构上的寄存器差异
- 不需考虑超过寄存器数量的参数如何处理
- 函数入参和出参的空间由栈来分配
1.2 参数传递
关键问题:传值还是传引用
区别:影响的是当我们在函数中对参数进行修改时会不会影响调用方看到的数据。
传值:调用时会对参数进行拷贝,调用方和被调用方持有两份不同的数据。
传引用:调用时会传递参数的指针,此时两方持有相同的数据,修改一方,另一方也会随之修改。
不同语言会由不同的传递方式,go选择了默认传值,当然也可以传引用/指针。
1.2.1 整型和数组
package main
func myFunction(i int ,arr [2]int){
fmt.Printf("in myFunction -i=(%d,%p) arr=(%v,%p)\n",i,&i,arr,&arr)
}
func main(){
i:=30
arr:=[2]int{66,77}
fmt.Printf("before call -i=(%d,%p) arr=(%v,%p)\n",i,&i,arr,&arr)
myFunction(i,arr)
fmt.Printf("after call -i=(%d,%p) arr=(%v,%p)\n",i,&i,arr,&arr)
$ go run main.go
before call -i=(30,0xc00009a000) arr=([66,77],0xc00009a010)
in myFunction -i=(30,0xc00009a008) arr=([66,77],0xc00009a020)
after call -i=(30,0xc00009a000) arr=([66,77],0xc00009a010)
可以看出main函数中i和arr的地址和myFunction函数中i和arr的地址不同,说明是参数是传值的方式。
若在myFunction中修改参数值:`
package main
func myFunction(i int ,arr [2]int){
i:=29
arr[1]=88
fmt.Printf("in myFunction -i=(%d,%p) arr=(%v,%p)\n",i,&i,arr,&arr)
}
func main(){
i:=30
arr:=[2]int{66,77}
fmt.Printf("before call -i=(%d,%p) arr=(%v,%p)\n",i,&i,arr,&arr)
myFunction(i,arr)
fmt.Printf("after call -i=(%d,%p) arr=(%v,%p)\n",i,&i,arr,&arr)
$ go run main.go
before call -i=(30,0xc00009a000) arr=([66,77],0xc00009a010)
in myFunction -i=(29,0xc00009a028) arr=([66,88],0xc00009a040)
after call -i=(30,0xc00009a000) arr=([66,77],0xc00009a010)
myFunction和main中的参数值和地址依然没有关系,所以整型和数组的传参方式为传值,是值的拷贝
1.2.2 结构体和指针
package main
type Mystruct struct{
i int
}
func myFunction(a Mystruct,b *Mystruct){
a.i=31
b.i=41
fmt.Printf("in myFunction -a=(%d,%p) -b=(%d,%p)\n",a,&a,b,&b)
}
func main(){
a:=Mystruct{i:30}
b:=&Mystruct{i:40}
fmt.Printf("before call -a=(%d,%p) -b=(%d,%p)\n",a,&a,b,&b)
myFunction(a,b)
fmt.Printf("after call -a=(%d,%p) -b=(%d,%p)\n",a,&a,b,&b)
}
$ go run main.go
before call -i=(30,0xc000018178) arr=(&{40},0xc00000c028)
in myFunction -i=(31,0xc000018198) arr=(&{41},0xc00000c038)
after call -i=(30,0xc000018178) arr=(&{41},0xc00000c028)
可以得出以下结论:
- 传递结构体时,传递的是值,会对值进行拷贝
- 传递结构体指针时,传递的是指针,仅对指针进行拷贝
b.i可以看作是(*b).i,即先获取指针b背后的结构体,再修改结构体的成员变量,下面分析go语言结构体在内存中的布局:
type Mystruct struct{
i int
j int
}
func myFunction(ms *Mystruct){
ptr:=unsafe.Pointer(ms)#位于栈顶的基地址
for i:=0;i<2;i++{
c:=(*int)(unsafe.Pointer((uintptr(ptr)+uintptr(8*i))))#每次循环,基地址移动8个字节访问下个成员
*c+=i+1
fmt.Printf("[%p] %d\n",c,*c)
}
}
func main(){
a:=&Mystruct{i:40,j:50}
myFunction(a)#传参传入的是指针,因此下面的printf地址不变
fmt.Printf("[%p] %v\n",a,*a)
}
$ go run main.go
[0xc000018180] 41
[0xc000018188] 52
[0xc000018180] &{41,52}
通过指针的方式修改结构体中的成员变量,结构体在内存中是一片连续空间,指向结构体的指针也是指向结构体的首地址,将Mystruct指针修改成int类型,访问新指针就会返回整型i。
所以go中,将指针作为参数时,会对指针进行复制,此时两个指针都指向同一片内存空间。所以在传递数组或内存很大的结构体时,尽量使用指针作为参数来传递 ,防止大量数据拷贝影响性能。
1.2.3 接口
一组方法的签名或集合。
作用:通过接口与具体实现分离,解除上下游的耦合,上层模块不再需要依赖下层模块,只需依赖好一个具体的接口。
隐式接口
type error interface{
Error() string
}
type RPCerror struct{
code int64
message string
}
func (e *RPCerror) Error() string{
return fmt.Sprintf(e.message,e.code)
}
- 在Java中,实现接口需要显式的声明接口并实现所有方法
- 在go中,只要实现了接口中的所有方法就隐式的实现了接口
- 此处可以看出RPCerror结构体实现了error接口,但是使用的时候并不关心它实现了哪些接口,只有在传递参数、返回参数以及变量赋值时才会进行类型检查。
func main(){
var rpcErr error=NewRPCerror(400,'unknown error')//变量赋值,类型检查
err:=AsErr(rpcErr)//类型检查,传递参数
println(err)
}
func NewRPCerror(code int64,msg string) error {
return &RPCerror{//类型检查,返回参数
code:code
message:msg}}
func AsErr(err error) error {
return err
}
结构体(指针)与接口(重要,易混)
同时使用接口和指针存在的困惑的问题
type cat struct{}
type Duck interface{
Quack()
}
func (c cat) Quack {}//结构体实现接口
func (c *cat) Quack {}//结构体指针实现接口
var d Duck = cat{}//结构体初始化接口
var d Duck = &cat{}//结构体指针初始化接口
实现接口和初始化接口与结构体和结构体指针组成了以下四种情况,并不都能通过编译
编译结果 |
结构体实现接口 |
结构体指针实现接口 |
结构体初始化变量 |
通过 |
不通过 |
结构体指针初始化变量 |
通过 |
通过 |
//结构体实现接口,结构体(指针)初始化变量
type cat struct{}
func (c cat) Quack() {
fmt.Println('ff'}
}
func main(){
var c Duck = cat{}/&cat{}//结构体指针可以隐式的获取唯一指向的结构体
c.Quack()
}
//结构体指针实现接口,结构体初始化变量
type cat struct{}
func (c *cat) Quack() {
fmt.Println('ff'}
}
func main(){
var c Duck = cat{}//编译报错,新建了一个全新cat类型,cat类型没有实现Duck接口,之前实现Duck接口的是指向之前的cat类型的指针
c.Quack()
}
附:书中第132页还对这几个指针和结构体的类型转换的底层汇编作了分析理解
注意:接口是不同于结构体、数组等数据结构的单独的数据结构
例如:
type TestStruct struct{}
func NilorNot(v interface{}) bool{
return v==nil
}
func main(){
var s *TestStruct
fmt.Println(s==nil)//True
fmt.Println(NilorNot(s))//False
}
NilorNot(s)返回为False的原因是参数的传递与赋值发生了类型的隐式转换,*TestStruct此结构体指针类型转换成了interface{}类型,转换后的变量包含了转换前的变量和类型信息。
1.3 反射机制
反射就是程序在运行时能够 “观察” 并且修改自己的行为,主要依赖reflect包实现简化代码逻辑。
包中主要有两个函数和两个类型:
- 函数:reflect.TypeOf获取入参的类型信息,reflect.ValueOf获取数据运行时的值
- 类型:Type,一个接口,包含methodbyname、implements等方法;Value,一个结构体,没有对外暴露的字段,提供了获取或者写入数据的方法。
反射包中所有方法都是围绕着Type和Value这两个反射对象设计的。反射中涉及到三种数据类型:go语言基本类型(int,string,float等)、空接口类型(interface{})、反射对象(使用了reflect包后得到的对象的统称,如reflect.TypeOf(author)、reflect.ValueOf(author)都是反射对象)
一个例子引入
func main() {
rv := []interface{}{"hi", 42, func() {}}
for _, v := range rv {
switch v := reflect.ValueOf(v); v.Kind() {
case reflect.String:
fmt.Println(v.String())
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
fmt.Println(v.Int())
default:
fmt.Printf("unhandled kind %s", v.Kind())
}
}
}
-----output----
hi
42
unhandled kind func
在程序中主要是声明了 rv 变量,变量类型为 interface{},其包含 3 个不同类型的值,分别是字符串、数字、闭包。
而在使用 interface{} 时常见于不知道入参者具体的基本类型是什么,那么我们就会用 interface{} 类型来做一个伪 “泛型”。
此时又会引出一个新的问题,既然入参是 interface{},那么出参时呢?
func F(interface{}) interface{},出参类型interface{}是int还是string还是其他,因此必然离不开类型的判断,这时候就要用到反射,也就是 reflect 标准库。反射过后又再进行 (type) 的类型断言。
1.3.1 三大法则
1.从interface{}变量可以反射出反射对象。
func main(){
author:='drave'
fmt.Println(reflect.TypeOf(author))//string
fmt.Println(reflect.ValueOf(author))//drave
}
TypeOf和ValueOf是链接go语言类型到反射类型转换的桥梁,虽然这里看着author是string类型,但是这俩函数TypeOf和ValueOf的入参为interface{}类型,中间发生了string到interface{}的类型转换,获取了反射对象后(Type和Value这两种)就可以使用反射包里的方法了
2.从反射对象可以还原回interface{}变量
func main(){
author:='drave'
//经历了基本类型-》空接口类型-》反射对象
fmt.Println(reflect.TypeOf(author))//string
fmt.Println(reflect.ValueOf(author))//drave
//经历了反射对象-》空接口类型-》基本类型
fmt.Println(reflect.ValueOf(author).Interface().(string))//还原
}
3.要修改反射对象,其值必须可修改
反射的意义:
因为反射在工程实践中,目的一就是可以获取到值和类型,其二就是要能够修改他的值,否则反射出来只能看,不能动,就会造成这个反射很鸡肋。例如:应用程序中的配置热更新,必然会涉及配置项相关的变量变动,大多会使用到反射来变动初始值
func main(){
i:=1
v:=reflect.ValueOf(i)
v.SetInt(10)
fmt.Println(i)//这样会报错,因为go中函数传参是值传递,这里的两个i没有任何关系,正确的应该是
i:=1
v:=reflect.ValueOf(&i)//获取变量指针
v.Elem().SetInt(10)//v.Elem()获取指针指向的变量,然后SetInt更新值
fmt.Println(i)
//借助以下来理解
i:=1
v:=&i//获取变量指针
*v=10//*v获取变量指针指向的值,即1,并修改
//从这里可以发现&i一般是等式右边,指变量地址;*v是等式左边,指地址的值
}
其他函数的意思
func (v Value) NumField() int
返回v这个结构体类型值的字段数,如果v的Kind不是Struct会panic
(v Value) Field(i int) Value
返回结构体的第i个字段(的Value封装)。如果v的Kind不是Struct或i出界会panic
func (v Value) NumMethod() int
返回v的方法数量
func (v Value) Method(i int)Value
返回v的第i个方法
func (v Value) MethodByName(name string) Value
示例:通过反射来遍历结构体
package main
import (
"fmt"
"reflect"
)
//使用反射来遍历结构体的字段,调用结构体的方法,修改结构体字段的值,并获取结构体标签的值
//定义结构体
type Student struct {
Name string `json:"name"` // 是 ` ` (tab键上的~按键) ,不是 ' '
Sex string `json:"sex"`
Age int `json:"age"`
Sal float64 `json:"sal"`
}
func (s Student) GetName() string { //第0个方法
fmt.Println("该结构体Name字段值为:",s.Name)
return s.Name
}
func (s *Student) Set(newName string,newAge int,newSal float64){ //第2个方法
s.Name = newName
s.Age = newAge
s.Sal = newSal
s.Print()
}
func (s Student) Print() { //第1个方法
fmt.Println("调用 Print 函数输出结构体:",s)
}
//反射获取结构体字段、方法,并调用
func testReflect(b interface{}) {
rVal := reflect.ValueOf(b).Elem()
rValI := reflect.ValueOf(b)
rType := reflect.TypeOf(b).Elem()
//判断是否是结构体在进行下一步操作
if rType.Kind() != reflect.Struct{
fmt.Println("该类型不是结构体。所以无法获取字段及其方法。")
}
//获取字段数量
numField := rVal.NumField()
fmt.Printf("该结构体有%d个字段\n",numField)
//遍历字段
for i := 0; i < numField; i++ {
//获取字段值、标签值
rFieldTag := rType.Field(i).Tag.Get("json")
if rFieldTag != "" {
fmt.Printf("结构体第 %v 个字段值为:%v ," +
"Tag‘json’名为:%v\n",i,rVal.Field(i),rFieldTag)
}
}
//获取方法数量
numMethod := rValI.NumMethod() //用指针可以获取到指针接收的方法
fmt.Printf("该结构体有%d个方法\n",numMethod)
//调用方法(方法顺序 按照ACSII码排序)
rVal.Method(0).Call(nil)
rVal.Method(1).Call(nil)
//参数也需要以 Value 的切片 传入
params := make([]reflect.Value ,3)
params[0] = reflect.ValueOf("hhhh")
params[1] = reflect.ValueOf(28)
params[2] = reflect.ValueOf(99.9)
rValI.Method(2).Call(params)
rVal.Method(1).Call(nil)
}
func main() {
stu := Student{
Name: "莉莉安",
Sex: "f",
Age: 19,
Sal: 98.5,
}
//调用编写的函数并输出
testReflect(&stu)
fmt.Println("主函数输出结构体 Student :",stu)
}
其他的根据网上资料用到再学
https://www.cnblogs.com/l1ng14/p/13921985.html 反射包的其他函数及属性
1.4 类型断言与转换
首先,让我们看看它们长什么样……
下面是一个类型断言的例子:
var greeting interface{} = "hello world"
greetingStr := greeting.(string)
接着看一个类型转换的例子:
greeting := []byte("hello world")
greetingStr := string(greeting)
最明显的不同点是他们具有不同的语法(variable.(type) vs type(variable) )。
1.4.1 类型断言
类型断言用于断言变量是属于某种类型。类型断言只能发生在interface{}类型上。上面类型断言的例子,greeting是一个interface{}类型,我们为其分配了一个字符串。现在,我们可以认为greeting实际上是一个string,但是对外展示的是一个interface{}。如果我们想获取greeting的原始类型,那么我们可以断言它是个string,并且此断言操作会返回其string类型。
这意味着在做类型断言的时候,我们应该知道任何变量的基础类型。但是情况并非总是这样的,这就是为什么类型断言操作实际上还返回了第二个可选值的原因。
var greeting interface{} = "42"
greetingStr, ok := greeting.(string)
第二个值是一个布尔值,如果断言正确,返回 true ,否则返回 false。
以下是一个类型判断的例子,
var greeting interface{} = 42
switch g := greeting.(type) {
case string:
fmt.Println("g is a string with length", len(g))
case int:
fmt.Println("g is an integer, whose value is", g)
default:
fmt.Println("I don't know what g is")
}
1.4.2 类型转换
只有当基础数据结构类型相同,类型之间才可以相互转换。
// `myInt` 是一个新类型,它的基类型是 `int`
type myInt int
// AddOne 方法适用于 `myInt` 类型,不适用于 `int` 类型
func (i myInt) AddOne() myInt { return i + 1}
func main() {
var i myInt = 4
fmt.Println(i.AddOne())
}
var i myInt = 4
originalInt := int(i)
//myInt的基础类型为int,因此与int基础类型相同,可以转换