Introduction
Gin is a HTTP web framework written in Go (Golang). It features a Martini-like API, but with performance up to 40 times faster than Martini. If you need smashing performance, get yourself some Gin.
The official website of Gin introduces itself as a web framework with "high performance" and "good productivity". It also mentions two other libraries. The first one is Martini, which is also a web framework and has a name of a liquor. Gin says it uses its API, but is 40 times faster. Using httprouter
is an important reason why it can be 40 times faster than Martini.
Among the "Features" on the official website, eight key features are listed, and we will gradually see the implementation of these features later.
- Fast
- Middleware support
- Crash-free
- JSON validation
- Routes grouping
- Error management
- Rendering built-in/Extendable
Start with a Small Example
Let's look at the smallest example given in the official documentation.
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
Run this example, and then use a browser to visit http://localhost:8080/ping
, and you will get a "pong".
This example is very simple. It can be split into only three steps:
- Use
gin.Default()
to create anEngine
object with default configurations. - Register a callback function for the "/ping" address in the
GET
method of theEngine
. This function will return a "pong". - Start the
Engine
to start listening to the port and providing services.
HTTP Method
From the GET
method in the above small example, we can see that in Gin, the processing methods of HTTP methods need to be registered using the corresponding functions with the same names.
There are nine HTTP methods, and the four most commonly used ones are GET
, POST
, PUT
, and DELETE
, which correspond to the four functions of querying, inserting, updating, and deleting respectively. It should be noted that Gin also provides the Any
interface, which can directly bind all HTTP method processing methods to one address.
The returned result generally contains two or three parts. The code
and message
are always there, and data
is generally used to represent additional data. If there is no additional data to return, it can be omitted. In the example, 200 is the value of the code
field, and "pong" is the value of the message
field.
Create an Engine Variable
In the above example, gin.Default()
was used to create the Engine
. However, this function is a wrapper for New
. In fact, the Engine
is created through the New
interface.
func New() *Engine {
debugPrintWARNINGNew()
engine := &Engine{
RouterGroup: RouterGroup{
//... Initialize the fields of RouterGroup
},
//... Initialize the remaining fields
}
engine.RouterGroup.engine = engine // Save the pointer of the engine in RouterGroup
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
Just take a brief look at the creation process for now, and don't focus on the meanings of various member variables in the Engine
structure. It can be seen that in addition to creating and initializing an engine
variable of type Engine
, New
also sets engine.pool.New
to an anonymous function that calls engine.allocateContext()
. The function of this function will be discussed later.
Register Route Callback Functions
There is an embedded struct RouterGroup
in the Engine
. The interfaces related to HTTP methods of the Engine
are all inherited from RouterGroup
. The "Routes grouping" in the feature points mentioned on the official website is achieved through the RouterGroup
struct.
type RouterGroup struct {
Handlers HandlersChain // Processing functions of the group itself
basePath string // Associated base path
engine *Engine // Save the associated engine object
root bool // root flag, only the one created by default in Engine is true
}
Each RouterGroup
is associated with a base path basePath
. The basePath
of the RouterGroup
embedded in the Engine
is "/".
There is also a set of processing functions Handlers
. All requests under the paths associated with this group will additionally execute the processing functions of this group, which are mainly used for middleware calls. Handlers
is nil
when the Engine
is created, and a set of functions can be imported through the Use
method. We will see this usage later.
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
The handle
method of RouterGroup
is the final entry for registering all HTTP method callback functions. The GET
method and other methods related to HTTP methods called in the initial example are just wrappers for the handle
method.
The handle
method will calculate the absolute path according to the basePath
of the RouterGroup
and the relative path parameter, and at the same time call the combineHandlers
method to get the final handlers
array. These results are passed as parameters to the addRoute
method of the Engine
to register the processing functions.
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
assert1(finalSize < int(abortIndex), "too many handlers")
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}
What the combineHandlers
method does is to create a slice mergedHandlers
, then copy the Handlers
of the RouterGroup
itself into it, then copy the handlers
of the parameters into it, and finally return mergedHandlers
. That is to say, when registering any method using handle
, the actual result includes the Handlers
of the RouterGroup
itself.
Use Radix Tree to Accelerate Route Retrieval
In the "Fast" feature point mentioned on the official website, it is mentioned that the routing of network requests is implemented based on the radix tree (Radix Tree). This part is not implemented by Gin, but by httprouter
mentioned in the introduction of Gin at the beginning. Gin uses httprouter
to achieve this part of the function. The implementation of the radix tree will not be mentioned here for the time being. We will only focus on its usage for now. Maybe we will write a separate article about the implementation of the radix tree later.
In the Engine
, there is a trees
variable, which is a slice of the methodTree
structure. It is this variable that holds references to all radix trees.
type methodTree struct {
method string // Name of the method
root *node // Pointer to the root node of the linked list
}
The Engine
maintains a radix tree for each HTTP method. The root node of this tree and the name of the method are saved together in a methodTree
variable, and all methodTree
variables are in trees
.
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
//... Omit some code
root := engine.trees.get(method)
if root == nil {
root = new(node)
root.fullPath = "/"
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
//... Omit some code
}
It can be seen that in the addRoute
method of the Engine
, it will first use the get
method of trees
to get the root node of the radix tree corresponding to the method
. If the root node of the radix tree is not obtained, it means that no method has been registered for this method
before, and a tree node will be created as the root node of the tree and added to trees
.
After getting the root node, use the addRoute
method of the root node to register a set of processing functions handlers
for the path path
. This step is to create a node for path
and handlers
and store it in the radix tree. If you try to register an already registered address, addRoute
will directly throw a panic
error.
When processing an HTTP request, it is necessary to find the value of the corresponding node through the path
. The root node has a getValue
method responsible for handling the query operation. We will mention this when talking about Gin processing HTTP requests.
Import Middleware Processing Functions
The Use
method of RouterGroup
can import a set of middleware processing functions. The "Middleware support" in the feature points mentioned on the official website is achieved through the Use
method.
In the initial example, when creating the Engine
struct variable, New
was not used, but Default
was used. Let's take a look at what Default
does extra.
func Default() *Engine {
debugPrintWARNINGDefault() // Output log
engine := New() // Create object
engine.Use(Logger(), Recovery()) // Import middleware processing functions
return engine
}
It can be seen that it is a very simple function. In addition to calling New
to create the Engine
object, it only calls Use
to import the return values of two middleware functions, Logger
and Recovery
. The return value of Logger
is a function for logging, and the return value of Recovery
is a function for handling panic
. We will skip this for now and look at these two functions later.
Although the Engine
embeds RouterGroup
, it also implements the Use
method, but it is just a call to the Use
method of RouterGroup
and some auxiliary operations.
func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers()
engine.rebuild405Handlers()
return engine
}
func (group *RouterGroup) Use(middleware...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
It can be seen that the Use
method of RouterGroup
is also very simple. It just adds the middleware processing functions of the parameters to its own Handlers
through append
.
Start Running
In the small example, the last step is to call the Run
method of the Engine
without parameters. After the call, the entire framework starts running, and visiting the registered address with a browser can correctly trigger the callback.
func (engine *Engine) Run(addr...string) (err error) {
//... Omit some code
address := resolveAddress(addr) // Parse the address, the default address is 0.0.0.0:8080
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine.Handler())
return
}
The Run
method only does two things: parse the address and start the service. Here, the address actually only needs to pass a string, but in order to achieve the effect of being able to pass or not pass, a variadic parameter is used. The resolveAddress
method handles the results of different situations of addr
.
Starting the service uses the ListenAndServe
method in the net/http
package of the standard library. This method accepts a listening address and a variable of the Handler
interface. The definition of the Handler
interface is very simple, with only one ServeHTTP
method.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Because the Engine
implements ServeHTTP
, the Engine
itself will be passed to the ListenAndServe
method here. When there is a new connection to the monitored port, ListenAndServe
will be responsible for accepting and establishing the connection, and when there is data on the connection, it will call the ServeHTTP
method of the handler
for processing.
Process Messages
The ServeHTTP
of the Engine
is the callback function for processing messages. Let's take a look at its content.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
The callback function has two parameters. The first one is w
which is used to receive the request reply. Write the reply data to w
. The other is req
which holds the data of this request. All data required for subsequent processing can be read from req
.
The ServeHTTP
method does four things. First, get a Context
from the pool
pool, then bind the Context
to the parameters of the callback function, then call the handleHTTPRequest
method with the Context
as a parameter to process this network request, and finally put the Context
back into the pool.
Let's first only look at the core part of the handleHTTPRequest
method.
func (engine *Engine) handleHTTPRequest(c *Context) {
//... Omit some code
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method!= httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, c.skippedNodes, unescape)
//... Omit some code
if value.handlers!= nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
//... Omit some code
}
//... Omit some code
}
The handleHTTPRequest
method mainly does two things. First, get the previously registered method from the radix tree according to the address of the request. Here, the handlers
will be assigned to the Context
for this processing, and then call the Next
function of the Context
to execute the methods in the handlers
. Finally, write the return data of this request in the responseWriter
type object of the Context
.
Context
When processing an HTTP request, all context-related data are in the Context
variable. The author also wrote in the comment of the Context
struct that "Context is the most important part of gin", which shows its importance.
When talking about the ServeHTTP
method of the Engine
above, it can be seen that the Context
is not directly created, but obtained through the Get
method of the pool
variable of the Engine
. After being taken out, its state is reset before use, and it is put back into the pool after use.
The pool
variable of the Engine
is of type sync.Pool
. For now, just know that it is an object pool provided by the Go official that supports concurrent use. You can get an object from the pool through its Get
method, and you can also put an object into the pool using the Put
method. When the pool is empty and the Get
method is used, it will create an object through its own New
method and return it.
This New
method is defined in the New
method of the Engine
. Let's take another look at the New
method of the Engine
.
func New() *Engine {
//... Omit other code
engine.pool.New = func() any {
return engine.allocateContext()
}
return engine
}
It can be seen from the code that the creation method of the Context
is the allocateContext
method of the Engine
. There is no mystery in the allocateContext
method. It just does two-step pre-allocation of slice lengths, then creates the object and returns it.
func (engine *Engine) allocateContext() *Context {
v := make(Params, 0, engine.maxParams)
skippedNodes := make([]skippedNode, 0, engine.maxSections)
return &Context{engine: engine, params: &v, skippedNodes: &skippedNodes}
}
The Next
method of the Context
mentioned above will execute all the methods in the handlers
. Let's take a look at its implementation.
func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}
Although handlers
is a slice, the Next
method is not simply implemented as a traversal of handlers
, but introduces a processing progress record index
, which is initialized to 0, incremented at the beginning of the method, and incremented again after a method execution is completed.
The design of Next
has a great relationship with its usage, mainly to cooperate with some middleware functions. For example, when a panic
is triggered during the execution of a certain handler
, the error can be caught using recover
in the middleware, and then Next
can be called again to continue executing the subsequent handlers
without affecting the entire handlers
array due to the problem of one handler
.
Handle Panic
In Gin, if the processing function of a certain request triggers a panic
, the entire framework will not directly crash. Instead, an error message will be thrown, and the service will continue to be provided. It is somewhat similar to how Lua frameworks usually use xpcall
to execute message processing functions. This operation is the "Crash-free" feature point mentioned in the official documentation.
As mentioned above, when using gin.Default
to create an Engine
, the Use
method of the Engine
will be executed to import two functions. One of them is the return value of the Recovery
function, which is a wrapper of other functions. The final called function is CustomRecoveryWithWriter
. Let's take a look at the implementation of this function.
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
//... Omit other code
return func(c *Context) {
defer func() {
if err := recover(); err!= nil {
//... Error handling code
}
}()
c.Next() // Execute the next handler
}
}
We don't focus on the details of error handling here, but only look at what it does. This function returns an anonymous function. In this anonymous function, another anonymous function is registered using defer
. In this inner anonymous function, recover
is used to catch the panic
, and then error handling is performed. After the handling is completed, the Next
method of the Context
is called, so that the handlers
of the Context
that were originally being executed in sequence can continue to be executed.
Leapcell: The Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis
Finally, let me introduce the best platform for deploying Gin services: Leapcell.
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.
Explore more in the Documentation!
Leapcell Twitter: https://x.com/LeapcellHQ
Top comments (0)