To understand the Gin framework in a simple way, it is enough to read this article

Introduction to Gin Framework

Gin is a simple, fast and powerful web framework for building web applications of all sizes. Its design concept is simplicity, efficiency and ease of use, which can help developers quickly build high-performance Web services. Whether building API services, web applications or microservices, Gin is a good choice. It has a martini-like API but much better performance, up to 40 times faster thanks to httprouter. If you need performance and good productivity, you'll love Gin.

Realize simple web program without gin framework

package main

import (
	"fmt"
	"net/http"
)

func main() {
    
    
	// 注册路由处理函数
	http.HandleFunc("/hello", sayhello)

	// 启动HTTP服务器并监听指定端口
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
    
    
		fmt.Printf("错误:%v\n", err)
		return
	}
}

// sayhello 是处理 /hello 路由的函数
func sayhello(w http.ResponseWriter, r *http.Request) {
    
    
	_, _ = fmt.Fprintf(w, "<h1>Hello</h1>")
}

After running the above code, enter the URL localhost:9090 in the browser and return the content <h1>Hello</h1>

download and installGin

go get -u github.com/gin-gonic/gin

First Gin example

package main

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

func main() {
    
    
	// 创建默认的Gin引擎
	r := gin.Default()

	// 定义路由和处理函数
	r.GET("/hello", func(c *gin.Context) {
    
    
		c.JSON(http.StatusOK, gin.H{
    
    
			"msg": "hello world",
		})
	})

	// 启动HTTP服务器并监听指定端口,默认在0.0.0.0:8080启动服务
	err := r.Run(":9090")
	if err != nil {
    
    
		return
	}
}

RESTful API

REST has nothing to do with technology, and represents a software architecture style. REST is the abbreviation of Representational State Transfer. The Chinese translation is "representational state transfer" or "representational state transfer".

Recommended reading Ruan Yifeng understands RESTful architecture

To put it simply, the meaning of REST is that when the client interacts with the Web server, the four request methods in the HTTP protocol are used to represent different actions.

  • GETused to obtain resources
  • POSTto create new resources
  • PUTused to update resources
  • PATCHUsed to update some resources
  • DELETEused to delete resources

As long as the API program follows the REST style, it can be called a RESTful API. At present, in the architecture where the front and back ends are separated, the front and back ends basically interact through RESTful APIs.

For example, we now want to write a conference management system, we can query, create, update and delete conferences, etc. When we write programs, we need to design the way and path for the client browser to interact with our Web server. According to experience, we usually design the following mode:

request method URL meaning
GET /meeting Query meeting information
POST /create_meeting Create meeting minutes
POST /update_meeting Update meeting information
POST /delete_meeting Delete meeting information

For the same requirements, we design the RESTful API as follows:

request method URL meaning
GET /meeting Query meeting information
POST /meeting Create meeting minutes
PUT /meeting Update meeting information
DELETE /meeting Delete meeting information

The Gin framework supports the development of RESTful APIs.

package main

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

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

	// GET请求,获取资源
	r.GET("/meeting", func(context *gin.Context) {
    
    
		context.JSON(http.StatusOK, gin.H{
    
    
			"msg": "GET",
		})
	})

	// POST请求,新建资源
	r.POST("/meeting", func(context *gin.Context) {
    
    
		context.JSON(http.StatusOK, gin.H{
    
    
			"msg": "POST",
		})
	})

	// PUT请求,更新资源
	r.PUT("/meeting", func(context *gin.Context) {
    
    
		context.JSON(http.StatusOK, gin.H{
    
    
			"msg": "PUT",
		})
	})

	// PATCH请求,部分更新资源
	r.PATCH("/meeting", func(context *gin.Context) {
    
    
		context.JSON(http.StatusOK, gin.H{
    
    
			"msg": "PATCH",
		})
	})

	// DELETE请求,删除资源
	r.DELETE("/meeting", func(context *gin.Context) {
    
    
		context.JSON(http.StatusOK, gin.H{
    
    
			"msg": "DELETE",
		})
	})

	// 启动HTTP服务器并监听指定端口
	err := r.Run(":9090")
	if err != nil {
    
    
		return
	}
}

When developing a RESTful API, we usually use Postman as a client testing tool.

go template syntax

package main

import (
	"errors"
	"fmt"
	"html/template"
	"net/http"
)

type UserInfo struct {
    
    
	Name   string
	Gender string
	Age    int
}

func sayHello(w http.ResponseWriter, _ *http.Request) {
    
    

	// 自定义函数
	admire := func(name string, gender string) (string, error) {
    
    
		var praise string
		if gender == "男" {
    
    
			praise = "真帅气!!!!!!!!"
		} else if gender == "女" {
    
    
			praise = "真漂亮!!!!!!!!"
		} else {
    
    
			return "", errors.New("invalid gender")
		}
		return name + praise, nil
	}

	// 解析指定文件生成模板对象(并注册自定义函数)
	tmpl, err := template.New("hello.tmpl").Funcs(template.FuncMap{
    
    "admire": admire}).ParseFiles("./hello.tmpl")
	if err != nil {
    
    
		fmt.Println("create template failed, err:", err)
		return
	}

	// 利用给定数据渲染模板,并将结果写入w
	user1 := UserInfo{
    
    
		Name:   "小王子",
		Gender: "男",
		Age:    17,
	}

	user2 := map[string]interface{
    
    }{
    
    
		"name":   "小公主",
		"gender": "女",
		"age":    19,
	}

	hobbylist := []string{
    
    
		"跑步",
		"听音乐",
		"学习",
	}

	err = tmpl.Execute(w, map[string]interface{
    
    }{
    
    
		"user1": user1,
		"user2": user2,
		"hobby": hobbylist,
	})
	if err != nil {
    
    
		return
	}
}

