According to the official documentation:
Default Directory Configuration
When initializing the gcfg configuration management object, it automatically adds the following configuration file search directories by default:
- Current working directory and its config subdirectory: e.g., /home/www and /home/www/config when working in /home/www
- Executable file directory and its config subdirectory: e.g., /tmp and /tmp/config for a binary in /tmp
- Main package directory and its config subdirectory (only effective in source development): e.g., /home/john/workspace/gf-app and /home/john/workspace/gf-app/config for a main package in that directory
During development, the configuration file can always be found correctly through the main package location regardless of binary relocation. Let’s examine the implementation in the GoFrame source code.
Looking at the gcfg.New
method:
func New(file ...string) *Config {
// ... [initialization logic]
// Customized dir path from env/cmd handling
if customPath := gcmd.GetOptWithEnv(commandEnvKeyForPath).String(); customPath != "" {
// ... [custom path logic]
} else {
// Default directory processing
// 1. Working directory
if err := c.AddPath(gfile.Pwd()); err != nil {
intlog.Error(context.TODO(), err)
}
// 2. Main package directory (development environment)
if mainPath := gfile.MainPkgPath(); mainPath != "" {
if err := c.AddPath(mainPath); err != nil {
intlog.Error(context.TODO(), err)
}
}
// 3. Executable directory
if selfPath := gfile.SelfDir(); selfPath != "" {
if err := c.AddPath(selfPath); err != nil {
intlog.Error(context.TODO(), err)
}
}
}
return c
}
The key method gfile.MainPkgPath()
works as follows:
func MainPkgPath() string {
// ... [initial checks]
// Iterate through call stack
for i := 1; i < 10000; i++ {
pc, file, _, ok := runtime.Caller(i)
if !ok {
break
}
// Filter Go standard library paths
if isStandardLibraryFile(file) {
continue
}
// Check if it's a main package file
if strings.Contains(getFileContent(file), "package main") {
return filepath.Dir(file)
}
}
// Fallback directory search
// ... [directory traversal logic]
}
Key implementation points:
- Call Stack Traversal: Uses
runtime.Caller
to traverse up to 10,000 call frames - Main Package Identification: Checks files for
package main
declaration - Path Validation: Verifies path existence and proper directory structure
How runtime.Caller
Works Internally
The Go runtime maintains execution context through:
// Simplified runtime.Caller implementation
func Caller(skip int) (pc uintptr, file string, line int, ok bool) {
rpc := make([]uintptr, 1)
n := callers(skip+1, rpc[:]) // Adjust for wrapper frames
frame, _ := CallersFrames(rpc).Next()
return frame.PC, frame.File, frame.Line, true
}
Key components:
- Program Counter (PC): Unique identifier for execution position
- Symbol Table: Built during linking (
go build
phase) - Module Data: Stores debug information and source mapping
Practical Implications
-
Development vs Production
- Development: Reliable main package detection through source analysis
- Production: Falls back to executable directory detection
-
Performance Considerations
- Call stack traversal has O(n) complexity but optimized for typical cases
- Path caching (mainPkgPath.Val()) prevents repeated computation
-
Edge Case Handling
- Recursive directory search when direct detection fails
- Special handling for cgo and inline functions
This implementation demonstrates how GoFrame combines runtime capabilities with file system analysis to create a robust configuration loading mechanism that works across different environments.