5

I am attempting to create named loggers automatically for HTTP handlers that I'm writing, where I am passed a function (pointer).

I'm using the code mentioned in this question to get the name of a function:

package utils

import (
    "reflect"
    "runtime"
)

func GetFunctionName(fn interface{}) string {
    value := reflect.ValueOf(fn)
    ptr := value.Pointer()
    ffp := runtime.FuncForPC(ptr)

    return ffp.Name()
}

I'm using this in my main function to try it out like so:

package main

import (
    "github.com/naftulikay/golang-webapp/experiments/functionname/long"
    "github.com/naftulikay/golang-webapp/experiments/functionname/long/nested/path"
    "github.com/naftulikay/golang-webapp/experiments/functionname/utils"
    "log"
)

type Empty struct{}

func main() {
    a := long.HandlerA
    b := path.HandlerB
    c := path.HandlerC

    log.Printf("long.HandlerA: %s", utils.GetFunctionName(a))
    log.Printf("long.nested.path.HandlerB: %s", utils.GetFunctionName(b))
    log.Printf("long.nested.path.HandlerC: %s", utils.GetFunctionName(c))
}

I see output like this:

github.com/naftulikay/golang-webapp/experiments/functionname/long.HandlerA

This is okay but I'd like an output such as long.HandlerA, long.nested.path.HandlerB, etc.

If I could get the Go module name (github.com/naftulikay/golang-webapp/experiments/functionname), I can then use strings.Replace to remove the module name to arrive at long/nested/path.HandlerB, then strings.Replace to replace / with . to finally get to my desired value, which is long.nested.path.HandlerB.

The first question is: can I do better than runtime.FuncForPC(reflect.ValueOf(fn).Pointer()) for getting the qualified path to a function?

If the answer is no, is there a way to get the current Go module name using runtime or reflect so that I can transform the output of runtime.FuncForPC into what I need?

Once again, I'm getting values like:

  • github.com/naftulikay/golang-webapp/experiments/functionname/long.HandlerA
  • github.com/naftulikay/golang-webapp/experiments/functionname/long/nested/path.HandlerB
  • github.com/naftulikay/golang-webapp/experiments/functionname/long/nested/path.HandlerC

And I'd like to get values like:

  • long.HandlerA
  • long.nested.path.HandlerB
  • long.nested.path.HandlerC

EDIT: It appears that Go does not have a runtime representation of modules, and that's okay, if I can do it at compile time that would be fine too. I've seen the codegen documentation and I'm having a hard time figuring out how to write my own custom codegen that can be used from go generate.

blackgreen
  • 34,072
  • 23
  • 111
  • 129
Naftuli Kay
  • 87,710
  • 93
  • 269
  • 411
  • @gopher is there a way to do codegen at build time to parse `go.mod` and embed the result in my program? – Naftuli Kay Oct 05 '21 at 19:16
  • 1
    I'd make very certain you need to "then strings.Replace to replace / with . " because that's going to look very wrong to most Go developers. That's how languages like Java organize packages, but it is *not* how Go organizes them. Go uses slashes. So unless there's a reason you *need* these to be dot-delimited, you should really consider leaving them slash-delimited as normal. – Adrian Oct 05 '21 at 19:30
  • @Adrian I'm generating logger names for my various routes, e.g. `app.routes.login.LoginHandler`. Slashes seem to work but the mix of slashes and dots seems confusing to me. I don't need a file name or a line, just need to extract that value from a function pointer. – Naftuli Kay Oct 05 '21 at 19:42
  • 1
    That's fair, and it's your design decision, I just wanted to recommend caution as what seems confusing to you is what's normal to developers used to Go (and vice-versa). YMMV. – Adrian Oct 05 '21 at 20:30
  • @gopher There isn't? What about [`debug.ReadBuildInfo()`](https://pkg.go.dev/runtime/debug#ReadBuildInfo)? – icza Oct 06 '21 at 10:23

2 Answers2

5

The module info is included in the executable binary, and can be acquired using the debug.ReadBuildInfo() function (the only requirement is that the executable must be built using module support, but this is the default in the current version, and likely the only in future versions).

BuildInfo.Path is the current module's path.

Let's say you have the following go.mod file:

module example.com/foo

Example reading the build info:

bi, ok := debug.ReadBuildInfo()
if !ok {
    log.Printf("Failed to read build info")
    return
}

fmt.Println(bi.Main.Path)
// or
fmt.Println(bi.Path)

This will output (try it on the Go Playground):

example.com/foo
example.com/foo

See related: Golang - How to display modules version from inside of code

icza
  • 389,944
  • 63
  • 907
  • 827
  • however, why not `bi.Main.Path` instead (for better clarity)? – blackgreen Oct 06 '21 at 11:13
  • 1
    @blackgreen `bi.Path` is the main module's path, same as `bi.Main.Path`. Both works the same, the former is shorter. – icza Oct 06 '21 at 11:20
  • Fantastic solution, only thing I'm noticing is that `ok` is `false` when running using `go test`, presumably because modules aren't being used there. Not sure what to do in that case, but I could probably fall-back to loading the `go.mod` since when `test` runs, source code is present. – Naftuli Kay Oct 07 '21 at 02:26
  • @NaftuliKay There must be something wrong how you do it, because reading build info works in tests too. Again, this solution only requires using the go tool in module mode, nothing else. – icza Oct 07 '21 at 14:04
  • It could be an IDE-related thing for me, the Goland test runner does some wizardry that has yielded other problems in the past. – Naftuli Kay Oct 08 '21 at 19:30
  • @blackgreen is correct; bi.Path may be 'command-line-arguments', while bi.Main.Path was the package name. Use bi.Main if you need the latter. :) – Tit Petric Nov 29 '22 at 16:11
3

If your goal is to just have the name of the module available in your program, and if you are okay with setting this value at link time, then you may use the -ldflags build option.

You can get the name of the module with go list -m from within the module directory.

You can place everything in a Makefile or in a shell script:

MOD_NAME=$(go list -m)
go build -ldflags="-X 'main.MODNAME=$MOD_NAME'" -o main ./...

With main.go looking like:

package main

import "fmt"

var MODNAME string

func main() {
    fmt.Println(MODNAME) // example.com
}

With the mentioned "golang.org/x/mod/modfile" package, an example might look like:

package main

import (
    "fmt"
    "golang.org/x/mod/modfile"
    _ "embed"
)

//go:embed go.mod
var gomod []byte

func main() {
    f, err := modfile.Parse("go.mod", gomod, nil)
    if err != nil {
        panic(err)
    }
    fmt.Println(f.Module.Mod.Path) // example.com
}

However embedding the entire go.mod file in your use case seems overkill. Of course you could also open the file at runtime, but that means you have to deploy go.mod along with your executable. Setting the module name with -ldflags is more straightforward IMO.

blackgreen
  • 34,072
  • 23
  • 111
  • 129
  • Okay, the second solution is _excellent_ and probably exactly what I'll use. I tried writing a [go generate command](https://github.com/naftulikay/golang-webapp/pull/2#issuecomment-934844493) but wasn't able to get the build automation working. This is definitely the next best thing, I'll bring it into my `constants` package in an `init` function and set a package variable or use a function to prevent modification. Thank you so much! – Naftuli Kay Oct 05 '21 at 22:55
  • @NaftuliKay nice, just to be clear, I'm *not* recommending you to actually embed the `go.mod` file. I gave the example mainly for learning purposes. My advice is to use `-ldflags`. Icza's answer also provides a good alternative to get the info at runtime without changing your build process – blackgreen Oct 06 '21 at 11:32