Golang: Build a simple web server and interact with it

icelandcheng
Nerd For Tech
Published in
10 min readNov 27, 2022

Golang is an open-source programming language supported by Google. It is famous for its powerful performance, and it also provides many useful packages for us to build a variety of applications. In this article, I would like to share how to build a simple web server by using some Golang packages.

1. Environment Preparation

first of all, we need to install Go. The way to download and install could see here on Golang's official website. After installing, could run go version to check the version installed.

Next, need to prepare Golang project environment. Could follow the steps here. Basically, you should create a folder and then run go mod init <modulepath> command to generate a go.modfile.

mkdir my-project
cd my-project
go mod init <modulepath>

Golang use go.mod to manage all the modules need to import in this project( similar like Gemfile in Rails project). The project itself is also a module, and the <modulepath> is the path prefix for package paths(the path of folders in the projects) within the module. More detail about the naming of <modulepath> could be found in Go Module Reference. Below is a part of the description. Usually, it will include the root path of your project.

A module path should describe both what the module does and where to find it. Typically, a module path consists of a repository root path, a directory within the repository (usually empty), and a major version suffix (only for major version 2 or higher).

Go Module Reference

If running go mod init my-project , it will generate a go.mod file in the project and the go.mod file content is like below.

// go.mod

module my-project

go 1.19

The way that Golang use go.mod to manage projects is different from using GOPATH . Golang will detect the file we are going to execute through the location of go.mod rather than checking in GOPATH . go.mod is the default build mode since Go 1.16, therefore the use of GOPATH is not recommended.

In Golang projects, they're usually a main.go file and a main function in it, and we will executego run main.go to run our project, so we need to create main.go and add main function in it, then we could start writing some code in main function to build the web server.

package main

func main() {

}

The project structure is like below.

2. Useful Package Introduction

For building a web server, Golang provides several packages, like net, net/http, log, fmt, and in the package, there are several functions that are useful for building a simple web server. The followings are introductions of those packages and functions:

Package net provide an interface for network I/O. It provides simple functions like Dial is for connecting a server and Listen is for creating a server. In Listen function, we could pass network and address , and it will start a server on that local network address and use the type of network . The network must be tcp, tcp4, tcp6, unix or unixpacket.

For TCP networks, if the host in the address parameter is empty or a literal unspecified IP address, Listen listens on all available unicast and anycast IP addresses of the local system. To only use IPv4, use network “tcp4”. The address can use a host name, but this is not recommended, because it will create a listener for at most one of the host’s IP addresses. If the port in the address parameter is empty or “0”, as in “127.0.0.1:” or “[::1]:0”, a port number is automatically chosen. The Addr method of Listener can be used to discover the chosen port.

From net package func Listen online document

func Listen(network, address string) (Listener, error)

The example code of Listen is as below. It will start a server on localhost:8080 and using TCP networks.

listener, error := net.Listen("tcp", ":8080")
if error != nil {
// handle error
}

Package net/http provides HTTP client and server implementations. It could be used for HTTP (or HTTPS) requests, and It provides functions like HandleFunc is for registering the handler function for the given pattern of URL path in DefaultServeMux which is a default ServeMux , a struct in Golang for routing collection, so that we could implement what is the response when sending HTTP(or HTTPS) requests to a specific URL.

ServeMux 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.

From net/http ServeMux online document

In HandleFunc , we just need to pass a pattern referring to a URL path and a handler to handle the response when sending an HTTP(or HTTPS) request to this URL path.

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

The sample code is as below and will work when sending HTTP(or HTTPS) requests to /bar , and the response will print Hello /bar on the web page.

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

Package net/http provide Serve function to accept incoming HTTP connections to the server. It should pass two parameters to this function. First is a Listener which might get from listener, error :=net.Listen(“tcp”, “localhost:8080”) , and the second is a Handler to handle the response when HTTP connects to the Listener . The Handler is typically nil, and it will use DefaultServeMux which means when receiving an HTTP(or HTTPS) request it will check the path and find the handler function which was already registered in DefaultServeMux (registration through HandleFunc).

func Serve(l net.Listener, handler Handler) error

Here is the sample code for Serve function. We register the handler for /bar through HandleFunc first, start a listener on localhost:8080 , and use Serve to accept an HTTP connection for the listener. When sending HTTP(or HTTPS) requests to localhost:8080/bar , we will see Hello /bar print on the web page.

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
listener, error := net.Listen("tcp", ":8080")
http.Serve(listener, nil)

It is important to run http.HandleFunc before run http.Serve because if the URL path and the handler function(response handle function) weren’t registered in DefaultServeMux, when run http.Serve, the server couldn’t find the related handler function in DefaultServeMux to do the response when receiving an HTTP(or HTTPS) request to a specific URL path.

There is another function in the package http which combine net.Listen and http.Serve function. It is ListenAndServe . We could pass the local network address and handler to ListenAndServe , and it will use TCP network and listen to the local network address, and then calls Serve with the handler to handle requests on incoming connections. Like http.Serve, the handler is typically nil, and it will use DefaultServeMux .

func (srv *Server) ListenAndServe() error

