François' Blog

Building Go Applications without Go Modules

Published on 2025-04-09

#go #golang #modules #deb #rpm #packaging #linux #distro

Recently 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.

Porto, Portugal

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!

History

Point your feed reader to the RSS Feed to keep up to date with new posts.