This post was originally published on https://thenewstack.io/building-a-web-server-in-go/
Go (Golang.org) is the system programming language that provides standard HTTP protocol support in its standard library, which makes it easy for developers to build and get a web server running very quickly. Meanwhile, Go offers developers a lot of flexibility. In this post, we lay out several ways to build an HTTP web server in Go and then offer an analysis about how and why these different approaches all work perfectly in Go.
Before we get started, I assume you have basic knowledge about how to write Go code, you understand HTTP and know what a web server is. Then, we can begin with the famous “hello world” example in an HTTP web server version.
It’s better to see the effect and then explain the detail. Create a file named http1.go
and copy and paste the following code into the file:
package main
import (
"io"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello world!")
}
func main() {
http.HandleFunc("/", hello)
http.ListenAndServe(":8000", nil)
}
In the terminal, execute the command go run http1.go
, then open the browser and visit http://localhost:8000
. You will see Hello world!
is showing on your screen.
How did that happen? Well, any runnable package must be named main
in Go, and here we have the main function and a hello function.
In the main function, we called the function http.HandleFunc
from the package net/http
to register another function to be the handle function, which is the hello function in this case. This function accepts two arguments. The first one is a string
type pattern, which is the route you want to match and it’s the root path in the example. The second argument is a function with the signature func(ResponseWriter, *Request)
. As you can see, our hello function has exactly the same signature, therefore we can pass it as the argument. The next line we called the http.ListenAndServe(“:8000”, nil)
function to listen on localhost with port 8000. Ignore the nil
argument just for now.
In the hello function, we have two arguments. One with type http.ResponseWriter
and its corresponding response stream, which is actually an interface type. The second is *http.Request
and its corresponding HTTP request. We don’t have to use all the arguments. Just like in the hello function, if we want the response string Hello world!
in the browser, then we only use http.ResponseWriter
. io.WriteString
is a helper function to let you write a string into a given writable stream. In Go, we call it the io.Writer
interface. It’s not the point here, so knowing it works for us is enough.
This example is a little bit more complicated:
package main
import (
"io"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello world!")
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", hello)
http.ListenAndServe(":8000", mux)
}
In the example above, we don’t use the nil
in function http.ListenAndServe
any more. Instead, replace it with a variable with type *ServeMux
. As you may guess, this example does exactly the same thing as the previous example but we use the variable mux
to register the handle function instead of directly registering from the net/http
package. What’s going on underneath? Well, the reason you can directly register the handle function in the package level is because net/http
has a default *ServeMux
inside the package, where now we defined our own.
To make the example even more complicated, see the code below:
package main
import (
"io"
"net/http"
)
func hello(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, "Hello world!")
}
var mux map[string]func(http.ResponseWriter, *http.Request)
func main() {
server := http.Server{
Addr: ":8000",
Handler: &myHandler{},
}
mux = make(map[string]func(http.ResponseWriter, *http.Request))
mux["/"] = hello
server.ListenAndServe()
}
type myHandler struct{}
func (*myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h, ok := mux[r.URL.String()]; ok {
h(w, r)
return
}
io.WriteString(w, "My server: "+r.URL.String())
}
To confirm your guess, this time we’re doing the same thing again, which is showing the Hello world!
string on the screen. However, we not only define the *ServeMux
, but also the variable server with type http.Server
. At this point, you should know why we are able to run and serve the HTTP server directly from the net/http
package. Yes, it has a default server inside the package as well. A new thing we see here is the type myHandler
we defined and its method ServeHTTP
. How is it possible to define a custom type and use it in an unmodified function from a standard library in a static programming language? The truth is simple. The Handler
is an interface and only required to implement one method whose signature is func(w http.ResponseWriter, r *http.Request)
and must be named as ServeHTTP
. The type myHandler
has the method that the interface expects, so we are good.
You can see how our code changed but for the same purpose. Don’t forget to try the three examples yourself, then make some changes and see how the change behaves. Hope you enjoy!