The sample code is like below, and the function will just work the same as the sample code above.

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
http.ListenAndServe(":8080", nil)

Package log provide a logging function. It prints the date and time of each logged message and each log message is output on a separate line. The basic function like Println

func Println(v ...any)

The sample code is like below. It will print out 2022/11/12 15:53:05 haha .

log.Println("haha")

Package log also provides Fatal function which could print some content like errors and exit the program.

log.Fatal(error)

Package fmt provides functions like Sprintf printing content in a specific format. How to use the format could check on fmt online documents.

func Sprintf(format string, a ...any) string

For example, the below code will print 12.00 .

fmt.Sprintf("%6.2f", 12.0)

3. Write our code and run it

After reading the above introduction, we know what packages and functions we could use to build the web server. Our goal now is to build a server host at localhost:8080 and if sending an HTTP request to localhost:8080/greeting , it will return greeting words Hello Worldon the web page, so the code in main.go could be like below.

// main.go

package main

// import the package we need to use
import (
"fmt"
"log"
"net"
"net/http"
)

func main() {

// set a HTTP request handle function for path /greeting and registrate it
http.HandleFunc("/greeting", func (w http.ResponseWriter,
r *http.Request) {

// when receive the request, print the greeting meassage
fmt.Fprint(w, "Hello World")

})

// print out the server is going to start and show the time
log.Println("Starting server....")

// create server at localhost:8080 and using tcp as the network
listener, err := net.Listen("tcp", ":8080")

// if recieve error, record it and exit the program
if err != nil {
log.Fatal(err)
}

// setup HTTP connection for the listener of the server
http.Serve(listener, nil)

}

Now running go run main.go in the terminal at the root of the project, and it will show the log of Starting server.

Then opening localhost:8080/greeting in the web browser. The greeting words Hello word will show on the web page.

4. Refactoring code

We now build a simple web server, but all the code is put together in main function. It is hard to know the purpose of each line of code at first glance, so we could make it clean by separating them into different functions. We could refactor http.HandleFunc by moving the handler function to a Greeting function and just calling this function in http.HandleFunc.

http.HandleFunc("/greeting", func (w http.ResponseWriter, 
r *http.Request) {
fmt.Fprint(w, "Hello World")
})

After doing that, the http.HandleFunc part will become much cleaner.

http.HandleFunc("/greeting", Greeting)

The content of Greeting function is the same as the handler function, but we name it as Greeting .

func Greeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
})

And Greeting function will place in a newly created file called greeting.go in the project root path.

// greeting.go

package main

import (
"fmt"
"net/http"
)

func Greeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello world")
}

The file tree in the project becomes as below

Now Running go run main.go again, but we will get an error message undefined: Greeting

This is because it will only load main.go when run go run main.go , so it couldn’t find Greeting function. To solve this error we should run go run main.go greeting.go instead, so run go run main.go greeting.go again, then it will show the log of Starting server and opening localhost:8080/greeting in the web browser, Hello word will show on the web page.

Now we refactor our code and make it cleaner, but it is a little annoying that we need to call two files when starting the server. There should be a better way to refactor and we still could just run go run main.goto start the web server. We could use one benefit of the Golang project, which is that each file in a different folder is like an individual package, and we could import them into other files to use them. We could create a greeting folder and move greeting.go in it, then define greeting.go as a package greeting . The structure of our project would become like the below picture.

The code in greeting.go is like below. It needs to name the package as greeting , so that we could use the name to call Greeting function. It could also name in another word, but need to use the package name to call the function in the package. For example, if we want to call Greeting function, the way to call it is greeting.Greeting when the package name is greeting but if we name the package as words then it will like words.Greeting .

// greeting.go

package greeting

import (
"fmt"
"net/http"
)

func Greeting(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello World")
}

We need to import the package greeting in main.go . The way to import it is by adding the path of the package greeting in the import list, the path is the combination of the prefix we define in go.mod which is my-project and the folder name greeting, so the path is my-project/greeting . After that, in main function, we could use Greeting function through greeting.Greeting .

// main.go

package main

import (
"log"
"net"
"net/http"
"my-project/greeting" // import greeting the path is like module/package name
)

func main() {
http.HandleFunc("/greeting", greeting.Greeting)

log.Println("Starting server....")

listener, err := net.Listen("tcp", "localhost:8080")
if err != nil {
log.Fatal(err)
}

http.Serve(listener, nil)
}

If we don’t want to call Greeting by greeting.Greeting , we could import greeting with an alias, for example controller , then we could call Greeting by controller.Greeting .

import (
controller "my-project/greeting" // import greeting the path is like module/package name
)

func main() {
http.HandleFunc("/greeting", controller.Greeting)
.......

Now running go run main.go and the web server could start successfully, opening localhost:8080/greeting in the web browser, Hello word also, show on the web page. We don’t need to call greeting.go anymore.

Reference

Go Modules Reference

Learn Go with test

Go official online document

使用Golang打造web 應用程式

Build Web Application with Golang

About go.mod

--

--

icelandcheng
Nerd For Tech

Programming Skill learner and Sharer | Ruby on Rails | Golang | Vue.js | Web Map API