Description
Go version
go version go1.24.4 darwin/amd64
Output of go env
in your module/workspace:
AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/Users/beaubrueggemann/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/beaubrueggemann/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/47/c0lsmsxn3_z86w657l519d6m0000gn/T/go-build1916904101=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='amd64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/Users/beaubrueggemann/go/src/cgobug/go.mod'
GOMODCACHE='/Users/beaubrueggemann/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/beaubrueggemann/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/beaubrueggemann/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_amd64'
GOVCS=''
GOVERSION='go1.24.4'
GOWORK=''
PKG_CONFIG='pkg-config'
What did you do?
Here's the most trivial/simple recipe I could come up with.
Create a cgobug
module with three simple packages. Here is the directory structure:
cgobug/
│
├── go.mod
│
├── cgopkg/
│ └── cgopkg.go
│
├── other/
│ └── other.go
│
└── pkginfo/
└── pkginfo.go
The go.mod
file was created with a go mod init cgobug
command, followed by go get golang.org/x/tools/go/packages
, which produces:
module cgobug
go 1.23.0
toolchain go1.24.4
require (
golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/tools v0.34.0 // indirect
)
Package cgopkg
is a simple package that uses cgo (the initial example at the top of the C? Go? Cgo! blog post). It is defined in the file cgopkg.go
:
package cgopkg
// #include <stdlib.h>
import "C"
func Random() int {
return int(C.random())
}
func Seed(i int) {
C.srandom(C.uint(i))
}
Package other
is a simple package that uses package cgopkg
. It is defined in the file other.go
:
package other
import "cgobug/cgopkg"
func init() {
cgopkg.Seed(1)
}
func OtherRandom() int {
return cgopkg.Random()
}
And finally there is command pkginfo
, which uses golang.org/x/tools/go/packages
to gather identifier information about package other
. It then prints the locations of the definitions of any identifiers that are used in other
, but are defined in cgopkg
. It is implemented in the file pkginfo.go
:
package main
import (
"fmt"
"golang.org/x/tools/go/packages"
)
// Note: This command must be run from its source directory
// (so that the "../other" package path works below).
func main() {
// Load type information for "other" package.
cfg := packages.Config{Mode: packages.LoadSyntax}
pkgs, err := packages.Load(&cfg, "../other")
if err != nil {
fmt.Println(err)
return
}
// Print locations of identifier definitions
// from package "cgopkg" that are used by package "other".
for _, pkg := range pkgs {
fmt.Printf("Package %q uses:\n", pkg.ID)
if len(pkg.Errors) > 0 {
fmt.Println(pkg.Errors)
return
}
if len(pkg.TypeErrors) > 0 {
fmt.Println(pkg.TypeErrors)
return
}
for _, obj := range pkg.TypesInfo.Uses {
pk := obj.Pkg()
if pk == nil || pk.Name() != "cgopkg" {
// Only print uses of identifiers
// from package cgopkg.
continue
}
name := obj.Name()
pos := pkg.Fset.Position(obj.Pos())
fmt.Printf("%s: %s\n", name, pos)
}
}
}
What did you see happen?
From the cgobug/pkginfo
directory (where this simple command is designed to be run from), I ran
go run .
which produced the following output:
Package "cgobug/other" uses:
Random: /Users/beaubrueggemann/go/src/cgobug/cgopkg/cgopkg.go:9:1
Seed: /Users/beaubrueggemann/go/src/cgobug/cgopkg/cgopkg.go:13:1
What did you expect to see?
I expected the line numbers to match those in cgopkg.go
, which would be lines 6
(for Random
) and 10
(for Seed
). Instead, the line numbers are three higher than they should be.
I've done many experiments, and the results are always the same: For any types.Object
found in a types.Info.Uses
map, if that Object
is from a different package (than the one the types.Info
struct is showing uses for), and if that Object
is also defined in a cgo file (that imports "C"), then the line numbers of the Object
's position will be incremented by three.
The columns are also incorrect, though I'm under the impression that that is to be expected with cgo files (though I can't remember where I read that). But I'm also under the impression that the line numbers are supposed to always be correct, even for cgo files (due to "line directives" in the cgo generated files).
Currently I'm working around this in my tool, by manually checking if the types.Object
's file is a cgo file from another package, and then manually adding 3 to the line numbers when I use them. But this doesn't feel good. And is it reliable? Can the offset ever be other than 3? So far it's always been 3 for me (on both linux and macos, with both go1.23 and go1.24), but I'm still worried the 3 might change depending on what's in the cgo file (though all the various cgo files that I've tried so far have always ended up with lines offset by precisely 3).