How to Build a Basic Server in Go


By Tye Porter

January 29, 2021

In this tutorial, we're going to be taking look at how to build a basic HTTP server in Go using the net/http package.

Prerequisites

  • Basic programming skills and / or working knowledge of Go
  • Basic knowledge on HTTP and CRUD
  • Go version 1.15+

Hello Go Server!

To get started, we'll first look at a simple HTTP server that returns the phrase "Hello Go server!" to the client when a GET request is made to the "/" endpoint:

package main

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

func helloHandler(res http.ResponseWriter, req *http.Request) {
fmt.Fprintf(res, "Hello Go Server!")
}

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

fmt.Println("Starting server on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}

As you can see, writing a HTTP server in Go is pretty simple. As a matter of fact, writing anything in Go feels pretty "simple" compared any other strongly-typed language. Making Go feel simple was an intentional design decision, which you can read more about here: Simplicity is Complicated.


To understand what's going on in this program, let's cover each line.


package main

package main

Go programs are made up of packages, and the main package is where your program starts running.


import

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

We can import other packages using the import keyword followed by the name of the package in quotations marks. Here, we're using a "factored" import statement which has a shorter syntax. Instead of writing the import statement several times, we can write it once and put the names of the packages we need inside parentheses with each package name on a separate line.


func helloHandler()

func helloHandler(res http.ResponseWriter, req *http.Request) {
fmt.Fprintf(res, "Hello Go Server!")
}

An important concept in building HTTP servers with Go is handler functions. Handler functions do the work of responding to the client after a request is made. When building servers using the net/http package, handler functions need to have a specific signature; they need to take a http.ResponseWriter and a pointer to a http.Request. Simply put, the http.ResponseWriter is used to construct and send an HTTP response back to the client and the http.Request contains information about the request sent from the client.


Because our helloHandler() function has the correct signature, it qualifies to be used as a handler function. It has a single statement where we're using the fmt.Fprintf() function to write the string "Hello Go Server!" to the http.ResponseWriter object. The fmt.Fprintf() function works by writing its second argument as a stream of bytes to its first argument which implements the io.Writer interface.


It should be noted that fmt.Fprintf() has a return type of (int, error). In a production application, the values returned should be stored in temporary variables, evaluated and handled properly.


func main()

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

fmt.Println("Starting server on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}

The main() function is where our program starts its execution.


In our main() function we are making a call to http.HandleFunc(). The http.HandleFunc() function registers handler functions for the default router in the net/http package. We are registering our helloHandler() function for the "/" endpoint / pattern.


Before starting our server, we print the line Starting server on port 8080... to the console using the fmt.Println() function. To start our sever, we make a call to http.ListenAndServe(), while passing it the name of the port (:8080) and nil to use the default router.


The http.ListenAndServe() runs a potential endless loop and it always returns a non-nil error, so we handle the error by making a call to log.Fatal(). log.Fatal() is similar to fmt.Print() but it has the additional feature of making a call to os.Exit(1) which terminates the program with a status code of 1.

Running the Server

Now that we understand what's going on in this program, let's open a new terminal window and navigate to the directory where our program file lives. We can now run the program by using the command go run main.go in the terminal. If your file has a different name, replace main.go with the name of your file in the go run command. Once the program starts, we should see the words Starting server on port 8080... in the terminal. Let us now navigate to http://localhost:8080/ in our browser client. We should be presented with the words Hello Go Server! when we navigate to the page.

Building a Basic Calculator

Before building our calculator, let's think about what our basic calculator would do. We'll give it the ability to do four basic mathematical operations – add, subtract, multiply and divide – on two operands and return the result of the calculation.


To get started, let's create a new project folder. Name it whatever you want; I'm going to name my folder go-calculator. Then, create a new file within that newly created folder and name it main.go; this is the file where we'll write our program. Once you've create a main.go file in your project folder, open that file and write in it the following code:

package main

import (
"fmt"
)

func main() {
fmt.Println("Hello Go!")
}

To run this program, open a new terminal window and change directories to your project folder. Run the command go run main.go and you should see the words Hello Go! in your terminal. This program simply uses the fmt.Println() function to print a string to the console.


Writing the Handler Function

That's cool and all but we're trying to build a server that functions as a basic calculator. So let's write a handler function that's able to parse form values through the URL, store and use those values to perform some calculation and return the result of the calculation back to the client:

package main

import (
"fmt"
"strconv"
)

func calculatorHandler(res http.ResponseWriter, req *http.Request) {
// Parse arguments, we have to call this to populate request form values
req.ParseForm()

// Get form values and make sure that they are in the correct format
operation := req.FormValue("operation")
leftOperand, err := strconv.ParseInt(req.FormValue("left"), 10, 64)
rightOperand, err := strconv.ParseInt(req.FormValue("right"), 10, 64)
if err != nil || operation == "" {
fmt.Fprintf(res, "Unable to parse request");
return
}

// Do calculation based on the values provided
switch operation {
case "add":
fmt.Fprintf(res, "%d", leftOperand + rightOperand)
return
case "subtract":
fmt.Fprintf(res, "%d", leftOperand - rightOperand)
return
case "multiply":
fmt.Fprintf(res, "%d", leftOperand * rightOperand)
return
case "divide":
if rightOperand == 0 {
fmt.Fprintf(res, "Division by 0 not possible!")
return
} else {
fmt.Fprintf(res, "%d", leftOperand / rightOperand)
return
}
default:
// Unknown operation...
fmt.Fprintf(res, "Unknown operation!")
return
}

// Some unknown error...
fmt.Fprintf(res, "Unknown error!")
}

func main() {
fmt.Println("Hello Go!")
}

Let's go over each part of our function.


func calculatorHandler()

func calculatorHandler(res http.ResponseWriter, req *http.Request) {
...
}

We first create our function and give it a name of calculatorHandler, and we make sure our function has the correct signature so it can be used as a handler function for our server. Remember, it needs to take a http.ResponseWriter and a pointer to a http.Request.


req.ParseForm()

func calculatorHandler(res http.ResponseWriter, req *http.Request) {
// Parse arguments, we have to call this to populate request form values
req.ParseForm()

...
}

Next, we make a call the ParseForm() method on the req object to populate its form values. Form values are stored as key-value pairs within req object.


req.FormValue()

func calculatorHandler(res http.ResponseWriter, req *http.Request) {
...

// Get form values and make sure that they are in the correct format
operation := req.FormValue("operation")
leftOperand, err := strconv.ParseInt(req.FormValue("left"), 10, 64)
rightOperand, err := strconv.ParseInt(req.FormValue("right"), 10, 64)
if err != nil || operation == "" {
fmt.Fprintf(res, "Unable to parse request");
return
}

...
}

After we populate req's form values, we can then call req.FormValue() to retrieve the individual values provided by the client so we can store them in variables and perform a calculation. We assume the form value keys are named "operation" (for the mathematical operation), "left" (for the left operand), and "right" (for the right operand); and we assume that the values of these keys are a string, a number, and another number respectively. As you can see, we're using the "strconv" package to attempt to convert the values returned from req.FormValue("left") and req.FormValue("right") to integers, and if the client doesn't provide the correct type of data, we return the message "Unable to parse request" to the client and return out of the function.


switch operation

func calculatorHandler(res http.ResponseWriter, req *http.Request) {
...

// Do calculation based on the values provided
switch operation {
case "add":
fmt.Fprintf(res, "%d", leftOperand + rightOperand)
return
case "subtract":
fmt.Fprintf(res, "%d", leftOperand - rightOperand)
return
case "multiply":
fmt.Fprintf(res, "%d", leftOperand * rightOperand)
return
case "divide":
if rightOperand == 0 {
fmt.Fprintf(res, "Division by 0 not possible!")
return
} else {
fmt.Fprintf(res, "%d", leftOperand / rightOperand)
return
}
default:
// Unknown operation...
fmt.Fprintf(res, "Unknown operation!")
return
}

...
}

After storing the operation, the left operand and the right operand in variables, we evaluate the operation. In the case that operation equals to "add", we'll use the fmt.Fprintf() function to add the values in the leftOperand and rightOperand variables and write the result as a string back to the http.ResponseWriter and return out of the function. The rest of the cases in the switch statement are similar with exception to case "divide": and default:.


In the case operation is equal to "divide", we have to make sure the value inside the rightOperand variable is not equal to 0. We know division by zero is an illegal operation, so we write the message "Division by 0 not possible" to the http.ResponseWriter object and return out of the function. If the value inside the operation variable is not equal to any of the cases, the function will come to the default case in which the operation is unknown and we write the message "Unknown operation!" to the http.ResponseWriter object and return out of the function.


Refactoring the main() Function

Let us now refactor the code inside our main() function by registering the calculatorHandler() handler function for the default router using the "net/http" package and making a call to http.ListenAndServe() to start our server.

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

func calculatorHandler(res http.ResponseWriter, req *http.Request) {
// Parse arguments, we have to call this to populate request form values
req.ParseForm()

// Get form values and make sure that they are in the correct format
operation := req.FormValue("operation")
leftOperand, err := strconv.ParseInt(req.FormValue("left"), 10, 64)
rightOperand, err := strconv.ParseInt(req.FormValue("right"), 10, 64)
if err != nil || operation == "" {
fmt.Fprintf(res, "Unable to parse request");
return
}

// Do calculation based on the values provided
switch operation {
case "add":
fmt.Fprintf(res, "%d", leftOperand + rightOperand)
return
case "subtract":
fmt.Fprintf(res, "%d", leftOperand - rightOperand)
return
case "multiply":
fmt.Fprintf(res, "%d", leftOperand * rightOperand)
return
case "divide":
if rightOperand == 0 {
fmt.Fprintf(res, "Division by 0 not possible!")
return
} else {
fmt.Fprintf(res, "%d", leftOperand / rightOperand)
return
}
default:
// Unknown operation...
fmt.Fprintf(res, "Unknown operation!")
return
}

// Some unknown error...
fmt.Fprintf(res, "Unknown error!")
}

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

fmt.Println("Starting server on port 8080...")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}

Running the Server

We can now run our server by running the command go run main.go in the terminal then navigate to the url http://localhost:8080/?operation=add&left=5&right=4 in your browser client. We should see the number 9 on the page. Try changing the form values in the url and see what results you get!

Conclusion

I hope this tutorial was useful in teaching you how to build a web server in Go. Feel free to shoot me a message on Twitter or LinkedIn and let me know if this tutorial helped you!