首先按照官网的原话
默认目录配置
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
,找到此文件的路径 - 如果此文件存在, 然后返回该文件的路径
- 至于获取当前源码所在文件路径,行号这些很多语言都提供这个功能, 比如
PHP
和C
语言中的__FILE__
, 而Go
中通过runtime.Caller
获取, 参数是``则获取当前,1
是上层,以此类推 - 在
C
语言中, 获取源码文件名、行号、函数,这些宏会在编译的时候替换为所在源码位置中的文件名等信息 - 而
Go
不同的是在运行时, 这些信息都由runtime
管理, 引用官方的原话:
Package runtime contains operations that interact with Go
s runtime system, such as functions to control goroutines. It also includes the low-level type information used by the reflect package; see reflect
s 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
可查看整个过程