DEV Community

Cover image for Is net/http All You Need, or Does Gin Offer More?
Leapcell
Leapcell

Posted on

Is net/http All You Need, or Does Gin Offer More?

Image description

Leapcell: The Next-Gen Serverless Platform for Golang app Hosting

Why Do We Still Need Gin When We Have net/http?

I. Introduction

In the Go language ecosystem, the net/http package, as the standard HTTP library, possesses powerful and flexible features. It can be used to build web applications and handle HTTP requests. Since this package belongs to the Go language standard library, all Go programs can directly call it. However, given that there is already such a powerful and flexible standard library as net/http, why do third-party libraries like Gin, which assist in building web applications, still emerge?

In fact, this is closely related to the positioning of net/http. The net/http package provides basic HTTP functions, and its design goal focuses on simplicity and generality rather than providing advanced features and a convenient development experience. In the process of handling HTTP requests and building web applications, developers may encounter a series of problems, which is exactly why third-party libraries like Gin were born.

The following will explain the necessity of the Gin framework by elaborating on a series of scenarios and comparing the different implementation methods of net/http and Gin in these scenarios.

II. Handling Complex Routing Scenarios

In the actual development process of web applications, it is extremely common to use the same routing prefix. Here are two relatively typical examples.

When designing an API, over time, the API often needs to be updated and improved. To maintain backward compatibility and allow multiple API versions to coexist, routing prefixes such as /v1, /v2, etc. are usually used to distinguish different versions of the API.

Another scenario is that large web applications are usually composed of multiple modules, with each module responsible for different functions. In order to organize the code more effectively and distinguish the routes of different modules, the module name is often used as the routing prefix.

In the above two scenarios, it is highly likely to use the same routing prefix. If we use net/http to build a web application, its implementation is roughly as follows:

package main

import (
        "fmt"
        "net/http"
)

func handleUsersV1(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "User list in v1")
}

func handlePostsV1(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Post list in v1")
}

func main() {
        http.HandleFunc("/v1/users", handleUsersV1)
        http.HandleFunc("/v1/posts", handlePostsV1)
        http.ListenAndServe(":8080", nil)
}
Enter fullscreen mode Exit fullscreen mode

In the above example, different routing handling functions are defined by manually calling http.HandleFunc. Judging from this code example, there doesn't seem to be an obvious problem, but this is only because there are currently only two routing groups. If the number of routes continues to increase, the number of handling functions will also increase, and the code will become more and more complex and lengthy. Moreover, each routing rule requires manually setting the routing prefix, such as the v1 prefix in the example. If the prefix is in a complex form like /v1/v2/..., the setting process will not only make the code architecture unclear but also be extremely cumbersome and error-prone.

In contrast, the Gin framework implements the routing group function. Here is the implementation code of this function in the Gin framework:

package main

import (
        "fmt"
        "github.com/gin-gonic/gin"
)

func main() {
        router := gin.Default()
        // Create a routing group
        v1 := router.Group("/v1")
        {
                v1.GET("/users", func(c *gin.Context) {
                        c.String(200, "User list in v1")
                })
                v1.GET("/posts", func(c *gin.Context) {
                        c.String(200, "Post list in v1")
                })
        }
        router.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

In the above example, a routing group with the v1 routing prefix is created by using router.Group. When setting the routing rules, there is no need to set the routing prefix again, and the framework will automatically assemble it. At the same time, the rules with the same routing prefix are all maintained in the same code block. Compared with the net/http code library, Gin makes the code structure clearer and easier to manage.

III. Middleware Handling

In the process of handling web application requests, in addition to executing the specific business logic, it is usually necessary to execute some common logic beforehand, such as authentication operations, error handling, or log printing functions, etc. These logics are collectively referred to as middleware handling logic, and they are often indispensable in practical applications.

First, regarding error handling. During the execution of an application, some internal errors may occur, such as database connection failures, file reading errors, etc. Reasonable error handling can prevent these errors from causing the entire application to crash and instead inform the client through an appropriate error response.

For authentication operations, in many web handling scenarios, users usually need to be authenticated before they can access certain restricted resources or perform specific operations. At the same time, authentication operations can also limit user permissions and prevent unauthorized access by users, which helps to improve the security of the program.

Therefore, the complete HTTP request handling logic is very likely to require these middleware handling logics. Theoretically, the framework or library should provide support for middleware logic. Let's first see how net/http implements it:

package main

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

// Error handling middleware
func errorHandler(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                defer func() {
                        if err := recover(); err != nil {
                                http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                                log.Printf("Panic: %v", err)
                        }
                }()
                next.ServeHTTP(w, r)
        })
}