func qianTao(w http.ResponseWriter, _ *http.Request) {
    
    

	tmpl, err := template.ParseFiles("./t.tmpl", "./ul.tmpl")
	if err != nil {
    
    
		fmt.Println("create template failed, err:", err)
		return
	}
	user := UserInfo{
    
    
		Name:   "小王子",
		Gender: "男",
		Age:    17,
	}
	err = tmpl.Execute(w, user)
	if err != nil {
    
    
		return
	}

}
func main() {
    
    
	http.HandleFunc("/", sayHello)
	http.HandleFunc("/demo", qianTao)
	err := http.ListenAndServe(":9090", nil)
	if err != nil {
    
    
		fmt.Println("HTTP server failed,err:", err)
		return
	}
}

Below is hello.tmplthe sample file

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>

<body>
<p>Hello {
   
   {.user1.Name}}</p>
<p>性别:{
   
   {.user1.Gender}}</p>
<p>年龄:{
   
   {.user1.Age}}</p>
<br>
<p>Hello {
   
   {.user2.name}}</p>
<p>性别:{
   
   {.user2.gender}}</p>
<p>年龄:{
   
   {.user2.age}}</p>

{
   
   {/*自定义变量*/}}
{
   
   { $a := 100 }}
{
   
   { $b := .user1.Age }}

<hr>

{
   
   {/*移除空格*/}}
<p>年龄:{
   
   {- .user2.age -}}</p>

<hr>

{
   
   {/*条件判断*/}}
{
   
   { if $a}}
    {
   
   {$a}}
{
   
   {else}}
    a 不存在
{
   
   {end}}

<hr>
{
   
   { if lt .user1.Age 18}}
    未成年
{
   
   {else}}
    上大学了
{
   
   {end}}

<hr>
{
   
   {range $index,$hobby :=.hobby}}
    <p>{
   
   {$index}}------{
   
   {$hobby}}</p>
{
   
   {else}}
    没有爱好
{
   
   {end}}

<hr>
{
   
   {/*with作用域*/}}
{
   
   {with .user1}}
    <p>Hello {
   
   {.Name}}</p>
    <p>性别:{
   
   {.Gender}}</p>
    <p>年龄:{
   
   {.Age}}</p>
{
   
   {end}}

<hr>
{
   
   {index .hobby 2}}
<hr>

{
   
   {/*自定义函数*/}}
{
   
   {admire .user1.Name .user1.Gender}}
{
   
   {admire .user2.name .user2.gender}}
</body>

predefined function

When executing a template, functions are looked up in two function dictionaries: first the template function dictionary, then the global function dictionary. Generally, functions are not defined in templates, but functions are added to templates using the Funcs method.

The predefined global functions are as follows:

and
    函数返回它的第一个empty参数或者最后一个参数;
    就是说"and x y"等价于"if x then y else x";所有参数都会执行;
or
    返回第一个非empty参数或者最后一个参数;
    亦即"or x y"等价于"if x then x else y";所有参数都会执行;
not
    返回它的单个参数的布尔值的否定
len
    返回它的参数的整数类型长度
index
    执行结果为第一个参数以剩下的参数为索引/键指向的值;
    如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。
print
    即fmt.Sprint
printf
    即fmt.Sprintf
println
    即fmt.Sprintln
html
    返回与其参数的文本表示形式等效的转义HTML。
    这个函数在html/template中不可用。
urlquery
    以适合嵌入到网址查询中的形式返回其参数的文本表示的转义值。
    这个函数在html/template中不可用。
js
    返回与其参数的文本表示形式等效的转义JavaScript。
call
    执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数;
    如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2);
    其中Y是函数类型的字段或者字典的值,或者其他类似情况;
    call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同);
    该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型;
    如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误;

comparison function

Boolean functions will treat any type of zero value as false and all others as true.

The following is a collection of binary comparison operations defined as functions:

eq      如果arg1 == arg2则返回真
ne      如果arg1 != arg2则返回真
lt      如果arg1 < arg2则返回真
le      如果arg1 <= arg2则返回真
gt      如果arg1 > arg2则返回真
ge      如果arg1 >= arg2则返回真

In order to simplify multi-parameter equality detection, eq (only eq) can accept 2 or more parameters, it will compare the first parameter with the remaining parameters in turn, and return the result of the following formula:

{
   
   {eq arg1 arg2 arg3}}

Comparison functions only work on primitive types (or redefined primitive types, such as "type Celsius float32"). However, integers and floating point numbers cannot be compared to each other.

custom function

For specific examples, refer to the main.go file mentioned in the above template syntax

