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