software design
software development

5 ways to create an HTTP server in Golang

Summary. Since Go was released, it has gotten better, and more tools and frameworks created for different purposes that make Go programming and maintenance easier. Choosing the right Golang HTTP framework ... more

Alireza Tanoomandian

2024-09-27

5 ways to create an http server in go-min Go programming language (aka Golang) is one of the most interesting programming languages and is useful for different use cases. By definition, "Go is an open-source programming language that makes it easy to build simple, reliable, and efficient software". It's simple to learn, statically typed and gives you a binary executable as a build result. Since Go was released, it has gotten better, and more tools and frameworks created for different purposes that make Go programming and maintenance easier.

Here we are demonstrating 5 ways to create an HTTP server and precisely explain their pros and cons. In each example we need to do four simple tasks:

  1. Serve static JSON on server /
  2. Handle not-found requests and return a proper response
  3. Recover the server from panic and keep it up and running
  4. Log the requests by the server

Although we can do much more on each case, like connecting to the DB, working with the file system, or adding authorization, it's considered to focus on base implementation and explain them further as needed.

1. net/http go package:

Here is the code:

package main

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

type baseResponse struct {
    Message string                 `json:"message"`
    Data    map[string]interface{} `json:"data"`
}

func handle404Request(w http.ResponseWriter) {
    // set `Content-Type` header
    w.Header().Add("Content-Type", "application/json")

    // set response status code
    w.WriteHeader(http.StatusNotFound)

    // encode response struct to json
    res, _ := json.Marshal(
        baseResponse{
            Message: "not found",
            Data: map[string]interface{}{
                "detail": "requested resource not found",
            },
        },
    )

    fmt.Fprint(w, string(res))

    log.Default().Println("request resource not found")
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet || r.RequestURI != "/" {
        handle404Request(w)
        return
    }

    // encode response struct to json
    res, _ := json.Marshal(
        baseResponse{
            Message: "http server is running with native package",
            Data:    map[string]interface{}{"created_by": "ARTM2000"},
        },
    )

    // set `Content-Type` header
    w.Header().Add("Content-Type", "application/json")

    // set response status code
    w.WriteHeader(http.StatusOK)

    // write the response
    fmt.Fprint(w, string(res))
}

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

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("panic recovered. error:\n", r)
        }
    }()

    log.Default().Println("server listen on port 3000!")
    err := http.ListenAndServe(":3000", nil)
    if err != nil {
        log.Fatalf("server failed to start: %s", err.Error())
    }
}

Let's dive in and explain what's happening. Here we are using the net/http Go native package. We have baseResponse struct that forms our response structure and has the Message and Data fields. We assume that all of our responses should formatted as baseResponse. Then, we have handle404Request and indexHandler. In this way we call handle404Request within the indexHandler as we can not register the request method or the URI pattern and each handler should consider the case.

As you can see on each handler after encoding the baseResponse struct to JSON, we have to manually write the response to the http.ResponseWriter and set the HTTP status code.

In this example, we have no built-in logger or panic recovery and we can implement them in our desired way.

2. fasthttp framework:

Here is the code:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/valyala/fasthttp"
)

type baseResponse struct {
    Message string                 `json:"message"`
    Data    map[string]interface{} `json:"data"`
}

type requestHandler struct {}

func (h *requestHandler) handle404Request(ctx *fasthttp.RequestCtx) {
    ctx.SetContentType("application/json")
    ctx.SetStatusCode(http.StatusNotFound)
    res, _ := json.Marshal(
        baseResponse{
            Message: "not found",
            Data: map[string]interface{}{
                "detail": "request resource not found",
            },
        },
    )
    ctx.SetBody(res)

    log.Default().Println("request resource not found")
}

func (h *requestHandler) indexHandler(ctx *fasthttp.RequestCtx) {
    ctx.SetContentType("application/json")
    ctx.SetStatusCode(http.StatusOK)
    res, _ := json.Marshal(
        baseResponse{
            Message: "http server is running with fasthttp framework",
            Data:    map[string]interface{}{"created_by": "ARTM2000"},
        },
    )
    ctx.SetBody(res)
}

