Simple Golang HTTP server
In this article, I guide you through the process of building a simple HTTP server in Golang using a structured three-layer architecture: router, controllers, and database. My goal was to create a clean and maintainable project while applying key design patterns in Go. I started by initializing a Go module and setting up a GitHub repository to share my code. Then, I implemented a basic server using Go’s net/http package, added graceful shutdown handling, and wrote unit tests to verify the server’s behavior. To make the project more robust, I later integrated the Gin framework for better routing and middleware support. Whether you're just starting with Go or looking to refine your architecture, I hope this guide helps you build with confidence.
Build a Go Project
By simply running the command, you can create a Go module file and start a project.
go mod init go-simpleThe go.mod file will look as follows:
module go-simplego 1.23.2Making a Git repository
Since I want to share my code with others, I can use a Git repository. To do this, I should create a repository on GitHub and commit the initial version of the project.
echo "# go-simple" >> README.mdgit initgit add README.mdgit commit -m "first commit"git branch -M
maingit remote add origin git@github.com:mobintmu/go-simple.gitgit push -u origin mainRepository GitHub Address
This repository will contain and share all of my code.
https://github.com/mobintmu/go-simpleBuild a Golang HTTP server
In this first step, I created a folder structure with a main.go file located in cmd/web. This file initializes a new instance of the application and starts the HTTP server. Since the server runs in a separate goroutine, I implemented a channel to receive a signal when the program should terminate, ensuring proper synchronization and graceful shutdown.
package main
import (
application "go-simple/cmd/web/app"
"os"
"os/signal"
"syscall"
)
func main() {
app := application.New() app.StartServer() // Wait for interrupt signal to gracefully shut down
sig := make(chan os.Signal, 1) signal.Notify(sig, os.Interrupt, syscall.SIGTERM) <-sig // Blocks
until signal is received
}The application file sets up a basic HTTP server that handles incoming requests on port 4000 via the HTTP protocol.
This Go code defines a simple web application using the standard net/http package. It starts by creating an Application struct that holds the server port, and a New() function that initializes the app by checking the PORT environment variable (using a default of :4000 if not set). The port is formatted to include a colon if missing, ensuring it's valid for the server to use.
The StartServer method sets up a basic HTTP router (ServeMux) that routes incoming requests to the appropriate handler—in this case, only the root path / is handled by helloHandler. It starts the server in a separate goroutine so it runs in the background, logging the startup message and any errors. The helloHandler function responds with "Hello World" in plain text for GET requests, and returns a 405 error for any other HTTP method.
package application
import (
"fmt"
"log"
"net/http"
"os"
)
type Application struct {
Port string
}
const defaultPort = ":4000"func New() *Application {
port := os.Getenv("PORT") if port == "" {
port = defaultPort
}
else if port[0] != ':' {
port = ":" + port
}
return &Application{
Port: port
}
}
func (app *Application) StartServer() {
// router mux := http.NewServeMux() mux.HandleFunc("/", helloHandler) //start the server go
func() {
log.Printf("Starting server at port %s", app.Port) err := http.ListenAndServe(app.Port, mux)
if err != nil {
log.Fatal(err)
}
}
()
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprint(w, "Hello World")
}To test the function, it is necessary to create a test file within the test folder. First, a simple server is set up, and then an HTTP request is sent to the endpoint to verify its behavior.
package test
import (
"io"
"net/http"
"testing"
"time" application "go-simple/cmd/web/app"
)
func startTestServer() {
a := application.New() a.StartServer() time.Sleep(200 * time.Millisecond) // Give server time to
start
}
func TestHelloWorldEndpoint(t *testing.T) {
startTestServer() resp, err := http.Get("http://localhost:4000/") if err != nil {
t.Fatalf("Failed to send GET request: %v", err)
}
defer resp.Body.Close() if resp.StatusCode != http.StatusOK {
t.Errorf("Expected status 200 OK, got %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body) if err != nil {
t.Fatalf("Failed to read response body: %v", err)
}
expected := "Hello World" if string(body) != expected {
t.Errorf("Expected body '%s', got '%s'", expected, string(body))
}
}
Run Golang project:
go run cmd/web/main.goGitHub code for this part
Gin Router
In the next step, I will integrate the Gin router to handle routing in my Go project.
Run this in your terminal:
go get -u github.com/gin-gonic/ginAdd a gin router in the app.go file
package application
import (
"log"
"net/http"
"os"
"github.com/gin-gonic/gin"
)
type Application struct {
Port string
Router *gin.Engine
}
const defaultPort = ":4000"func New() *Application {
// Set release mode gin.SetMode(gin.DebugMode) // Use New() for full control, or Default() if
you're okay with built-ins router := gin.New() // Add middleware explicitly router.Use(gin.Logger()) router.Use(gin.Recovery()) port := os.Getenv("PORT") if port == "" { port = defaultPort } return &Application{Port: port, Router: router}}
func (app *Application) StartServer() { //start the server go
func() { log.Printf("Starting Gin server at port %s", app.Port) err := app.Router.Run(app.Port)
if err != nil { log.Fatal(err) } }()}
func (a *Application) Routes() { a.Router.GET("/", helloHandler)}
func helloHandler(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "message": "hello world", })}Current commit on GitHub
🧩 Conclusion :
Building this simple Golang HTTP server was a great exercise in applying clean architecture and exploring both the standard net/http package and the more powerful Gin framework. Starting from scratch—setting up the module, structuring the project, writing tests, and finally integrating Gin—helped me understand how to scale a Go application while keeping it modular and maintainable. Sharing the code on GitHub not only makes it easier for others to learn from or contribute to the project, but also keeps me accountable for writing clean, well-documented code. I’m excited to keep expanding this project, and I hope it serves as a helpful starting point for anyone diving into web development with Go.
