Web Services: Gin framework

Posted by justAnoob on Fri, 14 Jan 2022 20:01:46 +0100

brief introduction

Gin framework is a Web framework written in Go language and encapsulated based on net/http package.

The routing function of Gin core is through the customized version HttpRouter It has high routing performance.

Basic functions of Web Services

1. HTTP / HTTPS support

Because Gin framework is a Web framework encapsulated based on net/http package, it naturally supports HTTP / HTTPS.

Start an HTTP service by:

insecureServer := &http.Server {
    Addr :         ":8080", 
    Handle:        router(),
    ReadTimeout:   5 * time.Second,
    WriteTimeout:  10 * time.Second,
}

...

err := insecureServer.ListenAndServe()

Start an HTTPS service by:

secureServer := &http.Server {
    Addr:         ":8443",
    Handler:      router(),
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
}

...

err := secureServer.ListenAndServeTLS("server.pem", "server.key")

2. Route matching

Gin framework supports two routing matching rules: exact matching and fuzzy matching.

2.1 exact matching

For example, the route is / products/:name

The matching is as follows:

routeMatching situation
/products/iphone12
/products/xiaomi8
/products/xiaomi8/music×
/products/×

2.2 fuzzy matching

For example, the route is / products/*name

The matching is as follows:

3. Routing packet

Gin realizes the function of routing packets through the Group function.

(1) Routes of the same version can be grouped.

(2) Routes of the same RESTful resources can be grouped.

(3) Through routing packets, the routes of the same packets can be processed uniformly.

// For unified processing, add gin to all routes belonging to v1 packets Basicauth middleware to realize authentication function.
// v1 grouping
v1 := router.Group("/v1", gin.BasicAuth(gin.Accounts{"foo": "var", "admin": "pass"})) {
    // Route matching
    productv1 := v1.Group("/products") {
        productv1.POST("", productHandler.Create)
        productv1.GET(":name", productHandler.Get)
    }

    // Route matching
    orderv1 := v1.Group("/orders") {
        orderv1.POST("", orderHandler.Create)
        orderv2.GET(":name", orderHandler.Get)
    }
}

// v2 grouping
v2 := router.Group("/v2", gin.BasicAuth(gin.Accounts{"foo": "var", "admin": "pass"})) {
    productv2.POST("", productHandler.Create)
    productv2.GET(":name", productHandler.Get)
}

4. One process and multiple services

The following code implements two same services, listening on different ports respectively.

Note that in order to start the second service without blocking, you need to execute the ListenAndServe function in goroutine and call eg.Wait() to block the program process, so that the two HTTP services can continuously listen to the port in goroutine and provide services.

var eg errgroup.Group
insecureServe := &http.Server{...}
secureServe := &http.Server{...}

eg.Go(func() error {
    err := insecureServer.ListenAndServe()
    if err := nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
    return err
})


eg.Go(func() error {
    err := secureServer.ListenAndServeTLS("server.pem", "server.key")
    if err != nil && err != http.ErrServerClosed {
        log.Fatal(err)
    }
    return err
})

if err := eg.Wait(); err != nil {
    log.Fatal(err)
}

5. Parameter analysis, parameter verification, logic processing and return results

A Web service should have four functions: parameter parsing, parameter verification, logical processing and return results.

HTTP request parameters can exist in different locations. How does Gin parse them?

5.1 HTTP has the following five parameter types

  • Path parameter (path)

For example: / user/:name

Where name is the path parameter.

  • Query string parameter (query)

For example: / welcome? firstname=tian&lastname=dh

Among them, firstname and lastname are query string parameters.

  • Form parameters (form)

For example: curl -X POST -F 'username=tian' -F 'password=123456' http://domain.com/login

Where username and password are form parameters.

  • HTTP header parameter (header)

For example: curl - x post - H 'content type: application / JSON' - D '{username ":" admin "," password ":" 123456 "}' http://domain.com/login

Where content type is the HTTP header parameter.      

  • Message body parameter (body)

For example: curl - x post - H 'content type: application / JSON' - D '{username ":" admin "," password ":" 123456 "}' http://domain.com/login

Where username and password are message body parameters.

5.2 parameter analysis

Method 1: directly read the value of a parameter

Use the c.Param() function

Method 2: bind similar HTTP parameters to a Go structure

When binding parameters, Gin determines which type of parameters to bind to the structure through the tag of the structure.

gin.Default().GET("/:name/:id", nil)

// Mode 1
name := c.Param("name")

// Mode II
type Person struct {
    ID   string  `uri: "id"    binding: "required,uuid"`
    Name string  `uri: "name"  binding: "required"`
}

if err := c.ShouldBindUri(&person); err != nil {
    // normal code
    return
}

Different HTTP parameters have different structures tag:

  • Path parameter: uri
  • Query string parameter: form
  • Form parameters: form
  • HTTP header parameter: header
  • Message body parameter: select json or xml automatically according to the content type; You can also call ShouldBindJSON or ShouldBindXML to specify which tag to use.

 

For each parameter type, Gin has corresponding functions to obtain and bind these parameters.

These functions are encapsulated based on ShouldBindWith and MustBindWith:

(1)ShouldBindWith(obj interface{}, b binding.Binding) error

The bottom layer of many ShouldBindXXX functions calls ShouldBindWith function to complete parameter binding.

This function will bind the parameters to the passed structure pointer according to the passed binding engine.

If the binding fails, only the error content is returned, but the HTTP request is not terminated.

ShouldBindWith supports multiple binding engines, such as:

                binding.JSON,binding.Query,binding.Uri,binging.Header, etc.

For more details, please refer to binding.go

(2)MustBindWith(obj interface{}, b binding.Binding) error

The bottom layer of many BindXXX functions calls MustBindWith function to complete parameter binding.

This function will bind the parameters to the passed structure pointer according to the passed binding engine.

If the binding fails, an error is returned and the request is terminated, and an HTTP 400 error is returned.

The binding engine supported is the same as the ShouldBindWith function.

Gin derives many new Bind functions based on ShouldBindWith and MustBindWith.

These functions can meet the requirements of obtaining HTTP parameters in different scenarios.

The function provided by Gin can obtain five categories of HTTP parameters.

(1) Path parameters

        ShouldBindUri

        BindUri

(2) Query string parameters

        ShouldBindQuery

        BindQuery

(3) Form parameters

        ShouldBind

(4) HTTP header parameters

        ShouldBindHeader

        BindHeader

(5) Message body parameters

        ShouldBindJSON

        BindJSON

Note: Gin does not provide functions such as ShouldBindForm and BindForm to bind form parameters.

When the HTTP method is GET, ShouldBind only binds Query type parameters;

When the HTTP method is POST, first check whether the content type is json or xml. If not, bind the parameters of Form type.

Therefore, ShouldBind can bind Form type parameters, but only if the HTTP method is POST and the content type is not application/json or application/xml.

It is recommended to use ShouldBindXXX to ensure that the set HTTP Chain (which can be understood as a series of processing plug-ins for HTTP requests) can continue to be executed.

5.3 examples

func (u *productHandler) Create (c *gin.Context) {
    u.Lock()
    defer u.Unlock()

    // 1. Parameter analysis
    var product Product
    if err := c.ShouldBindJSON(&product); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    }

    // 2. Parameter verification
    if _, ok := u.products[product.Name]; ok {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": fmt.Sprintf("product %s already exist", product.Nme)})
        return
    }
    product.CreatedAt = time.Now()

    // 3. Logic processing
    u.products[product.Name] = product
    log.Printf("Register product %s success", product.Name)

    // 4. Return results
    c.JSON(http.StatusOK, product)
}

Web services advanced features

1. Middleware

1.1 INTRODUCTION

Gin supports middleware. HTTP requests will be processed by a series of loaded middleware before being forwarded to the actual processing function.

1.2 use

In the middleware, you can parse HTTP requests and do some logical processing. For example: cross domain processing, or generate X-Request-ID and save it in the context to track a request.

After processing, (1) you can choose to interrupt and return the request. (2) You can also continue to forward the request to the next middleware for processing.

After all middleware processes are completed, the request will be transferred to the routing function for processing.

1.3 advantages and disadvantages

Through middleware, we can handle all requests uniformly, improve development efficiency and make our code more concise.

However, because all requests need to be processed by the middleware, the request delay may be increased.

For middleware features, the following suggestions are made:

  • The middleware is made loadable, and the middleware loaded when the program is started is specified through the configuration file.
  • Only some general and necessary functions are made into middleware.
  • When writing middleware, we must ensure the code quality and performance of middleware.

1.4 Gin uses Middleware

In Gin, you can use Gin Use method of engine to load middleware. It can be loaded in different locations, and the scope of action is different in different locations.

// Returns a gin Engine
router := gin.New()

// Use method to load Middleware
// Middleware works on all HTTP requests
router.Use(gin.Logger(), gin.Recovery()) 

// Middleware works on v1 group
v1 := router.Group("/v1").Use(gin.BasicAuth(gin.Accounts{"foo": "bar"})) 

//Middleware only works on the / v1/login API interface
v1.POST("/login", Login).Use(gin.BasicAuth(gin.Accounts{"foo": "bar"})) 

1.5 middleware supported by gin

  • gin.Logger()

The Logger middleware will write the log to gin DefaultWriter.

        gin.DefaultWriter defaults to OS Stdout .

  • gin.BasicAuth()

HTTP request basic authentication (authentication with user name and password)

  • gin.Recovery()

The Recovery middleware can recover from any panic and write a 500 status code.

  • gin.CustomRecovery(handle gin.ReciveryFunc)

It is similar to the Recover middleware, but the incoming handle method will be called for processing during recovery.

  • Custom Middleware

Middleware is actually a function. The function type is gin HandleFunc.

The underlying type of HandleFunc is func (*Context).

// Custom Logger() middleware
func Logger() gin.HandlerFunc {
    return func (c *gin.Context) {
        t := time.Now()

        // Set variable example
        c.Set("example", "12345")

        // Before request
        
        c.Next()

        // After request

        latency := time.Since(t)
        log.Print(latency)

        // Access the status we sent
        status := c.Writer.Status()
        log.Println(status)
    }
}

func main() {
    r := gin.New()

    // Customized Logger Middleware
    r.Use(Logger())

    r.GET("/test", func (c *gin.Context) {
        example := c.MustGet("example").(string)

        // print : "12345"
        log.Println(example)
    })

    r.Run(":8080")
}

1.6 open source middleware

middleware function
gin-jwtJWT middleware to realize JWT authentication
gin-swaggerAutomatically generate RESTful API documents in Swagger 2.0 format
corsCross domain implementation of HTTP requests
sessionsSession management middleware
authzAuthorization middleware based on caspin
pprofgin pprof Middleware
go-gin-prometheusPrometheus metrics exporter
gzipSupport gzip compression of HTTP requests and responses
gin-limitHTTP request Concurrency Control Middleware
requestidGenerate uuid for each Request and add it to the returned X-Request-ID Header.

2. Authentication, RequestID, cross domain

These three advanced functions can be realized through Gin middleware.

router := gin.New()

// authentication
router.Use(gin.BasicAuth(gin.Accounts{"foo": "bar"}))
router := gin.New()

// RequestID
router.Use(requestid.New(requestid.Config{
    Generator: func() string {
        return "test"
    },
}))
router := gin.New()

// Cross domain
// CORS for https://foo.com and https://github.com
// allowing:
//   - PUT and PATCH methods
//   - Origin header
//   - Credentials share
//   - Prefight requests cached for 12 hours
router.Use(cors.New(cors.Config{
    AllowOrigins:     []string{"https://foo.com"}, 
    AllowMethods:     []string{"PUT", "PATCH"},
    AllowHeaders:     []string{"Origin"},
    ExposeHeaders:    []string{"Content-Length"},
    AllowCredentials: true,
    AllowOriginFunc:  func (origin string) bool {
        return origin == "https://github.com"
    },
    MaxAge: 12 * time.Hour,
}))

3. Graceful shutdown

After the project goes online, we need to constantly iterate to enrich the project functions and repair bugs, which means that we need to constantly restart the service.

For HTTP services, if the traffic is large, many connections may not be disconnected and requests may not be completed when the service is restarted. If you close the service directly at this time, these connections will be directly disconnected and the request will terminate abnormally, which will affect the user experience.

We expect the HTTP service to close these connections normally after processing all requests, that is, to close the service gracefully.

There are two ways to gracefully turn off HTTP services:

3.1 with the help of a third party Go package

The most commonly used packages are fvbock/endless

You can use fvlock / endless to replace the ListenAndServe method of net/http.

router := gin.Default()

router.GET("/", handler)

...

endless.ListenAndServe(":4242", router)

3.2 self coding implementation

The advantage of using third-party packages is that it can slightly reduce the coding workload, but the disadvantage is that a new dependent package is introduced.

Therefore, they prefer to implement their own coding.

Go version 1.8 or later, http The server's built-in Shutdown method has realized graceful Shutdown.

func main () {
    router := gin.Default()
    router.GET("/", func(c *gin.Context) {
        time.Sleep(5 * time.Second)
        c.String(http.StatusOK, "Welcome Gin Server")
    })

    srv := &http.Server {
        Addr   : ":8080",
        Handler: router,
    }

    // Put SRV Listenandserve is executed in goroutine,
    // This will not block the SRV Shutdown function.
    go func() {
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("listen: %s\n", err)
        }
    }()

    // Because we put SRV Listenandserve is placed in goroutine,
    // So we need a mechanism that can make the whole process resident.
    // Use unbuffer channel and call signal The notify function binds the channel to SIGINT and SIGTERM
    // On the signal. In this way, after receiving SIGINT and SIGTERM signals, the quit channel will be written to the value, so as to end the blocking state and the program
    // Continue running and execute SRV Shutdown (CTX), graceful shutdown of HTTP service.
    quit := make(chan os.Signal)
    // kill (no param) default send syscall syscall.SIGTERM
    // kill -2  is syscall.SIGINT
    // kill -9  is syscall.SIGKILL but can't be cache, so don't need add it
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
    log.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown: ", err)
    }
    
    log.Println("Server exiting")
}