Go tip of the week: A demo-friendly web server

Today, I installed a Web app from a repo, but when I started the app, I found that the demo code serves the web pages hard-coded on port 8080. This port was already occupied on my machine, so I had to grab into the code to set the port to 8081.

Then, the app ran fine and listened on port 8081, but the log output just said something like: “Listening on :8081”.

I thought to myself, what if the app scans for a free port and then prints a clickable URL in the log output? I would then be able to run the demo without any obstacles.

If you write a web app and want to attract raving fans, you probably want your app to do these things, too.

Black Friday Special:
My course Master Go is $60 off until Dec 2nd.
Hop over to AppliedGo.com for more info.

And it's not that difficult. First, let a loop test if a port within a given port range is available. We can use a net.Listener for this. The loop exits if a free port is found or if the search range is exhausted.

func findFreePort(start, end int) (net.Listener, int, error) {
	for port := start; port <= end; port++ {
		addr := fmt.Sprintf(":%d", port)
		listener, err := net.Listen("tcp", addr)
		if err == nil {
			return listener, port, nil
		}
		log.Println(err)
	}
	return nil, 0, errors.New("No port available")
}

The HTTP server then can take the active listener to start serving pages on the port found.

func main() {
	listener, port, err := findFreePort(8080, 8180)
	if err != nil {
		log.Fatalf("findFreePort: %v\n", err)
	}

	log.Printf("Starting server...")
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello from port %d", port)
	})
	go func() {
		err := http.Serve(listener, nil)
		if err != nil {
			log.Fatalf("Could not start server on port %d: %v", port, err)
		}
	}()
	log.Printf("Server started. Open http://localhost:%d", port)
	select {} // prevent that main() exits (quick and dirty solution)
}

(Full code here. Note that it does not run in the playground, due to deadlock issues in the sandbox. You can save the code to a local main.go file and call go run main.go to play with it.)

A sample output of this code could look like this:

> go run .
2024/06/07 14:41:13 listen tcp :8080: bind: address already in use
2024/06/07 14:41:13 listen tcp :8081: bind: address already in use
2024/06/07 14:41:13 Starting server...
2024/06/07 14:41:13 Server started. Open http://localhost:8082

Now the server runs on the first open port in the search range, and the log output contains a clickable link (if the terminal supports link detection). No more failing demo servers!

Extra tip on top: let Unix select a port for you

On Unix-ish systems (like Linux or macOS), you can let the OS choose an available port for your listener by using either an empty port or “0”.

Examples:

listener, err := net.Listen("tcp", ":0")
listener, err := net.Listen("tcp", ":")
listener, err := net.Listen("tcp", "")
listener, err := net.Listen("tcp", "127.0.0.1:0")
listener, err := net.Listen("tcp", "127.0.0.1:")
listener, err := net.Listen("tcp", "[::]:0")
listener, err := net.Listen("tcp", "[::]:")

Unlike in the generic solution above, you cannot specify a port range, but it's still a neat, quick solution for development or testing purposes.