Steven's Thoughts

In the Beginning

This is the first entry in a weekly series that will teach you everything you need to know to build microservices in Go. The series will directly port all of the NodeJS code from the amazing Bootstrapping Microservices book by Ashley Davis.

Let's begin with the first example from Chapter 2: Creating Your First Microservice.

Example 1: Say Hello World! (source)

The Node code:

  1. Imports Express.
  2. Creates an Express instance.
  3. Defines the port to listen on.
  4. Adds a GET handler for the / path.
  5. The / handler writes "Hello World!" to the browser.
  6. Makes the server listen.

The Go code almost exactly mimics the Node code.

package main

import (
	"fmt"
    // #1: net/http contains http.ServeMux, the express equivalent
	"net/http"
)

// #3: Let's make the port a constant
const port = 3000

func main() {
    // #2: http.NewServeMux() mimics express()
	mux := http.NewServeMux()

    // #4: http.ServeMux lacks method-specific handlers,
	// so we specify the method before the path.  Not
	// specifying a method makes this HandlerFunc respond
	// to every request, including PUT, POST, and DELETE.
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		// #5: fmt.Fprint writes to anything that implements
		// io.Writer, which http.ResponseWriter does, so
		// "Hello World!" gets sent to the browser.
		fmt.Fprint(w, "Hello World!")
	})

	// #6: http.ListenAndServe is the equivalent of
	// app.Listen.  http.ListenAndServe can take either
	// nil, which uses the default ServeMux, or a custom
	// ServeMux, which is mux in our case.
	http.ListenAndServe(fmt.Sprint(":", port), mux)
}

If you understand the sample above, you're well on your way to building microservices in Go. Congrats!

You will need to initialize the Go modules in the directory before running the code. You can do so with:

$ go mod init video-streaming

If you're curious, Go modules are explained here.

Now let's move on to something only slightly more exciting: streaming a video.

Example 2: Stream a video. (source)

Streaming a video is not that different from sending a text response.

The server is created the same way. The HandlerFunc is added to the ServeMux the same way. The server is even started the same way.

The only thing that changes is our endpoint: we now listen for GET requests to the /video endpoint instead of /.

Let's see the code.

package main

import (
	"fmt"
	"io"
	"net/http"
	"os"
	"strconv"
)

const (
	// #1: Use constants to avoid typos.
	// This converts most typo-induced runtime errors
	// into compile Sun Sep  8 21:52:50 2024errors.
	contentLength = "Content-Length"
	contentType   = "Content-Type"
)

const port = 8080

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /video", func(w http.ResponseWriter, r *http.Request) {
		videoPath := "../videos/SampleVideo_1280x720_1mb.mp4"
		// #2: Let's open videoPath.
		videoReader, err := os.Open(videoPath)
		if err != nil {
			// #3: Fail early on error.
			w.WriteHeader(http.StatusNotFound)
			return
		}
		// #4: defer runs the given function right before
		// the function ends, so be sure we close the file
		// if we can successfully open it.
		defer videoReader.Close()

		// #5: videoReader.Stat() is the same as fs.promises.stat.
		// we use it to get the size, failing if we can't read the
		// file statistics.
		videoStats, err := videoReader.Stat()
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		// #6: videoStats.Size() returns a 64bit integer.
		// w.Header().Add(header, value) expects a string, so
		// strconv.FormatInt converts an integer into a string
		// with the given base: 10, in this case.
		w.Header().Add(contentLength, strconv.FormatInt(videoStats.Size(), 10))
		// #7: Set the Content-Type to video/mp4 so the browser can
		// interpret it correctly.
		w.Header().Add(contentType, "video/mp4")

		// #8: Finally, use io.Copy to stream to the browser.
		// This is the same as piping the video data to res.
		io.Copy(w, videoReader)
	})

	// #9: Starts the http.ServeMux.
	http.ListenAndServe(fmt.Sprint(":", port), mux)
}

Example 3: Live reloads. (source)

Go lacks a builtin live-reload feature, so we'll have to find a nodemon equivalent. Fortunately for us, air is the solution.

Install air with go install github.com/air-verse/air@latest, then start the server by changing to the example-03 directory and running air.

With the server still running, we decide that we want the server to run on any port we specify in the PORT environment variable, so we remove the const port = 3000 line and extract it from the environment instead.

package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
)

const (
	contentLength = "Content-Length"
	contentType   = "Content-Type"
)

func main() {
	// #1: Look the PORT environment variable.
	// Extract its value and learn if it was found.
	port, found := os.LookupEnv(`PORT`)
	// #2: If PORT wasn't provided, we fail and the server never starts.
	if !found {
		log.Fatal(`Please specify the port number for the HTTP server with the environment variable PORT.`)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /video", func(w http.ResponseWriter, r *http.Request) {
		videoPath := "../videos/SampleVideo_1280x720_1mb.mp4"
		videoReader, err := os.Open(videoPath)
		if err != nil {
			w.WriteHeader(http.StatusNotFound)
			return
		}
		defer videoReader.Close()
		videoStats, err := videoReader.Stat()
		if err != nil {
			w.WriteHeader(http.StatusInternalServerError)
			return
		}

		w.Header().Add(contentLength, strconv.FormatInt(videoStats.Size(), 10))
		w.Header().Add(contentType, "video/mp4")
		io.Copy(w, videoReader)
	})

	http.ListenAndServe(fmt.Sprint(":", port), mux)
}

That change wasn't too hard. The updated code now allows us to launch the server on any port we want, as seen below.

$ PORT=4000 air

We can even launch many servers, each listening on its own port! That feature will come in handy in a few weeks when we have multiple microservices running simultaneously.

Congratulations if you've made it this far! New updates will be posted every Monday for the next few months. We will be covering everything in Bootstrapping Microservices and then some!

#microservices #go