By guest author Yashish Dua (GitHub: https://github.com/yashishdua; Twitter: https://twitter.com/duayashish).

Note: This Go gopher was designed by Renee French. The design is licensed under the Creative Commons 3.0 Attributions license.

This tutorial is intended for people with some experience writing Go who want to learn more about HTTP servers in Go. Some Go concepts are explained in detail and some, like how functions and variables are declared and used, are skipped.

What is Go?

Go is an open source programming language designed at Google in 2007 to build simple, reliable, and efficient software. It is a statically typed language which feels like a dynamically typed language due to its fast compilation.

"Go is more about software engineering than programming language research. Or to rephrase, it is about language design in the service of software engineering." ~ Rob Pike, co-creator of Go

Fast and reliable

Go combines the ease of programming in an interpreted, dynamically typed language with the efficiency and safety of a statically typed, compiled language. Go compiles code very fast because it provides a model for software construction that makes dependency analysis easy and avoids much of the overhead of C-style include files and libraries.

Portable

Go code is compiled into static binaries, meaning compiled programs are completely independent and contain all of their dependencies. Go allows you to define a target architecture and build a static binary for it. You can pick up the binary and directly run it on a server that has that target architecture. This is a huge benefit when you're deploying to a number of servers. You can just copy the same binary to every other server that has the same machine image, making deployment scalable, easy, and fast.

Concurrent

Go provides first-class support for concurrency. The Go runtime can process hundreds of thousands of goroutines (lightweight threads) concurrently. This native concurrency support has made Go a popular choice for high-traffic web servers.

What you'll build

In this codelab, you build a web server in Go. Your server will:

What you'll learn

Because this codelab is focused on building a web server, we skip non-relevant concepts. Similarly, code blocks that don't illustrate the basic concepts are provided for you to simply copy.

What you'll need

The basic framework of the web server is that it accepts incoming requests and maps (routes) them to a handler function. You use the http.HandleFunc method to map a pattern string (a URL endpoint) to a handler function that's responsible for processing a request (the Request object) and writing the response to the ResponseWriter object. The method looks like this:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

Start your Go project

  1. Create a new project directory named go-webserver:
$ mkdir go-webserver
  1. Go to the go-webserver directory:
$ cd go-webserver
  1. Create a Go module.
$ go mod init example.com/webserver


This creates a Go module for dependency management. For more information, see Using Go Modules.

  1. Using your code editor, create a main.go file with the following content:
package main

import (
        "fmt"
        "net/http"
)

func main() {
        http.HandleFunc("/welcome", welcome)
}

func welcome(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Welcome to the Go webserver codelab")
}

This code snippet routes the /welcome URL to a function that writes "Welcome to the Go webserver codelab" to the ResponseWriter object. However, at this point, you have only mapped a handler and have yet not started an HTTP server to listen for requests.

Listening for requests

To listen for requests, you use the http.ListenAndServe method. This method starts an HTTP server with a given address and handler. The function returns an error if HTTP fails to start a TCP connection, which you can handle gracefully if required. The function looks like this:

func ListenAndServe(addr string, handler Handler) error

Add a listener

  1. Open the main.go file if you previously closed it.
  2. Copy the following code into the file, overwriting the existing contents:
package main

import (
        "fmt"
        "log"
        "net/http"
)

func main() {
        http.HandleFunc("/welcome", welcome)

        if err := http.ListenAndServe(":8080", nil); err != nil {
                log.Fatal(err)
        }
}

func welcome(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Welcome to the Go webserver codelab")
}


The Go http documentation states that the second argument to ListenAndServe should be a handler. If you pass nil, the value defaults to DefaultServeMux. A ServeMux object is essentially an HTTP request router (or multiplexor). The router compares incoming requests against a list of predefined URL paths and calls the associated handler for the path when a match is found. You will look at ServeMux more closely in later sections of the codelab. A nil handler is useful for initial development and simple apps.

Run the server

  1. Go to your project directory:
$ cd go-webserver
  1. Build the project:
$ go build


This creates a binary file in the same directory.

  1. Run the binary, which in this case runs the web server:
$ ./go-webserver


Your web server is now running at localhost:8080.

  1. In another terminal window, use curl to make a request to the server:
$ curl localhost:8080/welcome

You see the results that are returned by the web server:

Welcome to the Go webserver codelab

What are static assets?

As the name suggests, static assets are images, videos, files, and so on that are not updated dynamically by the server. They're just files that you're storing on your server and that are sent as is to users. A user can interact with these static assets and even can navigate to other linked static assets.

Set up static assets

  1. In the root of your project, create a directory named static. This directory name can be anything. In this codelab, you use static so it's easy to remember what the directory contains.
  2. Using your code editor, create a file in the static directory named index.html and copy the following code into it:
<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>A static page</title>
  <link rel="stylesheet" href="/main.css">
</head>
<body>
  <h1>Our static asset is live using Go!</h1>
</body>
</html>
  1. Create a file in the static directory named main.css and copy the following code into it:
body {color: #c0392b}
  1. Save and close both files. When you're done, you have a structure like this:
project-name/
  static/
    index.html
    main.css
  main.go

Serving static assets

To serve static assets, you use the http.FileServer function. This function takes a FileSystem object (which implements access to a collection of named files) and returns a Handler that you can register as an HTTP handler. The method looks like this:

func FileServer(root FileSystem) Handler

Add a handler for static assets

  1. Open the main.go file.
  2. Copy the following code into the file, overwriting the existing contents:
package main

import (
        "fmt"
        "net/http"
)

func main() {
        http.HandleFunc("/welcome", welcome)

        fs := http.FileServer(http.Dir("static"))
        http.Handle("/", fs)

        if err := http.ListenAndServe(":8080", nil); err != nil {
                fmt.Println(err)
        }
}

func welcome(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Welcome to the Go webserver codelab")
}


The static directory will now handle all requests other than /welcome because it was registered to handle the / path.

Run the server

  1. Go to your project directory:
$ cd go-webserver
  1. Build the project:
$ go build
  1. Run the binary:
$ ./go-webserver


Your server is running now at localhost:8080 and serving the static index.html file.

  1. Make a request:
$ curl localhost:8080

You see the following results:

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <title>A static page</title>
  <link rel="stylesheet" href="/main.css">
</head>
<body>
  <h1>Our static asset is live using Go!</h1>
</body>
</html>

In this section, you set up another HTTP endpoint with a different route. Routing lets you modularize code, increase reliability, and help improve the scalability of your app. In Go, requests are routed using an http.ServeMux. The http.DefaultServeMux instance and http.NewServeMux function create and set up an http.ServeMux object.

The http.ListenAndServe function starts an HTTP server with a given address and default handler, as discussed in the first section of this codelab. When the handler is nil, the http.ListenAndServe function uses the DefaultServeMux object.

Add a route

To handle a route, you add a handler function to the DefaultServeMux instance. Routes are matched to their corresponding handlers.

  1. Open the main.go file.
  2. Copy the following code into the file, overwriting the existing contents:
// The webserver command starts an example HTTP server.
package main

import (
        "encoding/json"
        "fmt"
        "net/http"
)

type profile struct {
        Name    string
        Hobbies []string
}

func main() {
        http.HandleFunc("/welcome", welcome)

        http.HandleFunc("/profile", getProfile)

        if err := http.ListenAndServe(":8080", nil); err != nil {
                fmt.Println(err)
        }
}

func getProfile(w http.ResponseWriter, r *http.Request) {
        if r.Method != http.MethodGet {
                http.Error(w, "Invalid request method.", http.StatusMethodNotAllowed)
                return
        }

        profile := profile{
                Name:    "Yashish",
                Hobbies: []string{"sports", "programming"},
        }

        w.Header().Set("Content-Type", "application/json")

        if err := json.NewEncoder(w).Encode(profile); err != nil {
                http.Error(w, err.Error(), http.StatusInternalServerError)
        }
}

func welcome(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Welcome to the Go webserver codelab")
}

This version of the file contains two endpoints: /welcome and /profile. The static content was removed to keep the code shorter.

To get the request method, http.Request has a property named Method.

The encoding/json package from the standard library in Go implements encoding and decoding of JSON data. To encode JSON data, you use the json.Encoder type. The json.NewEncoder method returns an Encoder object that writes JSON values to an output stream using the Encode method. If you want to write a string rather than a stream, you can use the json.Marshal and json.Unmarshal functions.

Run the server

Now run the server again. This time you have two routes available.

  1. Go to your project directory:
$ cd go-webserver
  1. Build the project:
$ go build
  1. Run the binary:
$ ./go-webserver

Your server is running now at localhost:8080.

  1. Make a request to localhost:8080/welcome:
$ curl localhost:8080/welcome

You see the following output:

Welcome to the Go webserver codelab
  1. Make a second request, this one to localhost:8080/profile:
$ curl localhost:8080/profile

You see the following output:

{"Name":"Yashish","Hobbies":["sports","programming"]}

The http.NewServeMux function returns a ServeMux object that's an HTTP request multiplexer. Unlike DefaultServeMux, the NewServeMux method allows you to have full control over handlers and routing. The ServeMux type can also be used as an HTTP handler for another ServeMux object. To understand this concept, imagine that you have the following routes:

As your system evolves, you want to deploy a second version of the API that supports the following paths:

With the help of a ServeMux object, you can create subrouting to make code modular, independent, and easily modifiable. Using the http.NewServeMux function, create a ServeMux object, v1Mux, for v1 APIs. Similarly, create another ServeMux object, v2Mux, for v2 APIs.

To use these different ServeMux objects under different routes, you need to create another ServeMux object, mux, that's passed to the http.ListenAndServe function and that's responsible for routing v1 and v2 requests to the correct ServeMux instance.

Add subrouting

  1. Open the main.go file.
  2. Copy the following code into it, overwriting the previous contents:
package main

import (
        "fmt"
        "net/http"
)

func main() {
        mux := http.NewServeMux()

        v1Mux := http.NewServeMux()

        v1Mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v1 Profile")
        })

        v1Mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v1 Posts")
        })

        v2Mux := http.NewServeMux()

        v2Mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v2 Profile")
        })

        v2Mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v2 Posts")
        })

        mux.Handle("/v1/", http.StripPrefix("/v1", v1Mux))
        mux.Handle("/v2/", http.StripPrefix("/v2", v2Mux))

        if err := http.ListenAndServe(":8080", mux); err != nil {
                fmt.Println(err)
        }
}


