runtime.Caller()
需求:
想知道某个rpc方法在哪里被调用(获取代码调用栈的位置信息)
思路:
想起平常使用 zap 包的 Error 方法时,会打印堆栈的信息,说明 Error 方法源码里面肯定有获取代码调用栈信息的方式。
func (log *Logger) Error(msg string, fields ...Field) {
if ce := log.check(ErrorLevel, msg); ce != nil {
ce.Write(fields...)
}
}
// 看 check 方法
func (log *Logger) check(lvl zapcore.Level, msg string) *zapcore.CheckedEntry {
// ...
// Adding the caller or stack trace requires capturing the callers of
// this function. We'll share information between these two.
// 译文:添加调用者或堆栈跟踪需要捕获此函数的调用者。我们将在这两者之间共享信息。
stackDepth := stacktraceFirst
if addStack {
stackDepth = stacktraceFull
}
stack := captureStacktrace(log.callerSkip+callerSkipOffset, stackDepth)// <--
defer stack.Free()
// ...
}
// 在captureStacktrace方法中,使用了 runtime 包的 Callers 方法(还有 Caller 方法供我们使用)
// captureStacktrace captures a stack trace of the specified depth, skipping
// the provided number of frames. skip=0 identifies the caller of
// captureStacktrace.
// 译文:captureStacktrace捕获指定深度的堆栈跟踪,跳过提供的帧数skip;
// skip = 0 表示captureStacktrace的调用者。
// The caller must call Free on the returned stacktrace after using it.
func captureStacktrace(skip int, depth stacktraceDepth) *stacktrace {
stack := _stacktracePool.Get().(*stacktrace)// 涉及了池化管理
// 这里根据 depth 参数的值来决定如何分配存储空间。如果深度为 stacktraceFirst,则只保留一个栈帧的空间;
// 如果深度为 stacktraceFull,则使用整个 stack.storage。
switch depth {
case stacktraceFirst: // const stacktraceFirst stacktraceDepth = iota
stack.pcs = stack.storage[:1]
case stacktraceFull: // const stacktraceFull stacktraceDepth
stack.pcs = stack.storage
}
// Unlike other "skip"-based APIs, skip=0 identifies runtime.Callers
// itself. +2 to skip captureStacktrace and runtime.Callers.
numFrames := runtime.Callers(// <--
skip+2,// 假设skip=0,则直接跳过了runtime.Callers(0层)和captureStacktrace(1层),
// 所以捕获到的栈帧是从调check方法开始的
stack.pcs,
)
// runtime.Callers truncates the recorded stacktrace if there is no
// room in the provided slice. For the full stack trace, keep expanding
// storage until there are fewer frames than there is room.
// 如果深度为 stacktraceFull,则会检查捕获到的栈帧数量是否与存储空间大小相同,如果相同则会扩展存储空间,直到足够放下所有的栈帧信息。
// 如果深度不是 stacktraceFull,则只保留捕获到的栈帧数。
if depth == stacktraceFull {
pcs := stack.pcs
for numFrames == len(pcs) {// 说明满了
pcs = make([]uintptr, len(pcs)*2)
numFrames = runtime.Callers(skip+2, pcs)
}
// Discard old storage instead of returning it to the pool.
// This will adjust the pool size over time if stack traces are
// consistently very deep.
stack.storage = pcs
stack.pcs = pcs[:numFrames]// 保留numFrames个帧
} else {
stack.pcs = stack.pcs[:numFrames]
}
stack.frames = runtime.CallersFrames(stack.pcs)
return stack
}
结果:
- 通过 runtime.Caller()获取当前代码的文件路径、行数
runtime.Caller(1)
表示获取调用栈的上一层,即调用runtime.Caller
函数的位置的信息。runtime.Caller(0)
,那么就是获取调用runtime.Caller
函数的位置的信息。
仅供参考:我们的rpc项目中,impl.xxxclient.RPCfunc()【我们想捕获的】 -> getxxxclient()【可忽略】 -> xxxclient.RPCfunc()【在此处使用 runtime.Caller】-> grpc Invoke
示例:
在每个rpc 调用xxxclient.RPCfunc()内,invoke 前去调用runtime.Caller(1),就可以捕获到impl.xxxclient.RPCfunc()的栈帧。
这个是 protoc-gen-grpc-go 插件生成的rpc方法代码
func (c *ServiceClient) RPCfunc(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) {
// 打印日志
_, file, line, _ := runtime.Caller(1)// <-- 捕获到impl.xxxclient.RPCfunc()的栈帧
grpclog.Infof(fmt.Sprintf("func: %s:%s"),file,line)
out := new(QueryServicePeriodResponse)
err := c.cc.Invoke(ctx, _func, in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}