Go version
go version go1.25.0 linux/amd64
Output of go env
in your module/workspace:
AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/flo/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/flo/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build965605651=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/flo/tmp/go-panic-print/go.mod'
GOMODCACHE='/home/flo/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/flo/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/flo/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Reproducer:
package main
import "github.com/florianl/foo"
func main() {
foo.Bar()
}
module github.com/florianl/issue-75198
go 1.25.0
require github.com/florianl/foo v0.0.0-20250830101743-a8ea00ca555f
replace github.com/florianl/foo => github.com/florianl/bar v0.0.0-20250830103411-1f7d777651a2
What did you see happen?
When building the reproducer with -trimpath
the following stack will be printed as triggered by the panic()
:
$ go clean
$ go build -trimpath ./...
$ ./issue-75198
panic: from bar
goroutine 1 [running]:
github.com/florianl/foo.bar()
github.com/florianl/foo@v0.0.0-20250830101743-a8ea00ca555f/bar.go:9 +0x25
github.com/florianl/foo.Bar(...)
github.com/florianl/foo@v0.0.0-20250830101743-a8ea00ca555f/bar.go:4
main.main()
github.com/florianl/issue-75198/main.go:6 +0x10
Please note that the file paths in the stack frames still reference github.com/florianl/foo@v0.0.0-20250830101743-a8ea00ca555f
.
When building the reproducer without -trimpath
the correct paths are shown in the stacktrace:
$ go clean
$ go build ./...
$ ./issue-75198
panic: from bar
goroutine 1 [running]:
github.com/florianl/foo.bar()
/home/user/go/pkg/mod/github.com/florianl/bar@v0.0.0-20250830103411-1f7d777651a2/bar.go:9 +0x25
github.com/florianl/foo.Bar(...)
/home/user/go/pkg/mod/github.com/florianl/bar@v0.0.0-20250830103411-1f7d777651a2/bar.go:4
main.main()
/home/user/issue-75198/main.go:6 +0x10
What did you expect to see?
The expected stacktrace should look like this:
$ go clean
$ go build -trimpath ./...
$ ./issue-75198
panic: from bar
goroutine 1 [running]:
github.com/florianl/bar.bar()
github.com/florianl/bar@v0.0.0-20250830103411-1f7d777651a2/bar.go:9 +0x25
github.com/florianl/bar.Bar(...)
github.com/florianl/bar@v0.0.0-20250830103411-1f7d777651a2/bar.go:4
main.main()
github.com/florianl/issue-75198/main.go:6 +0x10
Comment From: gabyhelp
Related Issues
- cmd/go: stacktraces with -trimpath don't use replaced module paths #68493 (closed)
- cmd/go: -trimpath with -mod=vendor doesn't trim path #36566 (closed)
- cmd/build: -trimpath is ignored when forcefully passing -gcflags=-trimpath=$SOMETHING #66849 (closed)
- import/path: go build -trimpath no trim in root path #69512 (closed)
- cmd/go: local failure in TestScript/build_trimpath_cgo #70669 (closed)
- cmd/go: panic in mod tidy when requiring main module #46078 (closed)
- cmd/go: build_trimpath test doesn't correctly test executable reproducibility #35435 (closed)
- cmd/go: trimpath gcflag not trimming anything #30696 (closed)
- cmd/go: update -trimpath documentation #50402 (closed)
- cmd/compile: encoded pkg path shown in stack trace #35558
(Emoji vote if this was helpful or unhelpful; more detailed feedback welcome in this discussion.)
Comment From: seankhliao
replace
replaces the source files of given dependency with those of the target, but it doesn't change the identity (name+version) of the dependency.
-trimpath
uses module identity as the source location.
replace
also supports local replaces.
What should -trimpath
with replace example.com/foo => ../my-local-foo
use as the source location if we don't use module identity?
Comment From: florianl
Looks like @gabyhelp is doing a better job in searching and identifying https://github.com/golang/go/issues/68493 as possible duplicate.
replace
replaces the source files of given dependency with those of the target, but it doesn't change the identity (name+version) of the dependency.
I think, replace
works just fine. In all produced stacktraces, the private function bar()
, that exists only in the module that replaces the original, is listed correctly.
Looking at this example, where -trimpath
is used:
github.com/florianl/foo.bar()
github.com/florianl/foo@v0.0.0-20250830101743-a8ea00ca555f/bar.go:9 +0x25
the module github.com/florianl/foo
does not have a private function named bar()
, that is called by the public Bar()
function.
replace
also supports local replaces. What should-trimpath
withreplace example.com/foo => ../my-local-foo
use as the source location if we don't use module identity?
For the named local replacement, I would expect not to get example.com/foo
for the module identity. The module identity should hint towards my-local-foo
. But I don't have an opinion on wether the relative or absolute path should be used.
Comment From: zigo101
@seankhliao did you locked https://github.com/golang/go/issues/68493?
Comment From: prattmic
cc @golang/command-line
Comment From: florianl
To help investigate this issue, I did create a test script file mod_replace_trimpath.txt
that can be place in src/cmd/go/testdata/script/.
:
# Test that 'go build -trimpath' works correctly with module replacements.
# This test verifies that when using both replace statements in go.mod
# and the -trimpath flag, the built binary contains properly trimmed paths.
[short] skip
# populate go.sum
go get
# Test 1: Build without -trimpath - should contain workspace paths
go build -o without-trimpath.exe .
exec ./without-trimpath.exe
stdout 'Hello from replacement dependency'
stdout 'Main contains workspace path: true'
stdout 'Dep contains workspace path: true'
stdout 'Dep contains example.com in path: false'
# Test 2: Build with -trimpath - should not contain workspace paths
go build -trimpath -o with-trimpath.exe .
exec ./with-trimpath.exe
stdout 'Hello from replacement dependency'
stdout 'Main contains workspace path: false'
stdout 'Dep contains workspace path: false'
stdout 'Dep contains example.com in path: false'
# Test 3: Verify that the replacement is actually being used
# The message should come from the replacement, not the original
! stdout 'Hello from original dependency'
# Test 4: Verify go list shows the replacement correctly
go list -m -f '{{.Path}} {{.Version}}{{with .Replace}} => {{.Path}}{{end}}' example.com/original-dep
stdout 'example.com/original-dep v1.0.0 => ./replacement-dep'
-- go.mod --
module example.com/main
go 1.25
require example.com/original-dep v1.0.0
replace example.com/original-dep v1.0.0 => ./replacement-dep
-- main.go --
package main
import (
"fmt"
"runtime"
"strings"
"example.com/original-dep"
)
func main() {
// Print message to verify replacement is working
fmt.Println(dep.Message())
// Get the current file path to check trimpath behavior
_, file, _, _ := runtime.Caller(0)
fmt.Printf("Main file path: %s\n", file)
// Get file path from dependency to check trimpath behavior
depFile := dep.GetCallerFile()
fmt.Printf("Dep file path: %s\n", depFile)
// Check if paths contain workspace directory (should not with -trimpath)
// Look for the temporary test directory structure
workspaceInMain := strings.Contains(file, "/tmp/") || strings.Contains(file, "gopath")
workspaceInDep := strings.Contains(depFile, "/tmp/") || strings.Contains(depFile, "gopath")
exampleComInDep := strings.Contains(depFile, "example")
fmt.Printf("Main contains workspace path: %t\n", workspaceInMain)
fmt.Printf("Dep contains workspace path: %t\n", workspaceInDep)
fmt.Printf("Dep contains example.com in path: %t\n", exampleComInDep)
}
-- replacement-dep/go.mod --
module example.com/replacement-dep
go 1.21
-- replacement-dep/replacement.go --
package dep
import "runtime"
// GetCallerFile returns the file path of the caller for testing trimpath behavior
func GetCallerFile() string {
_, file, _, _ := runtime.Caller(0)
return file
}
func Message() string {
return "Hello from replacement dependency"
}
-- original-dep/go.mod --
module example.com/original-dep
go 1.21
-- original-dep/dep.go --
package dep
import "runtime"
// GetCallerFile returns the file path of the caller for testing trimpath behavior
func GetCallerFile() string {
_, file, _, _ := runtime.Caller(0)
return file
}
func Message() string {
return "Hello from original dependency"
}