func sayHello(w http.ResponseWriter, _ *http.Request) {
    
    

	// 自定义函数
	admire := func(name string, gender string) (string, error) {
    
    
		var praise string
		if gender == "男" {
    
    
			praise = "真帅气!!!!!!!!"
		} else if gender == "女" {
    
    
			praise = "真漂亮!!!!!!!!"
		} else {
    
    
			return "", errors.New("invalid gender")
		}
		return name + praise, nil
	}

	// 解析指定文件生成模板对象(并注册自定义函数)
	tmpl, err := template.New("hello.tmpl").Funcs(template.FuncMap{
    
    "admire": admire}).ParseFiles("./hello.tmpl")
	if err != nil {
    
    
		fmt.Println("create template failed, err:", err)
		return
    }
}

transfer

{
   
   {admire .user1.Name .user1.Gender}}
{
   
   {admire .user2.name .user2.gender}}

template nesting

func qianTao(w http.ResponseWriter, _ *http.Request) {
    
    

	tmpl, err := template.ParseFiles("./t.tmpl", "./ul.tmpl")
	if err != nil {
    
    
		fmt.Println("create template failed, err:", err)
		return
	}
	user := UserInfo{
    
    
		Name:   "小王子",
		Gender: "男",
		Age:    17,
	}
	err = tmpl.Execute(w, user)
	if err != nil {
    
    
		return
	}

t.tmpl file

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>tmpl test</title>
</head>
<body>

<h1>测试嵌套template语法</h1>
<hr>
{
   
   {template "ul.tmpl"}}
<hr>
{
   
   {template "ol.tmpl"}}
</body>
</html>

{
   
   { define "ol.tmpl"}}
    <ol>
        <li>吃饭</li>
        <li>睡觉</li>
        <li>打豆豆</li>
    </ol>
{
   
   {end}}

<div>你好,{
   
   {.Name}}!</div>

ul.html file

<ul>
    <li>注释</li>
    <li>日志</li>
    <li>测试</li>
</ul>

template inheritance

main.go document

package main

import (
	"fmt"
	"html/template"
	"net/http"
)

func index(w http.ResponseWriter, _ *http.Request) {
    
    
	//定义模板
	//解析模板
	tmpl, err := template.ParseFiles("./base.tmpl", "./index.tmpl")
	if err != nil {
    
    
		fmt.Printf("parse error: %v\n", err)
		return
	}
	msg := "hello world"
	//渲染模板
	err = tmpl.ExecuteTemplate(w, "index.tmpl", msg)
	if err != nil {
    
    
		return
	}

}

func base(w http.ResponseWriter, _ *http.Request) {
    
    
	tmpl, err := template.ParseFiles("./base.tmpl")
	if err != nil {
    
    
		fmt.Printf("parse error: %v\n", err)
		return
	}
	msg := "这是base页面"
	//渲染模板
	err = tmpl.Execute(w, msg)
	if err != nil {
    
    
		return
	}

}

func main() {
    
    
	http.HandleFunc("/index", index)
	http.HandleFunc("/base", base)
	err := http.ListenAndServe(":9000", nil)
	if err != nil {
    
    
		fmt.Println("HTTP server failed,err:", err)
		return
	}
}

base.tmpl file

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>模板继承</title>
    <style>
        {
      
      
            margin: 0
        ;
        }
        .nav {
      
      
            height: 50px;
            width: 100%;
            position: fixed;
            top: 0;
            background-color: burlywood;
        }

        .main {
      
      
            margin-top: 50px;
        }

        .menu {
      
      
            width: 20%;
            height: 100%;
            position: fixed;
            left: 0;
            background-color: cornflowerblue;
        }

        .center {
      
      
            text-align: center;
        }
    </style>
</head>
<body>
<div class="nav"></div>
<div class="main">
    <div class="menu"></div>
    <div class="content center">
        {
   
   {.}}
        {
   
   {block "content" .}}
        {
   
   {end}}
    </div>

</div>

</body>
</html>

index.tmpl file

{
   
   {/*继承根模板*/}}

{
   
   {template "base.tmpl" .}}

{
   
   {/*重新定义模板*/}}

{
   
   {define "content"}}
    <h1>这是index页面</h1>
{
   
   {end}}

If our template names conflict, for example, a index.tmpltemplate is defined under different business lines, we can solve it by the following two methods.

  1. Use { {define 模板名}}the statement at the beginning of the template file to explicitly name the template.
  2. You can store the template files in templatesdifferent directories under the folder, and then use template.ParseGlob("templates/**/*.tmpl")the parsing template.

template supplement

Modify the default identifier

The template engine of the Go standard library uses curly braces { { and }}as identifiers, and many front-end frameworks (such as Vueand AngularJS) also use { { and }}as identifiers, so when we use the Go language template engine and the above front-end frameworks at the same time, there will be conflicts. At this time We need to modify the identifier, modify the front end or modify the Go language. Here's how to modify the default identifiers of the Go language template engine:

template.New("t.tmpl").Delims("{[", "]}").ParseFiles("./t.tmpl")

Finally when we render

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>自定义模板函数</title>
</head>
<body>
<h1>姓名: {[.Name]}</h1>
<h1>性别: {[.Gender]}</h1>
<h1>年龄: {[.Age]}</h1>
</body>
</html>

The difference between text/template and html/tempalte

html/templateIt is aimed at scenarios where HTML content needs to be returned, and some risky content will be escaped during the template rendering process to prevent cross-site scripting attacks (XSS).

For example, I define the following template file:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Hello</title>
</head>
<body>
    {
   
   {.}}
</body>
</html>

At this time, a piece of JS code is passed in and used html/templateto render the file, and the escaped JS content will be displayed on the page.

But in some scenarios, if we believe the content entered by the user and don't want to escape, we can write a safe function by ourselves and manually return a template.HTMLtype of content. Examples are as follows:

func xss(w http.ResponseWriter, r *http.Request){
    
    
	tmpl,err := template.New("xss.tmpl").Funcs(template.FuncMap{
    
    
		"safe": func(s string)template.HTML {
    
    
			return template.HTML(s)
		},
	}).ParseFiles("./xss.tmpl")
	if err != nil {
    
    
		fmt.Println("create template failed, err:", err)
		return
	}
	jsStr := `<script>alert('123')</script>`
	err = tmpl.Execute(w, jsStr)
	if err != nil {
    
    
		fmt.Println(err)
	}
}

In this way, we only need to use the safe function we defined after the content that does not need to be escaped in the template file.

{
   
   { . | safe }}

Gin rendering

We first define a folder for storing template files , and then define a folder and a folder templatesin it according to the business . The content of the file is as follows:postsusersposts/index.html

{
   
   {define "posts/index.tmpl"}}
    <!DOCTYPE html>
    <html lang="en">

    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <link rel="stylesheet" href="/xxx/index.css">
        <title>posts/index</title>
    </head>
    <body>
    {
   
   {.title |safe}}
    </body>
    <script src="/xxx/index.js"></script>
    </html>
{
   
   {end}}

users/index.htmlThe content of the file is as follows:

{
   
   {define "users/index.tmpl"}}
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>users/index</title>
    </head>
    <body>
    {
   
   {.title}}
    </body>
    </html>
{
   
   {end}}

LoadHTMLGlob()The or method is used in the Gin framework LoadHTMLFiles()to render HTML templates.

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"html/template"
	"net/http"
)

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

	// 设置静态文件路由,表示以/xxx开头的静态文件都会去statics目录下找
	r.Static("/xxx", "./statics")

	// 设置模板函数
	r.SetFuncMap(template.FuncMap{
    
    
		"safe": func(s string) template.HTML {
    
    
			return template.HTML(s)
		},
	})

	// 加载模板文件
	r.LoadHTMLGlob("templates/**/*")

	// 处理/posts/index请求
	r.GET("/posts/index", func(c *gin.Context) {
    
    
		c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
    
    
			"title": "<a href= https://uestcwxy.love>wxy的博客</a>",
		})
	})

	// 处理/users/index请求
	r.GET("/users/index", func(c *gin.Context) {
    
    
		c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
    
    
			"title": "https://uestcwxy.top",
		})
	})

	// 启动服务器
	err := r.Run(":9000")
	if err != nil {
    
    
		fmt.Println("服务器启动失败")
	}
}

