Golang 反射 runtime

有时候在Go的函数调用的过程中,我们需要知道函数被谁调用,比如打印日志信息等。例如下面的函数,我们希望在日志中打印出调用者的名字。你可以通过runtime.Callerruntime.Callersruntime.FuncForPC等函数更详细的跟踪函数的调用堆栈。

  •  Caller获取调用函数信息 参数作用:0表示调用函数本身
  • Callers 获取程序计算器,第二个参数会返回程序计算器列表,return值是个数
  • CallersFrames 获取栈的全部信息,通过和Callers配合来使用
  • FuncForPC 通过reflect的ValueOf().Pointer作为入参,获取函数地址、文件行、函数名等信息
  • Stack  获取栈信息
func main() {
	Foo()
}

func Foo() {
	fmt.Printf("我是 %s, %s 在调用我!\n", printMyName(), printCallerName())
	Bar()
}
func Bar() {
	fmt.Printf("我是 %s, %s 又在调用我!\n", printMyName(), printCallerName())
}
func printMyName() string {
	pc, _, _, _ := runtime.Caller(1)
	return runtime.FuncForPC(pc).Name()
}
func printCallerName() string {
	pc, _, _, _ := runtime.Caller(2)
	return runtime.FuncForPC(pc).Name()
}

func Caller(skip int) (pc uintptr, file string, line int, ok bool)

Caller可以返回函数调用栈的某一层的程序计数器、文件信息、行号。

0 代表当前函数,也是调用runtime.Caller的函数。1 代表上一层调用者,以此类推。

func Callers(skip int, pc []uintptr) int

Callers用来返回调用站的程序计数器, 放到一个uintptr中。

0 代表 Callers 本身,这和上面的Caller的参数的意义不一样,历史原因造成的。 1 才对应这上面的 0。

比如在上面的例子中增加一个trace函数,被函数Bar调用。

func Bar() {
	fmt.Printf("我是 %s, %s 又在调用我!\n", printMyName(), printCallerName())
	trace()
}
func trace() {
	pc := make([]uintptr, 10) // at least 1 entry needed
	n := runtime.Callers(0, pc)
	for i := 0; i < n; i++ {
		f := runtime.FuncForPC(pc[i])
		file, line := f.FileLine(pc[i])
		fmt.Printf("%s:%d %s\n", file, line, f.Name())
	}
}

func CallersFrames(callers []uintptr) *Frames

上面的Callers只是或者栈的程序计数器,如果想获得整个栈的信息,可以使用CallersFrames函数,省去遍历调用FuncForPC

上面的trace函数可以更改为下面的方式:

func trace2() {
	pc := make([]uintptr, 10) // at least 1 entry needed
	n := runtime.Callers(0, pc)
	frames := runtime.CallersFrames(pc[:n])
	for {
		frame, more := frames.Next()
		fmt.Printf("%s:%d %s\n", frame.File, frame.Line, frame.Function)
		if !more {
			break
		}
	}
}

func FuncForPC(pc uintptr) *Func

FuncForPC 是一个有趣的函数, 它可以把程序计数器地址对应的函数的信息获取出来。如果因为内联程序计数器对应多个函数,它返回最外面的函数。

它的返回值是一个*Func类型的值,通过*Func可以获得函数地址、文件行、函数名等信息。

除了上面获取程序计数器的方式,也可以通过反射的方式获取函数的地址:

runtime.FuncForPC(reflect.ValueOf(foo).Pointer()).Name()

获取程序堆栈

在程序panic的时候,一般会自动把堆栈打出来,如果你想在程序中获取堆栈信息,可以通过debug.PrintStack()打印出来。比如你在程序中遇到一个Error,但是不期望程序panic,只是想把堆栈信息打印出来以便跟踪调试,你可以使用debug.PrintStack()

抑或,你自己读取堆栈信息,自己处理和打印:

func DumpStacks() {
	buf := make([]byte, 16384)
	buf = buf[:runtime.Stack(buf, true)]
	fmt.Printf("=== BEGIN goroutine stack dump ===\n%s\n=== END goroutine stack dump ===", buf)
}

反射

通过对象获取当前对象对应类文件的各个属性和对应值。Go 语言中反射的应用非常广:IDE 中的代码自动补全功能、对象序列化(encoding/json)、fmt 相关函数的实现、ORM(全称是:Object Relational Mapping,对象关系映射)

使用反射的常见场景有以下两种:

  1. 不能明确接口调用哪个函数,需要根据传入的参数在运行时决定。
  2. 不能明确传入函数的参数类型,需要在运行时处理任意对象。

不推荐使用反射的理由有哪些?

  1. 与反射相关的代码,经常是难以阅读的。在软件工程中,代码可读性也是一个非常重要的指标。
  2. Go 语言作为一门静态语言,编码过程中,编译器能提前发现一些类型错误,但是对于反射代码是无能为力的。所以包含反射相关的代码,很可能会运行很久,才会出错,这时候经常是直接 panic,可能会造成严重的后果。
  3. 反射对性能影响还是比较大的,比正常代码运行速度慢一到两个数量级。所以,对于一个项目中处于运行效率关键位置的代码,尽量避免使用反射特性。

