Parse go module files
Did you ever need to know, inside your program, on which go version you are running? That's what we are going to solve today. The most common use case for this is logging. We want to be able to debug by reproducing the environment of where the binary is running as close as possible. This starts by knowing which version of go we are using.
Setup
To get started doing that, we will need a go module. Let's create that:
$ go mod init test.com
We should now have a go.mod file in our folder. If you inspect this file, we can get the go version which will be used for compiling the project. It looks like this:
-
module test.com go 1.20
That's pretty much it. We will use this file to get the info we want.
Go Command
One thing that I learned recently is that we can actually get a JSON representation of or modules, workspaces, ... via the command line. To do that, we can run the following command:
$ go mod edit -json
{
"Module": {
"Path": "test.com"
},
"Go": "1.20",
"Require": null,
"Exclude": null,
"Replace": null,
"Retract": null
}
And we have our JSON!
Parsing JSON
The only thing left to do is execute this command in our main, Unmarshal the JSON result and we should be able to get the version.
First, let's define the structs into which we will Unmarshal to.
-
type Module struct { Path string } type GoMod struct { Module Module Go string }
Notice here that I'm not taking Require, Exclude, ... into account. We only want the Go string.
After that, the rest is pretty easy. We can execute a command line and get its stdout result like so:
out, _ := exec.Command("go", "mod", "edit", "-json").Output()
I'm skipping the err handling here by dropping the error with _ but make sure to handle this for production-grade scripts.
And finally, we use json.Unmarshal
function which takes the ouput of the command line, and the destination of where we want to populate the data. In our case, this is an instance of GoMod.
In the end, the main function looks like:
-
package main import ( "encoding/json" "fmt" "os/exec" ) type Module struct { Path string } type GoMod struct { Module Module Go string } func main() { var mod GoMod out, _ := exec.Command("go", "mod", "edit", "-json").Output() if err := json.Unmarshal(out, &mod); err == nil { fmt.Println(mod.Go) } }
We can run that, and we get:
$ go run main.go
1.20
We now have our Go version at runtime and we can use it for logging, selecting features, ...
The Problem
Obviously, what we saw is far from great. What if we compile our go project to binary and the go.mod is not around anymore. Well, basically it doesn't work.
I'm presenting to you this idea because combined with the right tool to build your project, you can actually embed the version inside your binary. This can be done with ldflags (check Alex Ellis blog post on the subject). But this can also be done with Bazel and stamping. If you are interested in knowing how to do that, let me know.
Conclusion
In this short post, we saw how to get the go version on which a go project is compiled at runtime. This can be used in multiple ways, but the use case that has come to me is mostly logging. I hope this is interesting for you.
If you like this kind of content let me know in the comment section and feel free to ask for help on similar projects, recommend the next post subject or simply send me your feedback.