Use template inheritance

The Gin framework uses a single template by default. If you need to use block templatefunctions, you can use "github.com/gin-contrib/multitemplate"the library to implement them. The specific examples are as follows:

First of all, suppose we have the following template files under the templates folder in our project directory, which home.tmplare index.tmplinherited from base.tmpl:

templates
├── includes
│   ├── home.tmpl
│   └── index.tmpl
├── layouts
│   └── base.tmpl
└── scripts.tmpl

Then we define a loadTemplatesfunction as follows:

func loadTemplates(templatesDir string) multitemplate.Renderer {
    
    
	// 创建一个新的 multitemplate.Renderer 实例
	r := multitemplate.NewRenderer()

	// 加载 layouts 目录下的模板文件
	layouts, err := filepath.Glob(templatesDir + "/layouts/*.tmpl")
	if err != nil {
    
    
		panic(err.Error())
	}

	// 加载 includes 目录下的模板文件
	includes, err := filepath.Glob(templatesDir + "/includes/*.tmpl")
	if err != nil {
    
    
		panic(err.Error())
	}

	// 为 layouts/ 和 includes/ 目录生成 templates map
	for _, include := range includes {
    
    
		// 创建 layouts 的副本
		layoutCopy := make([]string, len(layouts))
		copy(layoutCopy, layouts)

		// 将 layouts 和 include 组合成一个文件切片
		files := append(layoutCopy, include)

		// 将文件切片添加到 multitemplate.Renderer 实例中
		r.AddFromFiles(filepath.Base(include), files...)
	}

	return r
}

we are in mainthe function

func indexFunc(c *gin.Context) {
    
    
	// 渲染 index.tmpl 模板并返回给客户端
	c.HTML(http.StatusOK, "index.tmpl", nil)
}

func homeFunc(c *gin.Context) {
    
    
	// 渲染 home.tmpl 模板并返回给客户端
	c.HTML(http.StatusOK, "home.tmpl", nil)
}