// Authentication middleware
func authMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // Simulate authentication
                if r.Header.Get("Authorization") != "secret" {
                        http.Error(w, "Unauthorized", http.StatusUnauthorized)
                        return
                }
                next.ServeHTTP(w, r)
        })
}

// Handle business logic
func helloHandler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, World!")
}

// Additionally
func anotherHandler(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Another endpoint")
}

func main() {
        // Create a route handler
        router := http.NewServeMux()

        // Apply middleware and register the handler
        handler := errorHandler(authMiddleware(http.HandlerFunc(helloHandler)))
        router.Handle("/", handler)

        // Apply middleware and register the handler for another request
        another := errorHandler(authMiddleware(http.HandlerFunc(anotherHandler)))
        router.Handle("/another", another)

        // Start the server
        http.ListenAndServe(":8080", router)
}
Enter fullscreen mode Exit fullscreen mode

In the above example, in net/http, the error handling and authentication functions are implemented through the two middleware errorHandler and authMiddleware. Looking at line 49 of the example code, we can see that the code uses the decorator pattern to add error handling and authentication operation functions to the original handler. The advantage of this code implementation is that by combining multiple handling functions through the decorator pattern to form a handler chain, the error handling and authentication functions are achieved without adding this part of the logic in each handling function handler, which improves the readability and maintainability of the code.

However, there is also a significant drawback here, that is, this function is not directly provided by the framework but implemented by the developer themselves. Every time a new handling function handler is added, it is necessary to decorate it and add error handling and authentication operations, which not only increases the burden on the developer but also is error-prone. Moreover, as the requirements continue to change, it is possible that some requests only need error handling, some requests only need authentication operations, and some requests need both error handling and authentication operations. Based on this code structure, the maintenance difficulty will become greater and greater.

In contrast, the Gin framework provides a more flexible way to enable and disable middleware logic. It can be set for a certain routing group without the need to set each routing rule separately. The following shows the example code:

package main

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

func authMiddleware() gin.HandlerFunc {
        return func(c *gin.Context) {
                // Simulate authentication
                if c.GetHeader("Authorization") != "secret" {
                        c.AbortWithStatusJSON(401, gin.H{"error": "Unauthorized"})
                        return
                }
                c.Next()
        }
}

