Golang 通过反射的方式调用结构体方法
在Go语言中,反射就是用来检查储存在接口变量内部pair对的一种机制,pair对是以值(value)和实际类型(concrete type)组成.在go中提供两种方法让我们可以轻松地访问接口变量的内容,分别是 reflect.ValueOf()和 reflect.TypeOf()
reflect.ValueOf(i interface{} )
用来获取输入参数接口中的数据的值,如果接口为空则返回0
reflect.TypeOf(i interface{} )
用来获取输入参数接口中的值的类型,如果接口为空则返回nil
var num float64 = 1.2345
fmt.Println(“type : ” , reflect.TypeOf(num)) //float64
fmt.Println(“type : ” , reflect.ValueOf(num)) //1.2345
这说明反射可以将“接口类型变量”转换为“反射类型变量” , 反射类型指的就是reflect.Type和reflect.Value
在构建框架工程的时候,需要可以随意扩展的方法,或者说在Web程序框架设计中编写调度分发控制器的时候,往往需要用到反射(reflect)来完成相关工作
以下例子是演示通过反射来调用结构体方法:
package main import ( "fmt" "reflect" ) type User struct{ Id int Name string Age int } //ToString方法 func (u User) String() string { return "User[ Id " + string(u.Id) +"]" } //设置Name方法 func (u *User) SetName(name string) string{ oldName := u.Name u.Name = name return oldName } //年龄数+1 func (u *User) AddAge() bool { u.Age++ return true } //测试方法 func (u User) TestUser() { fmt.Println("我只是输出某些内容而已....") } func main(){ //通过反射的方式调用结构体类型的方法 var setNameStr string = "SetName" var addAgeStr string = "AddAge" user := User{ Id : 1, Name : "env107" , Age : 18 , } //1.获取到结构体类型变量的反射类型 refUser:= reflect.ValueOf(&user) //需要传入指针,后面再解析 fmt.Println(refUser) //2.获取确切的方法名 //带参数调用方式 setNameMethod := refUser.MethodByName( setNameStr ) args := []reflect.Value{ reflect.ValueOf("Mike") } //构造一个类型为reflect.Value的切片 setNameMethod.Call(args) //返回Value类型 //不带参数调用方式 addAgeMethod := refUser.MethodByName( addAgeStr ) addAgeMethod.Call( make([]reflect.Value , 0) ) fmt.Println("User.Name = ",user.Name) fmt.Println("User.Age = ",user.Age) }
上述代码运行的结果将会是
<*main.User Value> User.Name = Mike User.Age = 19
实例二
main.go
var regStruct = make(map[string]interface{}) func init() { regStruct["server"] = &router.Server{} regStruct["api"] = &router.Api{} } flag.Parse() args := flag.Args() if len(args[2]) == 0 { return } var mapStr map[string]any json.Unmarshal([]byte(args[2]), &mapStr) //./cobra server param '{"contact":"contac2t","tel":"tel2","m":"php","c":"server","a":"AAA" }' if _, ok := mapStr["c"]; !ok { return } if _, ok := mapStr["a"]; !ok { return } c := mapStr["c"].(string) a := mapStr["a"].(string) delete(mapStr, "c") delete(mapStr, "a") if regStruct[c] != nil { router.Call(regStruct, c, a, mapStr) }
base.go
package router import ( "reflect" ) func Call(m map[string]interface{}, name string, tableName string, param interface{}) (result []reflect.Value, err error) { rf := reflect.ValueOf(m[name]) //带参数调用方式 setNameMethod := rf.MethodByName(tableName) args := []reflect.Value{reflect.ValueOf(param)} //构造一个类型为reflect.Value的切片 setNameMethod.Call(args) //返回Value类型 //不带参数调用方式 //setNameMethod2 := rf.MethodByName(tableName) //setNameMethod2.Call([]reflect.Value{}) return }
Server.go
type Server struct { } func (router *Server) AAA(args ...interface{}) { param, _ := json.Marshal(args[0]) fmt.Println(string(param)) } func (router *Server) BBB(args ...interface{}) { param, _ := json.Marshal(args[0]) fmt.Println(string(param)) } func (router *Server) CCC(args ...interface{}) { param, _ := json.Marshal(args[0]) fmt.Println(string(param)) }
api.go
type Api struct { } func (router *Api) BBB(args ...interface{}) { param, _ := json.Marshal(args[0]) fmt.Println(string(param)) }
可见,通过反射的方式成功的将user的名字更改为Mike并将Age的数+1
在这里需要思考一个问题,方法TestUser接收者的类型是User类型而非User的指针类型,在结构体当中,接收者类型的区别将影响该结构体方法的可见性。假设接收者类型为指针类型则该方法称为指针方法,假如是值类型,则该方法称为值方法。
如果将代码改成
refUser:= reflect.ValueOf(user)
在我们后续通过反射调用TestUser的时候,将会引起恐慌
panic: reflect: call of reflect.Value.Call on zero Value
造成该恐慌的原因是refUser.MethodByName( setNameStr )并没有返回一个reflect.Value而是一个nil
原因在于,User类型是*User的基底类型,在Go的指针知识中,有一条规则:一个指针类型拥有它以及它的基底类型为接收者类型的所有方法,而它的基底类型却只能拥有以它本身为接收者类型的方法。
也就是说,User为*User基底类型,所以User类型只能拥有以它本身为接收者类型(User类型)的方法,也就是TestUser , 而指针方法(SetName和AddAge)只有*User类型才拥有,而 refUser:= reflect.ValueOf(user) 拿到的是User类型的反射类型对象,因此并没有指针方法,引起恐慌。
这里补充一下指针方法和值方法的区别
type User struct{ Id int Name string Age int } func (u *User) test1() {} user := User{ Id : 1, Name : "env107" , Age : 18 , }
以接收者类型为*User的结构体方法,其中变量u是user的值的指针的副本,但如果接收者类型为User的结构体方法,也就是值方法,则变量u是user的一个副本,如果尝试改变u的副本的属性值,则对user的属性值是不会造成影响的