func main() {
    
    
	// 创建一个默认的 Gin 引擎实例
	r := gin.Default()

	// 加载模板文件,并将返回的 multitemplate.Renderer 实例赋值给 Gin 引擎的 HTMLRender 字段
	r.HTMLRender = loadTemplates("./templates")

	// 设置路由处理函数,处理 /index 请求
	r.GET("/index", indexFunc)

	// 设置路由处理函数,处理 /home 请求
	r.GET("/home", homeFunc)

	// 启动服务器,监听默认端口
	r.Run()
}

Supplementary file path handling

Regarding the paths of template files and static files, we need to set them according to the requirements of the company/project. The path of the currently executing program can be obtained using the following function.

import (
	"os"
	"path/filepath"
)

func getCurrentPath() string {
    
    
	// 获取可执行文件的路径
	if ex, err := os.Executable(); err == nil {
    
    
		// 返回可执行文件的目录路径
		return filepath.Dir(ex)
	}
	// 如果获取路径失败,则返回当前目录路径
	return "./"
}

JSON rendering

package main

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

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

	// gin.H 是map[string]interface{}的缩写
	r.GET("/someJSON", func(c *gin.Context) {
    
    
		// 方式一:自己拼接JSON
		c.JSON(http.StatusOK, gin.H{
    
    
			"message": "Hello world!",
			"name":    "wxy",
		})
	})

	r.GET("/moreJSON", func(c *gin.Context) {
    
    
		// 方法二:使用结构体
		type msg struct {
    
    
			Name    string `json:"name"`
			Message string `json:"message"`
			Age     int    `json:"age"`
		}
		data := msg{
    
    "121", "hh", 18}
		c.JSON(http.StatusOK, data)
	})
	err := r.Run(":9090")
	if err != nil {
    
    
		return
	}
}

XML rendering

Note the need to use named struct types.

func main() {
    
    
	r := gin.Default()
	// gin.H 是map[string]interface{}的缩写
	r.GET("/someXML", func(c *gin.Context) {
    
    
		// 方式一:自己拼接JSON
		c.XML(http.StatusOK, gin.H{
    
    "message": "Hello world!"})
	})
	r.GET("/moreXML", func(c *gin.Context) {
    
    
		// 方法二:使用结构体
		type MessageRecord struct {
    
    
			Name    string
			Message string
			Age     int
		}
		var msg MessageRecord
		msg.Name = "小王子"
		msg.Message = "Hello world!"
		msg.Age = 18
		c.XML(http.StatusOK, msg)
	})
	r.Run(":8080")
}

YMAL rendering

r.GET("/someYAML", func(c *gin.Context) {
    
    
	c.YAML(http.StatusOK, gin.H{
    
    "message": "ok", "status": http.StatusOK})
})

protobuf rendering

r.GET("/someProtoBuf", func(c *gin.Context) {
    
    
	reps := []int64{
    
    int64(1), int64(2)}
	label := "test"
	// protobuf 的具体定义写在 testdata/protoexample 文件中。
	data := &protoexample.Test{
    
    
		Label: &label,
		Reps:  reps,
	}
	// 请注意,数据在响应中变为二进制数据
	// 将输出被 protoexample.Test protobuf 序列化了的数据
	c.ProtoBuf(http.StatusOK, data)
})

Get parameters

get querystring parameter

querystringRefers to ?the parameters carried behind in the URL, for example: /user/search?username=wxy&address=沙河校区. The method to obtain the querystring parameter of the request is as follows:

package main

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

