For reasons outside the scope of this entry, I recently dug into how the Go runtime did (Unix) signal handling on 64-bit x86 Linux . When I undertook this quest, I decided that the easiest way to navigate through the code of the Go runtime was to use the code navigation features exposed by the standard Go language server, gopls . In the process I was surprised by just how good its code navigation was, even in the Go runtime.
On Linux, Go's signal handling talks directly to the Linux kernel rather than going through the C library. As you can imagine, this is relatively architecture and Linux specific, as well as being relatively specific to Unix. The result is a tangle of OS and architecture specific code in a variety of signal related files in src/runtime . The first challenge for code navigation is picking out the right ones that apply to the environment you're interested with; in Go this is handled through build tags, which gopls understand. So gopls had no problem navigating from general Unix signal handling to setsig() in Linux-specific code and the 64-bit x86 Linux definition of the struct involved .
But that was only half the puzzle, because I was looking into how the Go runtime receives signals. This is done by '
sigtramp()
' and the related function
sigreturn__sigaction()
, and it turns out that these functions are not defined in Go. All you'll find in Go is stubs of them at the start of os_linux.go . But gopls had no problems navigating from the stubs to the actual amd64 assembly version , despite the fact that the assembly version has an odd name, and then it was able to navigate from the assembly version of '
sigtramp()
' back to the Go '
sigtrampgo()
' .
(It turns out that one area where gopls is currently limited for Go assembly language is finding references for assembly language symbols. Fortunately I didn't need that here, all I needed was 'find definition'.)
Code navigation among Go code is not surprising, because that's what you expect from a (Go) language server like gopls . What surprised and impressed me is code navigation into and out of Go assembly code, where I was expecting to have to resort to manual searches with (rip)grep. This is almost certainly a relatively niche feature, yet gopls has basic support for it. This support doesn't come from the standard Go library; instead it's implemented specifically in gopls in internal/asm and internal/goasm .
Another nice trick that gopls can do (that I just investigated) is navigate from an interface or an interface method to everything in your codebase that implements the interface. This is done through (of course) the LSP 'find implementation' code navigation action (in Eglot in GNU Emacs, this is 'C-c i'). Gopls will also navigate backward from a concrete thing to all of the (in-scope) interfaces that it implements (again using 'find implementation'). Slightly inconveniently, if your thing has a String() method, this will report a number of interfaces in the Go standard library. Gopls currently includes non-exported interfaces in the standard library, which is technically correct but extra not useful.
(Specifically, currently this will include context.stringer and runtime.stringer, as well as fmt.Stringer , the public version (and expvar.Var , which has the same shape but incompatible return value requirements ). I assume the Go runtime and standard library has multiple versions of this interface internally to limit cross-imports.)