The v1Mux instance has the v1 subroutes, which are routed to /v1/ in the parent mux object. Similarly, the v2Mux instance has the v2 sub routes, which are routed to /v2/ in the parent mux object. The http.StripPrefix function returns a handler that serves HTTP requests by removing the given prefix from the request URL's path and invoking the handler.

Run the server

Now run the server again. This time you have two versions available.

  1. Go to your project directory:
$ cd go-webserver
  1. Build the project:
$ go build
  1. Run the binary:
$ ./go-webserver

Your server is running now at localhost:8080.

  1. Make GET requests to the following URLs:
  1. localhost:8080/v1/profile
  2. localhost:8080/v1/posts

The following example shows the commands and the corresponding results:

$ curl localhost:8080/v1/profile 
v1 Profile
$ curl localhost:8080/v1/posts  
v1 Posts
  1. For v2 APIs, make GET requests to the following URLs:
  1. localhost:8080/v2/profile
  2. localhost:8080/v2/posts

The following example shows the commands and results, which show that the requests were routed to the v2 handlers:

$ curl localhost:8080/v2/profile 
v2 Profile
$ curl localhost:8080/v2/posts  
v2 Posts

Middleware and its significance

Middleware is a layer that lies between the operating system and the applications running on it. When you're building a web application, there's probably some shared functionality that you want to run for many (or even all) HTTP requests. For example, you might want to log every request, compress every response with gzip, or check a cache before doing some heavy processing. In technical terms, middleware is a way to run code before or after the handler code in an HTTP request lifecycle.