func main() {
    
    
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.GET("/user/search", func(c *gin.Context) {
    
    
		username := c.DefaultQuery("username", "wxy")
		//username := c.Query("username")
		address := c.Query("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
    
    
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})
	err := r.Run()
	if err != nil {
    
    
		return
	}
}

Get form parameters

When the data requested by the front end is submitted through the form form, for example, sending /user/searcha POST request to China, the method of obtaining the request data is as follows:

func main() {
    
    
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.POST("/user/search", func(c *gin.Context) {
    
    
		// DefaultPostForm取不到值时会返回指定的默认值
		//username := c.DefaultPostForm("username", "wxy")
		username, ok := c.GetPostForm("username")
		if !ok {
    
    
			username = "hhh"
		}
		//username := c.PostForm("username")
		address := c.PostForm("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
    
    
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})

	err := r.Run(":8080")
	if err != nil {
    
    
		return
	}
}

get json parameter

When the data requested by the front end is submitted through JSON, such as /jsonsending a POST request to China, the method of obtaining the request parameters is as follows:

r.POST("/json", func(c *gin.Context) {
    
    
	// 注意:下面为了举例子方便,暂时忽略了错误处理
	b, _ := c.GetRawData()  // 从c.Request.Body读取请求数据
	// 定义map或结构体
	var m map[string]interface{
    
    }
	// 反序列化
	_ = json.Unmarshal(b, &m)

	c.JSON(http.StatusOK, m)
})

For a more convenient way to obtain request parameters, see the parameter binding section below.

Get the path parameter

The parameters of the request are passed through the URL path, for example: /user/search/wxy/沙河校区. The way to obtain the parameters in the request URL path is as follows.

func main() {
    
    
	//Default返回一个默认的路由引擎
	r := gin.Default()
	r.GET("/user/search/:username/:address", func(c *gin.Context) {
    
    
		username := c.Param("username")
		address := c.Param("address")
		//输出json结果给调用方
		c.JSON(http.StatusOK, gin.H{
    
    
			"message":  "ok",
			"username": username,
			"address":  address,
		})
	})

	err := r.Run(":8080")
	if err != nil {
    
    
		return
	}
}

parameter binding

Content-TypeIn order to obtain request-related parameters more conveniently and improve development efficiency, we can identify the request data type based on the request and use the reflection mechanism to automatically extract the request parameters such as QueryString, form表单, JSON, XMLetc. into the structure. The following sample code demonstrates .ShouldBind()a powerful feature that can automatically extract data of type and type based on the request JSON, form表单and QueryStringbind the value to the specified structure object.

package main

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

// Login 结构体用于绑定JSON数据
type Login struct {
    
    
	User     string `form:"user" json:"user" binding:"required"`
	Password string `form:"password" json:"password" binding:"required"`
}

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

	// 处理绑定JSON的示例请求 ({"user": "wxy", "password": "123456"})
	r.POST("/loginJSON", func(c *gin.Context) {
    
    
		var login Login

		// 将请求中的JSON数据绑定到Login结构体
		if err := c.ShouldBind(&login); err == nil {
    
    
			fmt.Printf("登录信息:%#v\n", login)
			c.JSON(http.StatusOK, gin.H{
    
    
				"user":     login.User,
				"password": login.Password,
			})
		} else {
    
    
			c.JSON(http.StatusBadRequest, gin.H{
    
    "error": err.Error()})
		}
	})

	// 处理绑定form表单的示例请求 (user=q1mi&password=123456)
	r.POST("/loginForm", func(c *gin.Context) {
    
    
		var login Login

		// 根据请求的Content-Type自动选择绑定器进行绑定
		if err := c.ShouldBind(&login); err == nil {
    
    
			c.JSON(http.StatusOK, gin.H{
    
    
				"user":     login.User,
				"password": login.Password,
			})
		} else {
    
    
			c.JSON(http.StatusBadRequest, gin.H{
    
    "error": err.Error()})
		}
	})

	// 处理绑定QueryString的示例请求 (/loginQuery?user=q1mi&password=123456)
	r.GET("/loginForm", func(c *gin.Context) {
    
    
		var login Login

		// 根据请求的Content-Type自动选择绑定器进行绑定
		if err := c.ShouldBind(&login); err == nil {
    
    
			c.JSON(http.StatusOK, gin.H{
    
    
				"user":     login.User,
				"password": login.Password,
			})
		} else {
    
    
			c.JSON(http.StatusBadRequest, gin.H{
    
    "error": err.Error()})
		}
	})

	// 监听并在0.0.0.0:8080上提供服务
	err := r.Run(":8080")
	if err != nil {
    
    
		return
	}
}

ShouldBindThe function selects the appropriate binding engine for data binding based on the method and content type of the request. For GETrequests, only query parameter binding is used; for POSTrequests, JSON or XML data binding is preferred, and if not JSON or XML, form data binding is used. This makes it easy to parse and bind the data in the request into a structure so that it can be used when processing the request.

File Upload

single file upload

File upload front-end page code:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>上传文件示例</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
    <input type="file" name="f1">
    <input type="submit" value="上传">
</form>
</body>
</html>

Part of the code of the backend gin framework:

func main() {
    
    
	router := gin.Default()
	// 处理multipart forms提交文件时默认的内存限制是32 MiB
	// 可以通过下面的方式修改
	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
    
    
		// 单个文件
		file, err := c.FormFile("f1")
		if err != nil {
    
    
			c.JSON(http.StatusInternalServerError, gin.H{
    
    
				"message": err.Error(),
			})
			return
		}

		log.Println(file.Filename)
		dst := fmt.Sprintf("C:/tmp/%s", file.Filename)
		// 上传文件到指定的目录
		c.SaveUploadedFile(file, dst)
		c.JSON(http.StatusOK, gin.H{
    
    
			"message": fmt.Sprintf("'%s' uploaded!", file.Filename),
		})
	})
	router.Run()
}

Multiple file uploads

func main() {
    
    
	router := gin.Default()
	// 处理multipart forms提交文件时默认的内存限制是32 MiB
	// 可以通过下面的方式修改
	// router.MaxMultipartMemory = 8 << 20  // 8 MiB
	router.POST("/upload", func(c *gin.Context) {
    
    
		// Multipart form
		form, _ := c.MultipartForm()
		files := form.File["file"]

		for index, file := range files {
    
    
			log.Println(file.Filename)
			dst := fmt.Sprintf("./upload/%s_%d", file.Filename, index)
			// 上传文件到指定的目录
			c.SaveUploadedFile(file, dst)
		}
		c.JSON(http.StatusOK, gin.H{
    
    
			"message": fmt.Sprintf("%d files uploaded!", len(files)),
		})
	})
	router.Run()
}

redirect

HTTP redirection

HTTP redirection is easy. Both internal and external redirection are supported.

package main

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

func main() {
    
    
	r := gin.Default()
	r.GET("/test", func(c *gin.Context) {
    
    
		c.Redirect(http.StatusMovedPermanently, "https://www.baidu.com/")
	})
	// Listen and serve on 0.0.0.0:8080
	err := r.Run(":8080")
	if err != nil {
    
    
		return
	}
}

