code and games
Built with Hugo and Hyde-Y.

Serving Multiple Sites from One Go Process

· Read in about 3 min · (449 Words)

A toy project I like to fiddle with is a person URL shortener. Right now I have an old URL shortener on zikes.me to which I added some logic to the routing, such that certain paths would lead to administrative areas while others would be treated as shortened URL keys. This was pretty hack-ish, and I’d like to be able to use /anything as a key without worry. That meant either a separate administrative process, an API, or somehow serving a separate admin area via a subdomain, like admin.zikes.me. The third option sounded ideal, as keeping the admin within the same process should keep the code pretty tight.

First, I created an HTTP multiplexer for each using http.NewServeMux()

keyServer := http.NewServeMux()

adminServer := http.NewServeMux()

Then defined a few routes on each:

keyServer := http.NewServeMux()
keyServer.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){})

adminServer := http.NewServeMux()
adminServer.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request){})
adminServer.HandleFunc("/add", func(w http.ResponseWriter, r *http.Request){})

Now, normally when there’s just one, you can http.ListenAndServe(":80", myServer) and call it a day. But in this case calling it for either one of those servers will block the process, preventing the second from starting until the first has stopped.

http.ListenAndServe(":80", keyServer)

// Won't be reached until the keyServer shuts down!
http.ListenAndServe(":8000", adminServer)

The obvious solution is to use a couple of goroutines:

go func() {
  http.ListenAndServe(":80", keyServer)
}
go func() {
  http.ListenAndServe(":8000", adminServer)
}

But the above code will cause func main() to cease blocking, meaning the process will only run for a fraction of a second!

There are a few solutions to this that I’m aware of, and probably many more that I’m not. I feel like a channel is the cheapest/simplest of the set I’m aware of, so that’s what I went with:

func main() {
  shutdown := make(chan bool)

  keyServer := ...
  adminServer := ...

  go func() {
    http.ListenAndServe(":80", keyServer)
    // Not reached unless the above server stops
    shutdown <- true
  }
  go func() {
    http.ListenAndServe(":8000", adminServer)
    // Not reached unless the above server stops
    shutdown <- true
  }

  // Blocks and waits until it receives a bool
  <-shutdown
}

This solution will block and wait until either server has stopped, which works well for my case. If for some reason you need to wait until both have stopped, you could use a sync.WaitGroup:

func main() {
  var wg sync.WaitGroup

  keyServer := ...
  adminServer := ...

  // Increment the WaitGroup counter
  wg.Add(1)
  go func() {
    http.ListenAndServe(":80", keyServer)
    // Not reached unless the above server stops
    wg.Done()
  }

  // Increment the WaitGroup counter
  wg.Add(1)
  go func() {
    http.ListenAndServe(":8000", adminServer)
    // Not reached unless the above server stops
    wg.Done()
  }

  // Blocks and waits until ALL WaitGroup members
  // have signaled completion
  wg.Wait()
}