func (h *requestHandler) HandleRequests(ctx *fasthttp.RequestCtx) {
    switch string(ctx.Path()) {
    case "/":
        if ctx.IsGet() {
            h.indexHandler(ctx)
            return
        }
        fallthrough

    default:
        h.handle404Request(ctx)
    }
}

func main() {
    h := requestHandler{}

    defer func() {
        if r := recover(); r != nil {
            fmt.Println("panic recovered. error:\n", r)
        }
    }()

    log.Default().Println("server listen on port 3000!")
    err := fasthttp.ListenAndServe(":3000", h.HandleRequests)
    if err != nil {
        log.Fatalf("server failed to start: %s", err.Error())
    }
}

fasthttp is another implementation of HTTP in Go to handle a real heavy load (i.e. more than a hundred thousand rps). As its document said:

fasthttp was designed for some high performance edge cases. Unless your server/client needs to handle thousands of small to medium requests per second and needs a consistent low millisecond response time fasthttp might not be for you. For most cases, net/http is much better as it's easier to use and can handle more cases. For most cases, you won't even notice the performance difference.

Unlike net/http, the fasthttp doesn't have a built-in router and we have to manually route the request and the URL's pattern with their request methods. So we have a requestHandler struct with a HandleRequests method to act as a hard-coded router. Like the last example we don't have a built-in logger or panic recovery and we have to implement them manually.

3. gin framework:

Here is the code:

package main

import (
    "log"
    "net/http"

    "github.com/gin-gonic/gin"
)

type baseResponse struct {
    Message string                 `json:"message"`
    Data    map[string]interface{} `json:"data"`
}

func handle404Request(ctx *gin.Context) {
    log.Default().Println("request resource not found")
    ctx.JSON(
        http.StatusNotFound,
        baseResponse{
            Message: "not found",
            Data: map[string]interface{}{
                "detail": "request resource not found",
            },
        },
    )
}

func indexHandler(ctx *gin.Context) {
    ctx.JSON(
        http.StatusOK,
        baseResponse{
            Message: "http server is running with gin framework",
            Data:    map[string]interface{}{"created_by": "ARTM2000"},
        },
    )
}

func main() {
    gin.SetMode(gin.ReleaseMode)

    router := gin.New()

    router.Use(gin.Logger())
    router.Use(gin.Recovery())

    router.GET("/", indexHandler)
    router.NoRoute(handle404Request)

    log.Default().Println("server listen on port 3000!")
    err := router.Run(":3000")
    if err != nil {
        log.Fatalf("server failed to start: %s", err.Error())
    }
}

gin framework is one the most popular HTTP frameworks in Go with more than 78K stars at the time of writing this article. It supports built-in routing with HTTP-method declaration on handler registeration, built-in logger and recovery, and some other stuff. As you can see, it's much easier than the explained way due to built-in JSON serialization, setting status, and more. It's also well integrated with other well-known Go packages, such as GORM.

4. fiber framework:

Here is the code:

package main

import (
    "log"
    "net/http"

    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/recover"
)

type baseResponse struct {
    Message string                 `json:"message"`
    Data    map[string]interface{} `json:"data"`
}

func handle404Request(ctx *fiber.Ctx) error {
    return ctx.Status(http.StatusNotFound).JSON(
        baseResponse{
            Message: "not found",
            Data: map[string]interface{}{
                "detail": "request resource not found",
            },
        },
    )
}

func indexHandler(ctx *fiber.Ctx) error {
    return ctx.Status(http.StatusOK).JSON(
        baseResponse{
            Message: "http server is running with fiber framework",
            Data:    map[string]interface{}{"created_by": "ARTM2000"},
        },
    )
}

func main() {
    app := fiber.New(fiber.Config{
        DisableStartupMessage: true,
    })

    app.Use(recover.New(recover.Config{EnableStackTrace: true}))
    app.Use(logger.New())

    app.Get("/", indexHandler)

    // not found routes handler should add at the end to catch any unhandled routes
    app.Use(handle404Request)

    log.Default().Println("server listen on port 3000!")
    err := app.Listen(":3000")
    if err != nil {
        log.Fatalf("server failed to start: %s", err.Error())
    }
}

