Get Name of Current Module in Go
Asked Answered
A

2

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.

Angeles answered 5/10, 2021 at 19:7 Comment(5)
@gopher is there a way to do codegen at build time to parse go.mod and embed the result in my program?Angeles
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.Contemplation
@Contemplation 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.Angeles
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.Contemplation
@gopher There isn't? What about debug.ReadBuildInfo()?Raynold
R
6

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

Raynold answered 6/10, 2021 at 10:20 Comment(6)
however, why not bi.Main.Path instead (for better clarity)?Oballa
@Oballa bi.Path is the main module's path, same as bi.Main.Path. Both works the same, the former is shorter.Raynold
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.Angeles
@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.Raynold
It could be an IDE-related thing for me, the Goland test runner does some wizardry that has yielded other problems in the past.Angeles
@Oballa 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. :)Rusell
O
4

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.

Oballa answered 5/10, 2021 at 20:38 Comment(2)
Okay, the second solution is excellent and probably exactly what I'll use. I tried writing a go generate command 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!Angeles
@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 processOballa

© 2022 - 2024 — McMap. All rights reserved.