任何方法都是有利有弊,通过反射的方法可以实现一个接口调用多个不同的函数,但是使用反射某种程度上也是牺牲了部分性能。

type Test struct{}

func (t *Test) PrintInfo(i int, s string) string {
	fmt.Println("call method PrintInfo i", i, ",s :", s)
	return s + strconv.Itoa(i)
}

func (t *Test) ShowMsg() string {
	fmt.Println("\nshow msg input 'call reflect'")
	return "ShowMsg"
}

func callReflect(any interface{}, name string, args ...interface{}) []reflect.Value {
	inputs := make([]reflect.Value, len(args))
	for i, _ := range args {
		inputs[i] = reflect.ValueOf(args[i])
	}

	if v := reflect.ValueOf(any).MethodByName(name); v.String() == "<invalid Value>" {
		return nil
	} else {
		return v.Call(inputs)
	}

}

func reflectJsonMethod(fn interface{}, args ...interface{}) {
	fnVal := reflect.ValueOf(fn)
	fnType := fnVal.Type()

	if len(args) != fnType.NumIn() {
		panic("")
	}

	argsVal := []reflect.Value{}
	for _, arg := range args {
		data, _ := json.Marshal(arg)
		argsVal = append(argsVal, reflect.ValueOf(data))
	}

	fnVal.Call(argsVal)

}

func main() {
	fmt.Printf("\n callReflectMethod PrintInfo :%s", callReflect(&Test{}, "PrintInfo", 10, "TestMethod")[0].String())
	fmt.Printf("\n callReflectMethod ShowMsg  %s", callReflect(&Test{}, "ShowMsg")[0].String())

	//<invalid Value> case
	callReflect(&Test{}, "ShowMs")
	if result := callReflect(&Test{}, "ShowMs"); result != nil {
		fmt.Printf("\n callReflectMethod ShowMs %s", result[0].String())
	} else {
		fmt.Println("\n callReflectMethod ShowMs didn't run ")
	}
	fmt.Println("\n reflect all ")
}
  • 判断类型是否一样

reflect.TypeOf(a).Kind() == reflect.TypeOf(b).Kind()

  • 判断两个interface{}是否相等

reflect.DeepEqual(a, b interface{})

  • 将一个interface{}赋值给另一个interface{}

reflect.ValueOf(a).Elem().Set(reflect.ValueOf(b))

Go语言反射获取类型属性和方法示例

如果变量是一个结构体,我们还可以通过结构体域类型对象 reflect.StructField 来获取结构体下字段的类型属性。Type 接口下提供了不少用于获取字段结构体域类型对象的方法,我们主要介绍以下几个接口:

// 获取一个结构体内的字段数量
NumField() int
// 根据 index 获取结构体内的成员字段类型对象
Field(i int) StructField
// 根据字段名获取结构体内的成员字段类型对象
FieldByName(name string) (StructField, bool)

StructField 中提供了 Type 用于获取字段的的类型信息,而 StructTag 一般用来描述结构体成员字段的额外信息,比如在 JSON 进行序列化和对象映射时会被使用

func main() {
	typeOfHero := reflect.TypeOf(Hero{})
	// 通过 #NumField 获取结构体字段的数量
	for i := 0; i < typeOfHero.NumField(); i++ {
		fmt.Printf("field' name is %s, type is %s, kind is %s\n", typeOfHero.Field(i).Name, typeOfHero.Field(i).Type, typeOfHero.Field(i).Type.Kind())
	}
	// 获取名称为 Name 的成员字段类型对象
	nameField, _ := typeOfHero.FieldByName("Name")
	fmt.Printf("field' name is %s, type is %s, kind is %s\n", nameField.Name, nameField.Type, nameField.Type.Kind())
}

预期的结果如下所示:

field' name is Name, type is string, kind is string
field' name is Age, type is int, kind is int
field' name is Speed, type is int, kind is int
field' name is Name, type is string, kind is string

Type 还提供方法获取接口下方法的方法类型对象 Method,接口方法描述如下:

// 根据 index 查找方法
Method(int) Method
// 根据方法名查找方法
MethodByName(string) (Method, bool)
// 获取类型中公开的方法数量
NumMethod() int

我们可以通过 Type 中提供的方法获取接口 Person 中方法的方法类型对象,代码如下所示:

func main() {
	// 声明一个 Person 接口,并用 Hero 作为接收器
	var person Person = &Hero{}
	// 获取接口Person的类型对象
	typeOfPerson := reflect.TypeOf(person)
	// 打印Person的方法类型和名称
	for i := 0; i < typeOfPerson.NumMethod(); i++ {
		fmt.Printf("method is %s, type is %s, kind is %s.\n", typeOfPerson.Method(i).Name, typeOfPerson.Method(i).Type, typeOfPerson.Method(i).Type.Kind())
	}
	method, _ := typeOfPerson.MethodByName("Run")
	fmt.Printf("method is %s, type is %s, kind is %s.\n", method.Name, method.Type, method.Type.Kind())
}

预期的输出结果如下所示:

method is Run, type is func(*main.Hero), kind is func
method is SayHello, type is func(*main.Hero, string), kind is func
method is Run, type is func(*main.Hero) string, kind is func.

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值