Building Go Applications without Go Modules
Published on 2025-04-09
#go #golang #modules #deb #rpm #packaging #linux #distroRecently I found out that go.mod
is used for more than just specifying the
name of the module and its dependencies. The Go version specified in it is also
used to determine which Go "feature" flags are enabled by default when building
the software. So, when Go modules are disabled, as is done when building
packages for Debian/Ubuntu or Fedora/EL, this information is not available.
This means that all features are left in the state that will prevent them
from breaking compatibility. Looking back, this makes perfect sense! If you
update your compiler, and nothing else, you don't want software to start
breaking.
Now, we run into trouble when we are building software that uses the new "pattern syntax and matching behavior of ServeMux" introduced in Go 1.22. When you build software without modules support, this will remain disabled!
We will explore this with an example, a simplified version of the example here using the new ServeMux pattern:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("GET /foo/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World!")
})
log.Fatal(http.ListenAndServe(":8080", nil))
}
If you compile this code with Go modules off, it will not work:
$ GO111MODULE=off go build -o http http.go
You can test this with e.g. curl
:
$ ./http &
$ curl http://localhost:8080/foo/bar
404 page not found
We can inspect the http
binary to see what is going on, we only show the
DefaultGODEBUG
line:
$ go version -m ./http
./http: go1.23.7
build DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,
httplaxcontentlength=1,httpmuxgo121=1,
httpservecontentkeepheaders=1,netedns0=0,panicnil=1,
tls10server=1,tls3des=1,tlskyber=0,tlsrsakex=1,
tlsunsafeekm=1,winreadlinkvolume=0,winsymlink=0,
x509keypairleaf=0,x509negativeserial=1
You can see the new ServeMux behavior is disabled because of the
httpmuxgo121=1
flag even though the code was compiled with the Go 1.23.7
compiler.
Linux distributions disable Go modules for various reasons, but I won't go into that here now to avoid too much distraction. We just want to make this work without fighting the distributions too much.
The problem we have now is that because distributions disable modules completely, not just the dependency fetching part, they also lose the target version of the Go compiler that was intended by the software author. The build system thus will never find out that the software was written with Go >= 1.22 in mind which enabled the new ServeMux behavior.
As far as I found, there is one way to "fix" this: put the targeted Go version
directly in the file that contains the main
function. If you want to target
Go 1.22, you put this line above the package main
in the example above:
//go:debug default=go1.22
Now recompile the software and rerun the go version
command:
$ GO111MODULE=off go build -o http http.go
$ go version -m ./http
./http: go1.23.7
build DefaultGODEBUG=asynctimerchan=1,gotypesalias=0,
httpservecontentkeepheaders=1,tls3des=1,tlskyber=0,
x509keypairleaf=0,x509negativeserial=1
You can see that the list in DefaultGODEBUG
shrunk significantly as we
"opted-in" to the default flag set of Go 1.22.
Run the curl
command again:
$ ./http &
$ curl http://localhost:8080/foo/bar
Hello World!
It works!
Point your feed reader to the RSS Feed to keep up to date with new posts.