Featured image of post 分析 GoFrame 是怎么动态加载配置文件和 runtime.Caller 的调用

分析 GoFrame 是怎么动态加载配置文件和 runtime.Caller 的调用

编译型语言对于目录的引用并不像脚本型语言一样方便,来看一下 GoFrame 是怎么引用的

首先按照官网的原话

默认目录配置
gcfg 配置管理对象初始化时,默认会自动添加以下配置文件搜索目录:
当前工作目录及其下的 config 目录:例如当前的工作目录为 /home/www 时,将会添加 /home/www 及 /home/www/config;
当前可执行文件所在目录及其下的 config 目录:例如二进制文件所在目录为 /tmp 时,将会添加 /tmp 及 /tmp/config;
当前 main 源代码包所在目录及其下的 config 目录 (仅对源码开发环境有效):例如 main 包所在目录为 /home/john/workspace/gf-app 时,将会添加 /home/john/workspace/gf-app 及 /home/john/workspace/gf-app/config


开发环境下无论我把二进制文件如何移动都是可以找到正确的配置文件(通过main包位置), 之前因为想写过类似的功能, 所以查看了一下gf实现的源码

直接跳到方法gcfg.New方法有一段这样的实现

func New(file ...string) *Config {
    name := DefaultConfigFile
    if len(file) > 0 {
        name = file[0]
    } else {
        // Custom default configuration file name from command line or environment.
        if customFile := gcmd.GetOptWithEnv(commandEnvKeyForFile).String(); customFile != "" {
            name = customFile
        }
    }
    c := &Config{
        defaultName: name,
        searchPaths: garray.NewStrArray(true),
        jsonMap:     gmap.NewStrAnyMap(true),
    }
    // Customized dir path from env/cmd.
    if customPath := gcmd.GetOptWithEnv(commandEnvKeyForPath).String(); customPath != "" {
        if gfile.Exists(customPath) {
            _ = c.SetPath(customPath)
        } else {
            if errorPrint() {
                glog.Errorf("[gcfg] Configuration directory path does not exist: %s", customPath)
            }
        }
    } else {
        // Dir path of working dir.
        if err := c.AddPath(gfile.Pwd()); err != nil {
            intlog.Error(context.TODO(), err)
        }

        // Dir path of main package.
        if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) {
            fmt.Println("main path:" + mainPath)
            if err := c.AddPath(mainPath); err != nil {
                intlog.Error(context.TODO(), err)
            }
        }

        // Dir path of binary.
        if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) {
            if err := c.AddPath(selfPath); err != nil {
                intlog.Error(context.TODO(), err)
            }
        }
    }
    return c
}
  • 如果没有指定配置文件路径, 那么会走// Dir path of working dir.这里的几个判断对应了文档中的工作目录,二进制文件目录和main包的目录,我们重点看一下gfile.MainPkgPath
func MainPkgPath() string {
    // It is only for source development environments.
    if goRootForFilter == "" {
        return ""
    }
    path := mainPkgPath.Val()
    if path != "" {
        return path
    }
    var lastFile string
    for i := 1; i < 10000; i++ {
        if pc, file, _, ok := runtime.Caller(i); ok {
            if goRootForFilter != "" && len(file) >= len(goRootForFilter) && file[0:len(goRootForFilter)] == goRootForFilter {
                continue
            }
            if Ext(file) != ".go" {
                continue
            }
            lastFile = file
            // Check if it is called in package initialization function,
            // in which it here cannot retrieve main package path,
            // it so just returns that can make next check.
            if fn := runtime.FuncForPC(pc); fn != nil {
                array := gstr.Split(fn.Name(), ".")
                if array[0] != "main" {
                    continue
                }
            }
            if gregex.IsMatchString(`package\s+main\s+`, GetContents(file)) {
                mainPkgPath.Set(Dir(file))
                return Dir(file)
            }
        } else {
            break
        }
    }
    // If it still cannot find the path of the package main,
    // it recursively searches the directory and its parents directory of the last go file.
    // It's usually necessary for uint testing cases of business project.
    if lastFile != "" {
        for path = Dir(lastFile); len(path) > 1 && Exists(path) && path[len(path)-1] != os.PathSeparator; {
            files, _ := ScanDir(path, "*.go")
            for _, v := range files {
                if gregex.IsMatchString(`package\s+main\s+`, GetContents(v)) {
                    mainPkgPath.Set(path)
                    return path
                }
            }
            path = Dir(path)
        }
    }
    return ""
}
  • 从当前调用函数往上10000层查询调用的文件
  • 从文件中找到调用函数是main方法的文件
  • 从当前文件中判断包名是package main,找到此文件的路径
  • 如果此文件存在, 然后返回该文件的路径

  • 至于获取当前源码所在文件路径,行号这些很多语言都提供这个功能, 比如PHPC语言中的__FILE__, 而Go中通过runtime.Caller获取, 参数是``则获取当前,1是上层,以此类推
  • C语言中, 获取源码文件名、行号、函数,这些宏会在编译的时候替换为所在源码位置中的文件名等信息
  • Go不同的是在运行时, 这些信息都由runtime管理, 引用官方的原话:

Package runtime contains operations that interact with Gos runtime system, such as functions to control goroutines. It also includes the low-level type information used by the reflect package; see reflects documentation for the programmable interface to the run-time type system.


  • 我们来看一下runtime.Caller
func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
    rpc := make([]uintptr, 1)
    n := callers(skip+1, rpc[:])
    if n < 1 {
        return
    }
    frame, _ := CallersFrames(rpc).Next()
    return frame.PC, frame.File, frame.Line, frame.PC != 0
}
  • 首先调用了runtime.callers函数, 这个函数其实就是内部的runtime.Callers函数, 看官方解释: 历史原因造成的。 1 才对应这runtime.Caller的 0。所以是skip+1
  • 然后会把函数调用的程序计数器(pc)填充到rpc切片中
  • 接下来跳到核心runtime.Frames.Next方法
func (ci *Frames) Next() (frame Frame, more bool) {
    for len(ci.frames) < 2 {
        // Find the next frame.
        // We need to look for 2 frames so we know what
        // to return for the "more" result.
        if len(ci.callers) == 0 {
            break
        }
        pc := ci.callers[0]
        ci.callers = ci.callers[1:]
        funcInfo := findfunc(pc)
        if !funcInfo.valid() {
            if cgoSymbolizer != nil {
                // Pre-expand cgo frames. We could do this
                // incrementally, too, but there's no way to
                // avoid allocation in this case anyway.
                ci.frames = append(ci.frames, expandCgoFrames(pc)...)
            }
            continue
        }
        f := funcInfo._Func()
        entry := f.Entry()
        if pc > entry {
            // We store the pc of the start of the instruction following
            // the instruction in question (the call or the inline mark).
            // This is done for historical reasons, and to make FuncForPC
            // work correctly for entries in the result of runtime.Callers.
            pc--
        }
        name := funcname(funcInfo)
        if inldata := funcdata(funcInfo, _FUNCDATA_InlTree); inldata != nil {
            inltree := (*[1 << 20]inlinedCall)(inldata)
            // Non-strict as cgoTraceback may have added bogus PCs
            // with a valid funcInfo but invalid PCDATA.
            ix := pcdatavalue1(funcInfo, _PCDATA_InlTreeIndex, pc, nil, false)
            if ix >= 0 {
                // Note: entry is not modified. It always refers to a real frame, not an inlined one.
                f = nil
                name = funcnameFromNameoff(funcInfo, inltree[ix].func_)
                // File/line is already correct.
                // TODO: remove file/line from InlinedCall?
            }
        }
        ci.frames = append(ci.frames, Frame{
            PC:       pc,
            Func:     f,
            Function: name,
            Entry:    entry,
            funcInfo: funcInfo,
            // Note: File,Line set below
        })
    }

    // Pop one frame from the frame list. Keep the rest.
    // Avoid allocation in the common case, which is 1 or 2 frames.
    switch len(ci.frames) {
    case 0: // In the rare case when there are no frames at all, we return Frame{}.
        return
    case 1:
        frame = ci.frames[0]
        ci.frames = ci.frameStore[:0]
    case 2:
        frame = ci.frames[0]
        ci.frameStore[0] = ci.frames[1]
        ci.frames = ci.frameStore[:1]
    default:
        frame = ci.frames[0]
        ci.frames = ci.frames[1:]
    }
    more = len(ci.frames) > 0
    if frame.funcInfo.valid() {
        // Compute file/line just before we need to return it,
        // as it can be expensive. This avoids computing file/line
        // for the Frame we find but don't return. See issue 32093.
        file, line := funcline1(frame.funcInfo, frame.PC, false)
        frame.File, frame.Line = file, int(line)
    }
    return
}
  • 这个方法就是通过切片里的程序计数器, 然后调用runtime.findfunc获取到函数信息, 这个函数返回了一个runtime.funcInfo结构体
type funcInfo struct {
    *_func
    datap *moduledata
}
  • 对于moduledata,源码中有一段这样的注释

    // moduledata records information about the layout of the executable
    // image. It is written by the linker. Any changes here must be
    // matched changes to the code in cmd/internal/ld/symtab.go:symtab.
    // moduledata is stored in statically allocated non-pointer memory;
    // none of the pointers here are visible to the garbage collector.


  • 这个moduledata记录了可执行文件的源码文件信息
  • 大概意思就是是由链接器写入的, 所以我们确定了符号表写入的时机
  • 关于链接器写入的时机, 这里贴上一张图

  • 执行go build -n可查看整个过程

贴图来自

Go 编译链接过程概述