route redirection

Route redirection, use HandleContext:

package main

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

func main() {
    
    
	r := gin.Default()
	r.GET("/test", func(c *gin.Context) {
    
    
		// 指定重定向的URL
		c.Request.URL.Path = "/test2"
		r.HandleContext(c)
	})
	r.GET("/test2", func(c *gin.Context) {
    
    
		c.JSON(http.StatusOK, gin.H{
    
    "hello": "world"})
	})
	// Listen and serve on 0.0.0.0:8080
	err := r.Run(":8080")
	if err != nil {
    
    
		return
	}
}

Gin Routing

common routing

r.GET("/index", func(c *gin.Context) {
    
    ...})
r.GET("/login", func(c *gin.Context) {
    
    ...})
r.POST("/login", func(c *gin.Context) {
    
    ...})

AnyIn addition, there is a method that can match all request methods as follows:

package main

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

func main() {
    
    
	r := gin.Default()
	r.Any("/user", func(c *gin.Context) {
    
    
		switch c.Request.Method {
    
    
		case http.MethodGet:
			c.JSON(http.StatusOK, gin.H{
    
    "method": http.MethodGet})
		case http.MethodPost:
			c.JSON(http.StatusOK, gin.H{
    
    "method": http.MethodPost})
		}
	})
	err := r.Run()
	if err != nil {
    
    
		fmt.Println(err.Error())
	}
}

Add a handler for a route that does not have a handler configured. By default, it returns a 404 code. The following code returns a views/404.htmlpage for all requests that do not match a route.

r.NoRoute(func(c *gin.Context) {
    
    
		c.HTML(http.StatusNotFound, "views/404.html", nil)
	})

routing group

We can group routes that share a common URL prefix into a route group. It is customary to use a {}pair of routes wrapped in the same group, this is just for clarity, {}there is no difference in function whether you use the package or not.

package main

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

func main() {
    
    
	r := gin.Default()
	userGroup := r.Group("/user")
	{
    
    
		userGroup.GET("/index", func(c *gin.Context) {
    
    })
		userGroup.GET("/login", func(c *gin.Context) {
    
    })
		userGroup.POST("/login", func(c *gin.Context) {
    
    })
	}
	shopGroup := r.Group("/shop")
	{
    
    
		shopGroup.GET("/index", func(c *gin.Context) {
    
    })
		shopGroup.GET("/cart", func(c *gin.Context) {
    
    })
		shopGroup.POST("/checkout", func(c *gin.Context) {
    
    })
	}
	err := r.Run()
	if err != nil {
    
    
		fmt.Println(err.Error())
	}
}

Routing groups also support nesting, for example:

shopGroup := r.Group("/shop")
	{
    
    
		shopGroup.GET("/index", func(c *gin.Context) {
    
    ...})
		shopGroup.GET("/cart", func(c *gin.Context) {
    
    ...})
		shopGroup.POST("/checkout", func(c *gin.Context) {
    
    ...})
		// 嵌套路由组
		xx := shopGroup.Group("xx")
		xx.GET("/oo", func(c *gin.Context) {
    
    ...})
	}

Usually we use route grouping to divide business logic or divide API versions.

routing principle

The routing principle of the Gin framework is dynamic routing implemented using a prefix tree. It uses a customized version of httprouter, and its routing principle is a tree structure that uses a lot of common prefixes, basically a compact Trie tree (or just a Radix Tree).

Gin middleware

The Gin framework allows developers to add the user's own hook (Hook) function in the process of processing the request. This hook function is called middleware. Middleware is suitable for processing some common business logic, such as login authentication, permission verification, data paging, logging, time-consuming statistics, etc.

Define middleware

Middleware in Gin must be a gin.HandlerFunctype.

Record interface time-consuming middleware

For example, we define a middleware that counts the time spent on requests like the following code.

// StatCost 是一个统计耗时请求耗时的中间件
func StatCost() gin.HandlerFunc {
    
    
	return func(c *gin.Context) {
    
    
		start := time.Now()
		c.Set("name", "wxy") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
		// 调用该请求的剩余处理程序
		c.Next()
		// 不调用该请求的剩余处理程序
		// c.Abort()
		// 计算耗时
		cost := time.Since(start)
		log.Println(cost)
	}
}

Middleware that logs the response body

Sometimes we may want to record the response data returned to the client under certain circumstances. At this time, we can write a middleware to handle it.

type bodyLogWriter struct {
    
    
	gin.ResponseWriter               // 嵌入gin框架ResponseWriter
	body               *bytes.Buffer // 我们记录用的response
}

// Write 写入响应体数据
func (w bodyLogWriter) Write(b []byte) (int, error) {
    
    
	w.body.Write(b)                  // 我们记录一份
	return w.ResponseWriter.Write(b) // 真正写入响应
}

// ginBodyLogMiddleware 一个记录返回给客户端响应体的中间件
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin
func ginBodyLogMiddleware(c *gin.Context) {
    
    
	blw := &bodyLogWriter{
    
    body: bytes.NewBuffer([]byte{
    
    }), ResponseWriter: c.Writer}
	c.Writer = blw // 使用我们自定义的类型替换默认的

	c.Next() // 执行业务逻辑

	fmt.Println("Response body: " + blw.body.String()) // 事后按需记录返回的响应
}

