Hello World server

👨‍🏫 Before we start…

  • Go comes with the net/http package, which provides HTTP client and server implementations. So,
  • We’ll start with the examples of the standard library documentation.
  • Then, we will Dockerize and rearrange the files with an idiomatic project structure.
  • ⭐ We use myapp as the project name/ project root folder name.

ListenAndServe

📖 ListenAndServe listens on the TCP network address addr and then calls Serve with handler to handle requests on incoming connections. Accepted connections are configured to enable TCP keep-alives.

Let’s save this code under main.go.

package main

import (
	"io"
	"net/http"
)

func main() {
	http.HandleFunc("/hello", hello)
	http.ListenAndServe(":8080", nil)
}

func hello (w http.ResponseWriter, req *http.Request) {
	io.WriteString(w, "Hello, world!")
}
  • Use go run ./main.go command, to run it locally.
  • You should see Hello, world! text while visit localhost:8080/hello in the browser.

NewServeMux

📖 ServeMux 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. NewServeMux allocates and returns a new ServeMux.

💡 Because, default ServeMux is very limited and not very performant.

Let’s update the main.go with the following code.

package main

import (
	"io"
	"net/http"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", hello)

	http.ListenAndServe(":8080", mux)
}

func hello(w http.ResponseWriter, req *http.Request) {
	io.WriteString(w, "Hello, world!")
}
  • Same way, use the go run ./main.go command, to run it locally.
  • You should see the same response, Hello, world! text while visit localhost:8080/hello in the browser.

Server

📖 Server A Server defines parameters for running an HTTP server.

Let’s update the main.go and see how we can change default timeout configs of the Server.

package main

import (
	"io"
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", hello)

	s := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  2 * time.Second,
		WriteTimeout: 2 * time.Second,
		IdleTimeout:  5 * time.Second,
	}

	s.ListenAndServe()
}

func hello(w http.ResponseWriter, req *http.Request) {
	io.WriteString(w, "Hello, world!")
}
  • You should see the same response, Hello, world! in the browser.
  • To verify the functionality, you can use time.Sleep() function and after 2 seconds the server will automatically break the connection.
func hello(w http.ResponseWriter, req *http.Request) {
	time.Sleep(3 * time.Second)
	io.WriteString(w, "Hello, world!")
}

Logs and error handling

Let’s update the main.go to see how we can add some logs and handle the basic errors of the above code.

package main

import (
	"io"
	"log"
	"net/http"
	"time"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/hello", hello)

	s := &http.Server{
		Addr:         ":8080",
		Handler:      mux,
		ReadTimeout:  2 * time.Second,
		WriteTimeout: 2 * time.Second,
		IdleTimeout:  5 * time.Second,
	}

	log.Println("Starting server :8080")
	if err := s.ListenAndServe(); err != nil && err != http.ErrServerClosed {
		log.Fatal("Server startup failed")
	}
}

func hello(w http.ResponseWriter, r *http.Request) {
	io.WriteString(w, "Hello, world!")
}

Dockerfile

📖 Docker is a platform for developers and sysadmins to develop, deploy, and run applications with containers. A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. 🔍 If you are a newcomer to Docker, I recommend you to read What is a Container? article and its Get Started guild on its official documentation and install the Docker.

Let’s add the Dockerfile. We use official Go Docker alpine image to test our server.

FROM golang:alpine

WORKDIR /myapp
COPY . .

RUN go build main.go

CMD ["/myapp/main"]
EXPOSE 8080
  • To build the Docker image: docker build -t myapp .
  • To run: docker run -dp 8080:8080 myapp
  • You should see the same response, Hello, world! text while visit localhost:8080/hello in the browser.

💡 Docker generates a random name for our container, unless we set a custom name via the --name flag. We can use docker ps command to see more info. To stop the container docker stop <name> and to remove the image docker rmi -f <image-id>.

docker-compose.yml

📖 Docker Compose is a tool for defining and running multi-container Docker applications. With a single command, we can create and start all the services according to the content in the docker-compose.yml file. 🔍 If you are new to Docker Compose, I recommend you to read its Get Started guild on its official documentation.

Let’s add the docker-compose.yml.

version: '3'
services:

  app:
    build: .
    ports:
      - "8080:8080"
  • You can use docker-compose build and docker-compose up commands, to build and run the application.
  • You should see the same response, Hello, world! text while visit localhost:8080/hello in the browser.
  • Use the docker-compose down command to stop the application.

An idiomatic structure

Let’s arrange the files to follow an idiomatic project structure.

1. go.mod

📖 A Go module is a collection of related Go packages that are versioned together as a single unit. Most often, a version control repository contains exactly one module defined in the repository root. (Multiple modules are supported in a single repository, but typically that would result in more work on an on-going basis than a single module per repository).

You can create the go.mod by running go mod init myapp. It will generate the go.mod file in the project root with the module name and the Go version of your system.

module myapp

go 1.19

2. cmd and bin folders

It’s common to have multiple buildable binaries in a single project. Even in our project, we will add another executable to run the database migrations. It’s a good practice to store all these buildable binaries as subfolders inside the cmd folder and store all binaries inside the bin folder

  • First, let’s move the main.go file to cmd/api/main.go

  • Then, let’s update the Dockerfile to update the paths.

FROM golang:alpine

WORKDIR /myapp
COPY . .

RUN go build -o ./bin/api ./cmd/api

CMD ["/myapp/bin/api"]
EXPOSE 8080

By changing RUN go build main.go to RUN go build -o ./bin/api ./cmd/api, we have updated few important things.

  1. While running go build, we target /cmd/api directory instead of main.go file.
    • So, we can divide the code into multiple files if needed.
  2. We store the compiled binary inside the bin folder.
    • Adding the -o flag is necessary because the bin folder needs to create with the binary.

📁 Final project structure

myapp
├── cmd
│  └── api
│     └── main.go
├── go.mod
├── docker-compose.yml
└── Dockerfile

👨‍🏫 What’s next…

In the next article, we’ll connect a database and add database migrations to our application.