The nice thing about middleware is that if it's implemented correctly, middleware is extremely flexible, reusable, and shareable.

Building middleware handlers in Go

In Go, a typical place to use middleware is between a ServeMux instance and your application handlers. A common approach in Go is to create a wrapper around the existing http.Handler instance, like the following:

func exampleMiddleware(handler http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // Do something BEFORE handling request.

                handler.ServeHTTP(w, r)
                log.Printf("Handler finished processing request")

                // Do something AFTER handling request.
        })
}

This exampleMiddleware function takes an http.Handler object, does some processing, and returns a new http.Handler object. The ServeHTTP function is responsible for executing the request. Preprocessing is done before invoking this method, and post processing is done after ServeHTTP is executed. With this approach, the handler has handled the request, but you can still modify the response.

You can use this format to create your own middleware that logs every incoming request to the server.

Add logging using middleware

  1. Open the main.go file.
  2. Copy the following code into it, overwriting the existing contents:
package main

import (
        "fmt"
        "log"
        "net/http"
)

func main() {
        mux := http.NewServeMux()

        v1Mux := http.NewServeMux()

        v1Mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v1 Profile")
        })

        v1Mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v1 Posts")
        })

        v2Mux := http.NewServeMux()

        v2Mux.HandleFunc("/profile", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v2 Profile")
        })

        v2Mux.HandleFunc("/posts", func(w http.ResponseWriter, r *http.Request) {
                fmt.Fprintln(w, "v2 Posts")
        })

        mux.Handle("/v1/", http.StripPrefix("/v1", v1Mux))
        mux.Handle("/v2/", http.StripPrefix("/v2", v2Mux))

        loggedHandler := loggingMiddleware(mux)

        if err := http.ListenAndServe(":8080", loggedHandler); err != nil {
                fmt.Println(err)
        }
}

func loggingMiddleware(handler http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                log.Printf("Got a %s request for: %v\n", r.Method, r.URL)
                handler.ServeHTTP(w, r)
                log.Printf("Handler finished processing request")
        })
}


Now instead of passing nil or mux to the http.ListenAndServe function, the code passes the wrappedMux instance, which uses the LoggingMiddleware function to log every request.

Run the server

Now run the server again. This time, requests are logged.

  1. Go to your project directory:
$ cd go-webserver
  1. Build the project:
$ go build
  1. Run the binary:
$ ./go-webserver

Your server is running now at localhost:8080.

  1. Try making GET requests to the server. You get the same HTTP responses as in the previous section. You also see log messages in the terminal window where you started the server:
2019/09/23 17:17:04 Got a GET request for: /v1/profile
2019/09/23 17:17:04 Handler finished processing request
2019/09/23 17:17:14 Got a GET request for: /v1/posts
2019/09/23 17:17:14 Handler finished processing request
2019/09/23 17:17:18 Got a GET request for: /v2/profile
2019/09/23 17:17:18 Handler finished processing request
2019/09/23 17:17:21 Got a GET request for: /v2/posts
2019/09/23 17:17:21 Handler finished processing request

Congratulations, you've successfully built your first web server in Go!

You added static assets, and you added multiple routes to your server. You learned what middleware is and how to build middleware in Go.

You now know the key steps required to build any web server in Go.

Further reading

Reference docs