ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

go捕获代码调用栈帧-runtime.Caller()

2026/9/25 22:45:12 拓冰建站 浏览量
go捕获代码调用栈帧-runtime.Caller()

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 := stacktraceFirstif 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 = iotastack.pcs = stack.storage[:1]case stacktraceFull:  // const stacktraceFull stacktraceDepthstack.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.pcsfor 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 = pcsstack.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
}