Alireza Tanoomandian
2024-09-27
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:
/
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.
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.
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.
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.
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.
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.
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. |
Choosing the right framework depends on your specific needs and priorities.
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