Go语言中是否存在快速唯一标识函数调用方的实现方案?
解决方案
该需求完全可以通过Go原生能力实现,无需每次调用日志都承担runtime.Caller()的完整开销,有以下几种可行方案:
方案1:使用runtime.GetCallerPC()(推荐,Go 1.18及以上支持)
runtime.GetCallerPC(skip int)是Go 1.18新增的接口,仅返回调用方的程序计数器(PC),不会执行解析文件名、行号的额外操作,单次调用耗时仅30~50ns,远低于runtime.Caller()的500ns开销。你可以将PC作为缓存key,仅在第一次命中缓存失败时调用runtime.Caller()获取文件、行号信息存入缓存即可:
package main import ( "fmt" "runtime" "sync" ) type Info struct { file string line int // otherData } // 用sync.Map保证并发安全,也可以用sync.RWMutex+普通map var infoMap sync.Map func Log(str string, args ...interface{}) { pc := runtime.GetCallerPC(1) infoVal, found := infoMap.Load(pc) if !found { _, file, line, _ := runtime.Caller(1) info := Info{file: file, line: line} // 其他自定义数据生成逻辑写在这里 infoMap.Store(pc, info) infoVal = info } info := infoVal.(Info) fmt.Println(info.file, info.line, fmt.Sprintf(str, args...)) } // client code func foo() { // do some work Log("Here's a message with <> some <> args", "foo", "bar") // do more work Log("The cow jumped over the moon") } func main() { foo() foo() }
该方案完全兼容你现有的调用方式,无需修改业务侧代码,性能提升明显。
方案2:低版本Go兼容方案
如果你的Go版本低于1.18,可以用runtime.Callers()仅提取PC,性能也远高于runtime.Caller():
func getCallerPC(skip int) uintptr { var pcs [1]uintptr // skip层数需要加上当前getCallerPC本身的一层 runtime.Callers(skip+1, pcs[:]) return pcs[0] }
将Log函数中获取PC的逻辑替换为调用getCallerPC(1)即可,其余逻辑和方案1一致。
方案3:极致性能优化方案
如果需要完全消除运行期的PC获取开销,可以在调用侧预初始化日志实例,每个日志点仅初始化一次:
func NewLogger() func(string, ...interface{}) { pc := runtime.GetCallerPC(1) _, file, line, _ := runtime.Caller(1) info := Info{file: file, line: line} // 其他自定义数据生成逻辑 return func(str string, args ...interface{}) { fmt.Println(info.file, info.line, fmt.Sprintf(str, args...)) } } // 业务侧使用 func foo() { log1 := NewLogger() // 仅第一次调用时执行runtime操作 log1("第一条日志") log2 := NewLogger() log2("第二条日志") }
该方案每次日志调用完全没有runtime相关开销,性能最高,仅需要业务侧做少量调整。
内容的提问来源于stack exchange,提问作者Locke
相关产品推荐
相关产品推荐