fiber framework is another popular Go HTTP framework with more than 33K stars at the time of writing this article. Fiber is written on the top of fasthttp which makes it extremely fast and performant. It also has built-in logger and panic recovery as middleware packages that help to better manage your project dependencies size. In addition, so many middlewares for different purposes are available that can easily adopted and configured by your project needs.

As you can see, the same JSON serialization is available and the handler structure looks like gin. Fiber could also easily be integrated into external packages such as GORM or go-redis and any other packages. I recommend having a look at the fiber documentation for more information.

5. echo framework:

Here is the code:

package main

import (
    "log"
    "net/http"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

type baseResponse struct {
    Message string                 `json:"message"`
    Data    map[string]interface{} `json:"data"`
}

func handle404Request(ctx echo.Context) error {
    return ctx.JSON(
        http.StatusNotFound,
        baseResponse{
            Message: "not found",
            Data: map[string]interface{}{
                "detail": "request resource not found",
            },
        },
    )
}

func indexHandler(ctx echo.Context) error {
    return ctx.JSON(http.StatusOK,
        baseResponse{
            Message: "http server is running with echo framework",
            Data:    map[string]interface{}{"created_by": "ARTM2000"},
        },
    )
}

func main() {
    app := echo.New()

    app.Use(middleware.Logger())
    app.Use(middleware.Recover())

    app.GET("/", indexHandler)
    app.RouteNotFound("/*", handle404Request)

    log.Default().Println("server listen on port 3000!")
    app.Logger.Fatal(app.Start(":3000"))
}

echo framework with more than 29K stars at the time is another well-known Go HTTP framework. At first look nothing is new, the syntax looks the same as the gin framework but its documentation is so much better and has so many use cases and examples to get the work done. It also supports HTTP2 by itself.

Comparison

Here's a comparison of the pros and cons of net/http, fasthttp, gin, fiber, and echo:

Framework Pros Cons
net/http - Standard library, widely used and well-documented. - Minimal API, offering flexibility and control. - Built-in support for features like HTTP/2 and TLS. - Can be verbose and require more manual coding. - Limited built-in features compared to other frameworks. - Larger memory footprint compared to some alternatives.
fasthttp - Extremely high performance and low latency. - Minimal API, focusing on raw performance. - Asynchronous request handling for efficient concurrency. - There is a steep learning curve due to its minimal API. - Limited built-in features and community support. - More manual configuration is required for features like middleware and routing.
gin - High performance and efficient. - Clean and concise API with a focus on developer experience. - Built-in support for middleware, routing, and validation. - Extensive documentation and active community. - May lack some advanced features compared to other frameworks. - Limited database integration options.
fiber - Extremely high performance and lightweight. - Modular design with a wide range of plugins available. - Built-in support for routing, middleware, and templating. - Active community and growing ecosystem. - Community may be smaller than some other options.
echo - Highly customizable and extensible framework. - High performance and scalability. - Flexible routing and middleware system. - Support for various template engines and database drivers. - Active community and growing ecosystem. - Can be slightly more complex than Gin for simple use cases. - Documentation may not be as comprehensive as other frameworks.

Conclusion:

Choosing the right framework depends on your specific needs and priorities.

  • net/http: Ideal for simple applications where performance is not a critical concern and you value flexibility and control.
  • fasthttp: Best suited for high-performance applications with demanding latency requirements and resource constraints.
  • gin: A good choice for developers who value simplicity, clean code, and built-in features.
  • fiber: Excellent option for high-performance applications with a preference for modularity and extensive plugin support.
  • echo: Suitable for developers who require a highly customizable and extensible framework with a broad range of features.

Ultimately, the best way to choose is to experiment with each framework and evaluate which aligns best with your project's goals and development style.

If you want to explore more, have a look at the code explained here on my GitHub page.

Alireza Tanoomandian

2024-09-27