func main() {
        router := gin.Default()
        // Add Logger and Recovery middleware globally
        // Create a routing group, and all routes in this group will apply the authMiddleware middleware
        authenticated := router.Group("/")
        authenticated.Use(authMiddleware())
        {
                authenticated.GET("/hello", func(c *gin.Context) {
                        c.String(200, "Hello, World!")
                })

                authenticated.GET("/private", func(c *gin.Context) {
                        c.String(200, "Private data")
                })
        }

        // Not in the routing group, so the authMiddleware middleware is not applied
        router.GET("/welcome", func(c *gin.Context) {
                c.String(200, "Welcome!")
        })

        router.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

In the above example, a routing group named authenticated is created through router.Group("/"), and then the Use method is used to enable the authMiddleware middleware for this routing group. All routing rules under this routing group will automatically execute the authentication operation implemented by authMiddleware.

Compared with net/http, the advantages of Gin are as follows. First, there is no need to decorate each handler to add middleware logic. Developers only need to focus on business logic development, which reduces the development burden. Second, it has higher maintainability. If the business no longer requires authentication operations, in Gin, only the call to the Use method needs to be deleted; while in net/http, it is necessary to handle the decoration operations of all handlers and delete the authentication operation nodes in the decorator nodes, which is a large amount of work and error-prone. Finally, in the scenario where different parts of requests need to use different middleware, Gin is more flexible and easier to implement. For example, some requests need authentication operations, some requests need error handling, and some requests need both error handling and authentication operations. In this scenario, only three routing groups need to be created through Gin, and then different routing groups call the Use method respectively to enable different middleware, which can meet the requirements. This is more flexible and easier to maintain compared with net/http. This is also one of the important reasons why the Gin framework appears even though there is already net/http.

IV. Data Binding

When handling HTTP requests, a common function is to automatically bind the data in the request to a structure. Taking form data as an example, the following shows how to bind the data to a structure if net/http is used:

package main

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

type User struct {
        Name  string `json:"name"`
        Email string `json:"email"`
}

func handleFormSubmit(w http.ResponseWriter, r *http.Request) {
        var user User

        // Bind the form data to the User structure
        user.Name = r.FormValue("name")
        user.Email = r.FormValue("email")

        // Process the user data
        fmt.Fprintf(w, "user has been created:%s (%s)", user.Name, user.Email)
}

func main() {
        http.HandleFunc("/createUser", handleFormSubmit)
        http.ListenAndServe(":8080", nil)
}
Enter fullscreen mode Exit fullscreen mode

In this process, it is necessary to call the FormValue method to read the data from the form one by one and then set it into the structure. When there are many fields, it is very likely to miss some fields, resulting in problems in the subsequent processing logic. Moreover, each field needs to be manually read and set, which seriously affects the development efficiency.

Next, let's see how Gin reads the form data and sets it into the structure:

package main

import (
        "fmt"
        "github.com/gin-gonic/gin"
)

type User struct {
        Name  string `json:"name"`
        Email string `json:"email"`
}

func handleFormSubmit(c *gin.Context) {
        var user User

        // Bind the form data to the User structure
        err := c.ShouldBind(&user)
        if err != nil {
                c.JSON(http.StatusBadRequest, gin.H{"error": "error form"})
                return
        }

        // Process the user data
        c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("user has been created:%s (%s)", user.Name, user.Email)})
}

func main() {
        router := gin.Default()
        router.POST("/createUser", handleFormSubmit)
        router.Run(":8080")
}
Enter fullscreen mode Exit fullscreen mode

Observing line 17 of the above example code, it can be found that by directly calling the ShouldBind function, the form data can be automatically mapped to the structure without the need to read each field one by one and set it into the structure separately. Compared with using net/http, the Gin framework is more convenient in terms of data binding and less error-prone. Gin provides various APIs that can map various types of data to structures, and users only need to call the corresponding APIs. However, net/http does not provide such operations, and users need to read the data themselves and manually set it into the structure.

V. Conclusion

In the Go language, although net/http provides basic HTTP functions, its design goal focuses on simplicity and generality rather than providing advanced features and a convenient development experience. When handling HTTP requests and building web applications, net/http is insufficient in the face of complex routing rules; for some common operations such as logging and error handling, it is difficult to achieve a pluggable design; in terms of binding request data to structures, net/http does not provide convenient operations, and users need to implement them manually.

This is why third-party libraries like Gin appear. Gin is built on top of net/http and aims to simplify and accelerate the development of web applications. Overall, Gin can help developers build web applications more efficiently, providing a better development experience and richer functions. Of course, whether to choose to use net/http or Gin depends on the scale, requirements, and personal preferences of the project. For simple small projects, net/http may be sufficient; but for complex applications, Gin may be more suitable.

Leapcell: The Next-Gen Serverless Platform for Golang app Hosting

Finally, I recommend a platform that is most suitable for deploying Go services: Leapcell

Image description

1. Multi-Language Support

  • Develop with JavaScript, Python, Go, or Rust.

2. Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

3. Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

4. Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

5. Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Image description

Explore more in the documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Top comments (0)