I have some code mostly copied from https://github.com/go-errors/errors which generates a Stack Trace
When I run it in VSCode debugger the output is exactly as expected. File names and function names are correct.
Debug Output
C:/work/projects/go/src/errtest/customerrors.go:17 (0x4d149f)
NewDbError: dbe.stacktrace = NewStackTrace()
C:/work/projects/go/src/errtest/errortest04a.go:6 (0x4d156f)
errorTest04a: err := NewDbError("This is a db error")
C:/work/projects/go/src/errtest/errortest04.go:4 (0x4d1507)
errorTest04: errorTest04a()
C:/work/projects/go/src/errtest/main.go:4 (0x4d16b7)
main: errorTest04()
C:/Go/src/runtime/proc.go:200 (0x431bc1)
main: fn()
C:/Go/src/runtime/asm_amd64.s:1337 (0x458bc1)
goexit: BYTE $0x90 // NOP
When I build and run the same code however the output is not correct.
Build Output
C:/work/projects/go/src/errtest/customerrors.go:17 (0x4a5dbe)
errorTest04a: dbe.stacktrace = NewStackTrace()
C:/work/projects/go/src/errtest/customerrors.go:17 (0x4a5db9)
NewDbError: dbe.stacktrace = NewStackTrace()
C:/work/projects/go/src/errtest/errortest04.go:4 (0x4a5e88)
main: errorTest04a()
C:/work/projects/go/src/errtest/errortest04.go:4 (0x4a5e83)
errorTest04: errorTest04a()
C:/Go/src/runtime/proc.go:200 (0x42d5ac)
main: fn()
C:/Go/src/runtime/asm_amd64.s:1337 (0x452911)
goexit: BYTE $0x90 // NOP
Question:
Why does the code generate different output depending on whether run in Debug or build?
NewDbErrordbe.stacktrace = NewStackTrace()customerrors.go:17
errorTest04abut in the file
The second line in the Build output shows what appears as the first line in the Debug output
main
It seems pretty weird that the same code generates different output.
End Question
I was thinking perhaps the compile removed file names but they are there, just in an incorrect order and with the function names out of order too.
If anyone can shed some light it would be appreciated. Thanks
Here is the code I have used
main.go
package main
func main() {
errorTest04()
}
errortest04.go
package main
func errorTest04() {
errorTest04a()
}
errortest04a.go package main
import "fmt"
func errorTest04a() {
err := NewDbError("This is a db error")
fmt.Print(string(err.stacktrace.Stack()))
}
customerorrs.go
package main
// DbError is a custom error that holds a dberror
type DbError struct {
stacktrace *StackTrace
errmsg string
}
func (e *DbError) Error() string {
return e.errmsg
}
// NewDbError returns a new DbError
func NewDbError(e string) DbError {
dbe := DbError{errmsg: e}
dbe.stacktrace = NewStackTrace()
return dbe
}
package main
import (
"bytes"
"fmt"
"io/ioutil"
"runtime"
"strings"
)
const maxStackDepth = 50
// StackTrace holds the stack data
type StackTrace struct {
callers []uintptr
callerslen int
frames []StackFrame
}
// Stack - returns a formatted Stack Trace
func (st *StackTrace) Stack() []byte {
buf := bytes.Buffer{}
for _, frame := range st.frames {
buf.WriteString(frame.String())
}
return buf.Bytes()
}
// NewStackTrace creates a new stack object
func NewStackTrace() *StackTrace {
st := StackTrace{}
st.callers = make([]uintptr, maxStackDepth)
st.callerslen = runtime.Callers(2, st.callers[:])
st.frames = make([]StackFrame, st.callerslen)
for i := range st.frames {
st.frames[i] = newStackFrame(st.callers[i])
}
return &st
}
// StackFrame - details of a caller. Lifted from https://github.com/go-errors/errors/blob/master/stackframe.go
type StackFrame struct {
// The path to the file containing this ProgramCounter
File string
// The LineNumber in that file
LineNumber int
// The Name of the function that contains this ProgramCounter
FnName string
// The Package that contains this function
Package string
// The underlying ProgramCounter
ProgramCounter uintptr
}
func newStackFrame(pc uintptr) StackFrame {
frame := StackFrame{ProgramCounter: pc}
if frame.Func() == nil {
return frame
}
frame.Package, frame.FnName = packageAndName(frame.Func())
// pc -1 because the program counters we use are usually return addresses,
// and we want to show the line that corresponds to the function call
// frame.File, frame.LineNumber = frame.Func().FileLine(pc)
frame.File, frame.LineNumber = frame.Func().FileLine(pc - 1)
return frame
}
// Func returns the function that contained this frame.
func (frame *StackFrame) Func() *runtime.Func {
if frame.ProgramCounter == 0 {
return nil
}
return runtime.FuncForPC(frame.ProgramCounter)
}
// SourceLine gets the line of code (from File and Line) of the original source if possible.
func (frame *StackFrame) SourceLine() (string, error) {
data, err := ioutil.ReadFile(frame.File)
// _ replace with err
if err != nil {
return "", err
}
lines := bytes.Split(data, []byte{'
'})
if frame.LineNumber <= 0 || frame.LineNumber >= len(lines) {
return "???", nil
}
// -1 because line-numbers are 1 based, but our array is 0 based
return string(bytes.Trim(lines[frame.LineNumber-1], " \t")), nil
}
// String returns the stackframe formatted in the same way as go does
// in runtime/debug.Stack()
func (frame *StackFrame) String() string {
str := fmt.Sprintf("%s:%d (0x%x)
", frame.File, frame.LineNumber, frame.ProgramCounter)
source, err := frame.SourceLine()
if err != nil {
return str
}
return str + fmt.Sprintf("\t%s: %s
", frame.FnName, source)
}
func packageAndName(fn *runtime.Func) (string, string) {
name := fn.Name()
pkg := ""
// The name includes the path name to the package, which is unnecessary
// since the file name is already included. Plus, it has center dots.
// That is, we see
// runtime/debug.*T·ptrmethod
// and want
// *T.ptrmethod
// Since the package path might contains dots (e.g. code.google.com/...),
// we first remove the path prefix if there is one.
if lastslash := strings.LastIndex(name, "/"); lastslash >= 0 {
pkg += name[:lastslash] + "/"
name = name[lastslash+1:]
}
if period := strings.Index(name, "."); period >= 0 {
pkg += name[:period]
name = name[period+1:]
}
name = strings.Replace(name, "·", ".", -1)
return pkg, name
}