Building a Lightweight Web Server with Golang

ยท

6 min read

1-web-server-3edc7d64437aeed2116ff819898d2d4d.png

Let's have some fun with Golang by building a simple website application server. Golang is a very robust and fast programming language. We will build this purely with only Golang no extra dependencies.

To follow this tutorial, you need to have basic knowledge of Golang programming. Visit Go Tour to quickly get a basic understanding of programming Golang.

The goal is to write a Go program that serves HTML, CSS, JS, and static files from a root directory. For example, if we have a directory structure like below

project
โ”œโ”€โ”€ website
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ”œโ”€โ”€ file1.html
|   โ”œโ”€โ”€ file2.html
|   โ””โ”€โ”€ assets
โ”‚       โ”œโ”€โ”€ style.css
โ”‚       โ””โ”€โ”€ script.js
โ””โ”€โ”€ main.go

When we run main.go with port 8000, we would be able to visit the following URLs

The website directory is the root directory. We can place all our HTML files and static files in that directory. Then we can access them with the URL http://localhost:8000/. This URL represents our root directory, website. While building the web server we can decide

  • which directory to be the root folder,
  • what the URL should be, and
  • how files are processed.

Why Golang

Languages like Python requires developers to depend on web frameworks like Django or Flask to build web applications. Golang has a lot of built-in functionality for building web apps, like;

  • Handling routes,
  • Database connections,
  • Template rendering,
  • Testing,
  • and more.

Golang requires fewer to no dependencies installed to build a website application.

Get set, Go! ๐Ÿ›ฉ๏ธ

Install Golang

Install Golang by visiting Go.dev to install Golang on your machine. The installation binaries are available for any Operating System (Windows, Mac, Linux, etc.).

Setup project directory

To setup our project directory,

  • create a new directory/folder for the web server project,
  • create a file named main.go, and
  • open the file in any editor of choice.

Let's build ๐Ÿ’ช๐Ÿฝ

Paste the following content in the main.go file.

package main


func main(){

}

Every Go program is made up of packages and Programs start running in package main. The function main is executed when we run our main.go file or its build.

Create a function called ServeStatic that registers the pattern / to a handler function that serves static contents in the root folder. The pattern is registered in the default DefaultServeMux. We could also create our own ServeMux, and register patterns to it. We could have multiple ServeMux depending on your requirement.

import "net/http"

// Provides handler function to read and respond with static files
func ServeStatic() {
    h1 := func(w http.ResponseWriter, r *http.Request) {
        // code here
    }
    http.HandleFunc("/", h1)
}

Take a look at http.HandleFunc("/", h1), the first parameter is the pattern, and the second parameter is the handler function. http.HandleFunc registers the pattern, /, to the handler function, h1.

DefaultServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.

We use the pattern "/" because it matches all paths not matched by other registered patterns, not just the URL with Path == "/". Meaning the pattern, "/" matches URLs paths

  • /
  • /anything
  • /a.html
  • /a/b/c.html
  • /a/b/c/.../z.html

When we visit paths like /blog/posts/web-developments/how-to-build-a-website.html our handler function will be executed, because its pattern "/" matches it.

The handle function is executed with two parameters, a http.ResponseWriter and a *http.Request. We can use the ResponseWriter to construct an HTTP response. The Request represents an HTTP request received from the server. It contains data like URL path, Request headers, Request body, the Request method, etc.

The static file path can be extracted from the URL Path, then we can get the content from the file path and use it to construct an HTTP response with our ResponseWriter.

Let's create a function to construct the file path relative to a root folder called website. We can use any directory as our root folder. I am using this for simplicity.

// Gets the relative path of the requested path, making `website` the base path
func RelPath(path string) string {
    return "website" + path
}

This is my directory structure,

go_server
โ”œโ”€โ”€ website
โ”‚   โ”œโ”€โ”€ index.html
โ”‚   โ”œโ”€โ”€ about.html
|   โ”œโ”€โ”€ main.css
|   โ””โ”€โ”€ contact.html
โ””โ”€โ”€ main.go

Therefore, if the path is /about.html, the file path relative to the root folder, website, is website/about.html.

For example, if our directory structure is like the below.

go_server
โ”œโ”€โ”€ website
โ”‚   โ””โ”€โ”€ public
โ”‚       โ”œโ”€โ”€ index.html
โ”‚       โ”œโ”€โ”€ about.html
|       โ”œโ”€โ”€ main.css
|       โ””โ”€โ”€ contact.html
โ””โ”€โ”€ main.go

If we wanted the public folder to be the root folder, then our RelPath function should look like this

func RelPath(path string) string {
    return "website/public" + path
}

The complete ServeStatic function is below.

import "net/http"

// Provides handler function to read and respond with static files
func ServeStatic() {
    h1 := func(w http.ResponseWriter, r *http.Request) {
        path := RelPath(r.URL.Path)
        http.ServeFile(w, r, path)
    }
    http.HandleFunc("/", h1)
}

http.ServeFile is a function that replies to the request with the contents of the named file or directory. After the execution of RelPath(r.URL.Path), the variable, path, will be a file path relative to the root folder. The program is able to access this folder because the main.go file is in the same directory as the root folder.

If the path does not exist, a 404 page not found response is written to the ResponseWriter. Read more on http.ServeFile to see how it handles paths.

Finally, We need to listen to requests on a TCP network address.

import "log"

// Listen and serve port
func StartServer(addr string) {
    log.Printf("Starting server listening on: http://localhost:%v", addr)
    log.Fatal(http.ListenAndServe(":"+addr, nil))
}

StartServer function accepts a network address, addr, to use with http.ListenAndServe. The ListenAndServe function accepts an addr, string, and a handler, http.Handler.

ListenAndServe listens to the TCP network address, addr, and then calls Serve with the handler to handle requests on incoming connections. The handler is typically nil, in which case the DefaultServeMux is used. You can create your own ServeMux to use here instead.

For example, if addr is 6500, then the URL will be http://localhost:6500. So any time a request is sent to the URL, the DefaultServeMux handles it.

Now we update our main function to execute the ServeStatic and StartServer functions.

func main() {

    ServeStatic()
    StartServer("8000")

}

Here we used 8000 as our TCP network address, therefore our URL will be http://localhost:8000.

This is what the complete main.go file should look like

package main

import (
    "log"
    "net/http"
)

func main() {

    ServeStatic()
    StartServer("8000")

}

// Listen and serve port
func StartServer(addr string) {
    log.Printf("Starting server listening on: http://localhost:%v", addr)
    log.Fatal(http.ListenAndServe(":"+addr, nil))
}

// Gets the relative path of the requested path, making `website` the base path
func RelPath(path string) string {
    return "website" + path
}

// Provides handler function to read and respond with static files
func ServeStatic() {
    h1 := func(w http.ResponseWriter, r *http.Request) {
        path := RelPath(r.URL.Path)
        http.ServeFile(w, r, path)
    }
    http.HandleFunc("/", h1)
}

Check out this repo for the complete project. It has sample HTML and CSS files to use with the web server.

Conclusion ๐ŸŽฏ

You can do much with Golang's standard HTTP library. For sample projects, you can build a website form, polling website, todo website, CRUD website, Rest APIs, and more

The best place to learn and get more information about Golang libraries & packages is their documentation. It has robust documentation with examples.

Image description

ย