👨🏫 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 usedocker ps
command to see more info. To stop the containerdocker stop <name>
and to remove the imagedocker 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
anddocker-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 tocmd/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.
- While running
go build
, we target/cmd/api
directory instead ofmain.go
file.- So, we can divide the code into multiple files if needed.
- We store the compiled binary inside the
bin
folder.- Adding the
-o
flag is necessary because thebin
folder needs to create with the binary.
- Adding the
📁 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.