Cross domain middleware cors

It is recommended to use the community’s https://github.com/gin-contrib/cors library, a line of code to solve the cross-domain problem under the front-end and back-end separation architecture.

Note: This middleware needs to be registered before the business processing function.

This library supports various commonly used configuration items, and the specific usage methods are as follows.

package main

import (
  "time"

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

func main() {
    
    
  router := gin.Default()
  // CORS for https://foo.com and https://github.com origins, allowing:
  // - PUT and PATCH methods
  // - Origin header
  // - Credentials share
  // - Preflight requests cached for 12 hours
  router.Use(cors.New(cors.Config{
    
    
    AllowOrigins:     []string{
    
    "https://foo.com"},  // 允许跨域发来请求的网站
    AllowMethods:     []string{
    
    "GET", "POST", "PUT", "DELETE",  "OPTIONS"},  // 允许的请求方法
    AllowHeaders:     []string{
    
    "Origin", "Authorization", "Content-Type"},
    ExposeHeaders:    []string{
    
    "Content-Length"},
    AllowCredentials: true,
    AllowOriginFunc: func(origin string) bool {
    
      // 自定义过滤源站的方法
      return origin == "https://github.com"
    },
    MaxAge: 12 * time.Hour,
  }))
  router.Run()
}

Of course, you can simply use the default configuration like the sample code below to allow all cross-origin requests.

func main() {
    
    
  router := gin.Default()
  // same as
  // config := cors.DefaultConfig()
  // config.AllowAllOrigins = true
  // router.Use(cors.New(config))
  router.Use(cors.Default())
  router.Run()
}

Register middleware

In the gin framework, we can add any number of middleware for each route.

Register for global routing

func main() {
    
    
	// 新建一个没有任何默认中间件的路由
	r := gin.New()
	// 注册一个全局中间件
	r.Use(StatCost())
	
	r.GET("/test", func(c *gin.Context) {
    
    
		name := c.MustGet("name").(string) // 从上下文取值
		log.Println(name)
		c.JSON(http.StatusOK, gin.H{
    
    
			"message": "Hello world!",
		})
	})
	r.Run()
}

Individually register for a route

// 给/test2路由单独注册中间件(可注册多个)
	r.GET("/test2", StatCost(), func(c *gin.Context) {
    
    
		name := c.MustGet("name").(string) // 从上下文取值
		log.Println(name)
		c.JSON(http.StatusOK, gin.H{
    
    
			"message": "Hello world!",
		})
	})

Register middleware for routing group

There are two ways to register middleware for routing group.

Writing 1:

shopGroup := r.Group("/shop", StatCost())
{
    
    
    shopGroup.GET("/index", func(c *gin.Context) {
    
    ...})
    ...
}

Writing 2:

shopGroup := r.Group("/shop")
shopGroup.Use(StatCost())
{
    
    
    shopGroup.GET("/index", func(c *gin.Context) {
    
    ...})
    ...
}

Middleware Considerations

gin default middleware

gin.Default()Loggerand middleware are used by default Recovery, where:

  • LoggerMiddleware writes logs gin.DefaultWritereven if configured GIN_MODE=release.
  • RecoveryThe middleware will recover anything panic. If there is a panic, a 500 response code will be written.

If you don't want to use the above two default middleware, you can create gin.New()a new route without any default middleware.

Use goroutine in gin middleware

handlerWhen starting a new one in middleware or in goroutine, the original context (c *gin.Context) cannot be usedc.Copy() , its read-only copy ( ) must be used.

run multiple services

We can start services on multiple ports, for example:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
)

var (
	g errgroup.Group
)

func router01() http.Handler {
    
    
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
    
    
		c.JSON(
			http.StatusOK,
			gin.H{
    
    
				"code":  http.StatusOK,
				"error": "Welcome server 01",
			},
		)
	})

	return e
}

func router02() http.Handler {
    
    
	e := gin.New()
	e.Use(gin.Recovery())
	e.GET("/", func(c *gin.Context) {
    
    
		c.JSON(
			http.StatusOK,
			gin.H{
    
    
				"code":  http.StatusOK,
				"error": "Welcome server 02",
			},
		)
	})

	return e
}

func main() {
    
    
	server01 := &http.Server{
    
    
		Addr:         ":8080",
		Handler:      router01(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	server02 := &http.Server{
    
    
		Addr:         ":8081",
		Handler:      router02(),
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
	}

	// 借助 errgroup.Group 或者自行开启两个 goroutine 分别启动两个服务
	g.Go(func() error {
    
    
		// 启动 server01 服务
		return server01.ListenAndServe()
	})

	g.Go(func() error {
    
    
		// 启动 server02 服务
		return server02.ListenAndServe()
	})

	// 等待所有 goroutine 完成,并返回可能发生的错误
	if err := g.Wait(); err != nil {
    
    
		log.Fatal(err)
	}
}

Refer to a big guy's blog to organize

Guess you like

Origin blog.csdn.net/m0_63230155/article/details/131732504