Gin入门

Gin入门

声明:本博客为看李文周大佬gin入门视频笔记gin入门

我的代码仓库6月 · 沉着冷静/2023 - 码云 - 开源中国 (gitee.com)

安装

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

第一个Gin实例:

package main

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

func sayHello(c *gin.Context) {
	c.JSON(200, gin.H{
		"message": "hello go",
	})
}

func main() {
	//设置默认路由引擎
	r := gin.Default()
	// 指定用户使用Get访问/hello
	r.GET("/hello", sayHello)
	//启动服务器
	r.Run() //r.Run("9090")
}
RESTful API

REST与技术无关,代表的是一种软件架构风格,REST是Representational State Transfer的简称,中文翻译为“表征状态转移”或“表现层状态转化”。

简单来说,REST的含义就是客户端与Web服务器之间进行交互的时候,使用HTTP协议中的4个请求方法代表不同的动作。

  • GET用来获取资源
  • POST用来新建资源
  • PUT用来更新资源
  • DELETE用来删除资源。

只要API程序遵循了REST风格,那就可以称其为RESTful API。

实例

基于RESTful API的框架:图书管理系统

请求方法URL含义
GET/book查询书籍信息
POST/book创建书籍信息
PUT/book更新书籍信息
DELETE/book删除书籍信息
func main() {
	//设置默认路由引擎
	r := gin.Default()
	// 指定用户使用Get访问/hello
	r.GET("/hello", sayHello)
	//图书管理系统
	r.GET("/book", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method":  "GET",
			"message": "查询书籍",
		})
	})
	r.POST("/book", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method":  "POST",
			"message": "创建书籍",
		})
	})
	r.PUT("/book", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method":  "PUT",
			"message": "更新书籍",
		})
	})
	r.DELETE("/book", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method":  "DELETE",
			"message": "删除书籍",
		})
	})
	//启动服务器
	r.Run() //r.Run("9090")
}

使用ApiPost进行测试

GET:

image-20240601173500222

POST:

image-20240601173515559

PUT:

image-20240601173529425

DELETE:

image-20240601173546761

Go渲染

html渲染实例

{{ . }}

package main

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

func sayHello(w http.ResponseWriter, r *http.Request) {
	//解析模版
	t, err := template.ParseFiles("./hello.tmpl")
	if err != nil {
		panic(err)
	}
	//渲染模版
	name := "张三"
	err = t.Execute(w, name)
	if err != nil {
		panic(err)
	}
}

func main() {
	http.HandleFunc("/", sayHello)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}
#hello.tmpl
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <tittle>Hello GO</tittle>
</head>
<body>
    <p>Hello {{ . }}</p>
</body>
</html>

image-20240601181600253

暂时简单理解为字符串替换。

解析结构体
type User struct {
	Name   string
	Gender string
	Age    int64
}

func sayHello(w http.ResponseWriter, r *http.Request) {
	//解析模版
	t, err := template.ParseFiles("./hello.tmpl")
	if err != nil {
		panic(err)
	}
	//渲染模版
	u1 := User{
		Name:   "张三",
		Gender: "男",
		Age:    18,
	}
	//name := "张三"
	err = t.Execute(w, u1)
	if err != nil {
		panic(err)
	}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<body>
    <p>Hello {{ .Name }}</p>
    <p>年龄:{{ .Age }}</p>
    <p>性别:{{ .Gender }}</p>
</body>
</html>

注意:结构体首字母需要大写

如果它的首字母是小写,那么它就是未导出的(即字段名首字母为小写)。

在 Go 语言中,未导出的字段和方法只能在同一个包内访问。

解析map
func sayHello(w http.ResponseWriter, r *http.Request) {
	//解析模版
	t, err := template.ParseFiles("./hello.tmpl")
	if err != nil {
		panic(err)
	}
	//渲染模版
	m1 := map[string]interface{}{
		"name":   "李四",
		"gender": "女",
		"age":    20,
	}
	//name := "张三"
	err = t.Execute(w, m1)
	if err != nil {
		panic(err)
	}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<body>
    <p>Hello {{ .name }}</p>
    <p>年龄:{{ .age }}</p>
    <p>性别:{{ .gender }}</p>
</body>
</html>

注意:首字母无需大写,因为map通过key寻找value

空接口自定义
func sayHello(w http.ResponseWriter, r *http.Request) {
	//解析模版
	t, err := template.ParseFiles("./hello.tmpl")
	if err != nil {
		panic(err)
	}
	//渲染模版
	u1 := User{
		Name:   "张三",
		Gender: "男",
		Age:    18,
	}
	m1 := map[string]interface{}{
		"name":   "李四",
		"gender": "女",
		"age":    20,
	}
	//name := "张三"
	err = t.Execute(w, map[string]interface{}{
		"u1": u1,
		"m1": m1,
	})
	if err != nil {
		panic(err)
	}
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<body>
    <p>Hello {{ .m1.name }}</p>
    <p>年龄:{{ .m1.age }}</p>
    <p>性别:{{ .m1.gender }}</p>

    <p>Hello {{ .u1.Name }}</p>
    <p>年龄:{{ .u1.Age }}</p>
    <p>性别:{{ .u1.Gender }}</p>
</body>
</html>

image-20240601184409395

常用语法
注释
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<body>
    <p>Hello {{ .m1.name }}</p>
    <p>年龄:{{ .m1.age }}</p>
    <p>性别:{{ .m1.gender }}</p>
{{/*这是一条注释*/}}
    <p>Hello {{ .u1.Name }}</p>
    <p>年龄:{{ .u1.Age }}</p>
    <p>性别:{{ .u1.Gender }}</p>
</body>
</html>
变量
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<body>
    <p>Hello {{ .m1.name }}</p>
    <p>年龄:{{ .m1.age }}</p>
    <p>性别:{{ .m1.gender }}</p>
{{/*这是一条注释*/}}
    <p>Hello {{ .u1.Name }}</p>
    <p>年龄:{{ .u1.Age }}</p>
    <p>性别:{{ .u1.Gender }}</p>
<hr>
{{/*这是两个变量*/}}
{{ $v1 := 20 }}
{{ $age := .m1.age}}
</body>
</html>
移除空格
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<hr>
    {{/*name的空格被移除*/}}
    <p>Hello {{- .m1.name -}}</p>
</body>
</html>

image-20240601184907784

条件判断
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<hr>
    {{/*name的空格被移除*/}}
    <p>Hello {{- .m1.name -}}</p>
    <p>年龄:{{ .m1.age }}</p>
    <p>性别:{{ .m1.gender }}</p>
{{/*这是一条注释*/}}
    <p>Hello {{ .u1.Name }}</p>
    <p>年龄:{{ .u1.Age }}</p>
    <p>性别:{{ .u1.Gender }}</p>
<hr>
{{/*这是两个变量*/}}
{{ $v1 := 20 }}
{{ $age := .m1.age}}
{{ if $v1}}
{{ $v1 }}
{{else}}
啥也没有
{{end}}
</body>
</html>

image-20240601185151291

当然也有else if

比较函数

例如

lt:小于

gt:大于

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<hr>
    {{/*name的空格被移除*/}}
    <p>Hello {{- .m1.name -}}</p>
    <p>年龄:{{ .m1.age }}</p>
    <p>性别:{{ .m1.gender }}</p>
{{/*这是一条注释*/}}
    <p>Hello {{ .u1.Name }}</p>
    <p>年龄:{{ .u1.Age }}</p>
    <p>性别:{{ .u1.Gender }}</p>
<hr>
{{/*这是两个变量*/}}
{{ $v1 := 20 }}
{{ $age := .m1.age}}
{{ if $v1}}
{{ $v1 }}
{{else}}
啥也没有
{{end}}
{{ if lt .m1.age 20 }}
好好学习
{{ else if gt .m1.age 22 }}
好好工作
{{ else }}
666
{{end}}
</body>
</html>

image-20240601190056919

range
//...
hobbylist := []string{
		"篮球",
		"足球",
		"乒乓球",
	}
	//name := "张三"
	err = t.Execute(w, map[string]interface{}{
		"u1":    u1,
		"m1":    m1,
		"hobby": hobbylist,
	})
//...
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
//...
<hr>
{{ range $idx, $hobby := .hobby }}
    <p>{{$idx}} - {{$hobby}}</p>
{{end}}
</body>
</html>

image-20240601191028678

也可以加else

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
//...
<hr>
{{ range $idx, $hobby := .hobby }}
    <p>{{$idx}} - {{$hobby}}</p>
{{else}}
    空空如也
{{end}}
</body>
</html>
with

引一个作用域

//...
{{with .m1}}
<p>{{ .name }}</p>
<p>{{ .gender }}</p>
<p>{{ .age }}</p>
{{end}}
//...

方便替换

//...
<p>Hello {{- .m1.name -}}</p>
<p>年龄:{{ .m1.age }}</p>
<p>性别:{{ .m1.gender }}</p>
//...

例如:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<hr>
    {{/*name的空格被移除*/}}
    <p>Hello {{- .m1.name -}}</p>
    <p>年龄:{{ .m1.age }}</p>
    <p>性别:{{ .m1.gender }}</p>
{{/*这是一条注释*/}}
    <p>Hello {{ .u1.Name }}</p>
    <p>年龄:{{ .u1.Age }}</p>
    <p>性别:{{ .u1.Gender }}</p>
<hr>
{{/*这是两个变量*/}}
{{ $v1 := 20 }}
{{ $age := .m1.age}}
{{ if $v1}}
{{ $v1 }}
{{else}}
啥也没有
{{end}}
{{ if lt .m1.age 20 }}
好好学习
{{ else if gt .m1.age 22 }}
好好工作
{{ else }}
666
{{end}}
<hr>
{{ range $idx, $hobby := .hobby }}
    <p>{{$idx}} - {{$hobby}}</p>
{{else}}
    空空如也
{{end}}
<hr>
{{with .m1}}
<p>{{ .name }}</p>
<p>{{ .gender }}</p>
<p>{{ .age }}</p>
{{end}}

</body>
</html>

image-20240601191435873

自定义函数
func f1(w http.ResponseWriter, r *http.Request) {
	//定义函数
	kua := func(name string) (string, error) {
		return name + "年轻帅气", nil
	}
	//定义模版
	//解析模版
	t := template.New("f.tmpl")
	// 告诉引擎多了一个自定义函数kua
	t.Funcs(template.FuncMap{
		"kua": kua,
	})
	_, err := t.ParseFiles("./f.tmpl")
	if err != nil {
		panic(err)
	}
	name := "张三"
	//渲染模版
	t.Execute(w, name)
}
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>Hello Go Template</title>
</head>
<body>
    {{ kua . }}
</body>
</html>

image-20240601193314321

模版嵌套

f.tmpl

<!DOCTYPE html>
<html lang="zh-CN">
<body>

<h1>测试嵌套模版</h1>
<hr>
{{/*嵌套了另外一个单独的模版文件*/}}
{{template "ul.tmpl"}}
<hr>
{{/*嵌套另一个define定义的模版*/}}
{{template "ol.tmpl"}}
<div>你好 {{ . }}</div>
</body>
</html>
{{/*通过define定义一个模版*/}}
{{ define "ol.tmpl"}}
    <ol>
        <li>吃饭</li>
        <li>睡觉</li>
        <li>编程</li>
    </ol>
{{end}}

ul.tmpl

<h3>
    <li>注册</li>
    <li>登录</li>
    <li>退出</li>
</h3>
func demo(w http.ResponseWriter, r *http.Request) {
	//被包含的写在后面
	t, err := template.ParseFiles("./f.tmpl", "./ul.tmpl")
	if err != nil {
		panic(err)
	}
	name := "李四"
	t.Execute(w, name)
}

func main() {
	http.HandleFunc("/", f1)
	http.HandleFunc("/tmpl", demo)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

image-20240601195016273

模版继承:block

就相当于以下两个页面中,只有标题内容是不同的,我们完全没必要再写一个重复内容的前端页面

<!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;
            position: relative;
        }
        .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">
        <h1>{{ . }}</h1>                  ///...不同
    </div>
</div>
</body>
</html>
<!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;
            position: relative;
        }
        .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">
        <h1>{{ . }}</h1>                  ///...不同
    </div>
</div>
</body>
</html>
package main

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

func index(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.ParseFiles("./index.tmpl")
	if err != nil {
		panic(err)
	}
	msg := "index页面"
	//渲染模版
	t.Execute(w, msg)
}
func home(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.ParseFiles("./home.tmpl")
	if err != nil {
		panic(err)
	}
	msg := "home页面"
	//渲染模版
	t.Execute(w, msg)
}

func main() {
	http.HandleFunc("/index", index)
	http.HandleFunc("/home", home)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

此时我们就可以使用block了。

base

{{/*或者使用define重命名文件*/}}
{{/*{{define "xx"}}*/}}
<!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;
            position: relative;
        }
        .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>

index2.tmpl

{{/*继承根模版*/}}
{{/*记得接收数据*/}}
{{template "base.tmpl" .}}//这个.就是接收来的数据
{{/*重新定义根模版*/}}
{{define "content"}}
    <h1>这是index2页面</h1>
    <p>Hello {{ . }}</p>
{{end}}

home2.tmpl

{{/*继承根模版*/}}
{{/*记得接收数据*/}}
{{template "base.tmpl" .}}
{{/*重新定义根模版*/}}
{{define "content"}}
    <h1>这是home2页面</h1>
    <p>Hello {{ . }}</p>
{{end}}

main

package main

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

func index(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.ParseFiles("./f.tmpl")
	if err != nil {
		panic(err)
	}
	msg := "index页面"
	//渲染模版
	t.Execute(w, msg)
}
func home(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.ParseFiles("./f.tmpl")
	if err != nil {
		panic(err)
	}
	msg := "home页面"
	//渲染模版
	t.Execute(w, msg)
}

func index2(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.ParseFiles("./template/base.tmpl", "./template/index.tmpl")
	if err != nil {
		panic(err)
	}
	msg := "张三"
	//渲染模版
	t.ExecuteTemplate(w, "index.tmpl", msg)
}
func home2(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.ParseFiles("./template/base.tmpl", "./template/home.tmpl")
	if err != nil {
		panic(err)
	}
	msg := "李四"
	//渲染模版
	t.ExecuteTemplate(w, "home.tmpl", msg)
}

func main() {
	http.HandleFunc("/index", index)
	http.HandleFunc("/home", home)
	http.HandleFunc("/index2", index2)
	http.HandleFunc("/home2", home2)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}
模版补充
修改默认标识符
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>修改模拟引擎的标识符</title>
</head>
<body>
<div>Hello {[ . ]}</div>
</body>
</html>
package main

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

func index(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.New("index.tmpl").
		Delims("{[", "]}").
		ParseFiles("./index.tmpl")
	if err != nil {
		panic(err)
	}
	//渲染模版
	name := "张三"
	t.Execute(w, name)

}

func main() {
	http.HandleFunc("/index", index)
	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}
text/template和html/template的区别

html/template针对的是需要返回HTML内容的场景,在模板渲染过程中会对一些有风险的内容进行转义,以此来防范跨站脚本攻击。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>xss</title>
</head>
<body>
{{ . }}
</body>
</html>

这个时候传入一段JS代码并使用html/template去渲染该文件,会在页面上显示出转义后的JS内容。 <script>alert('123')</script> 这就是html/template为我们做的事。

但是在某些场景下,我们如果相信用户输入的内容,不想转义的话,可以自行编写一个safe函数,手动返回一个template.HTML类型的内容。示例如下:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <title>xss</title>
</head>
<body>
    {{ .str1 }}
    {{/* "|" 管道符*/}}
    {{ .str2 | safe }}
</body>
</html>
package main

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

func index(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版
	t, err := template.New("index.tmpl").
		Delims("{[", "]}").
		ParseFiles("./index.tmpl")
	if err != nil {
		panic(err)
	}
	//渲染模版
	name := "张三"
	err = t.Execute(w, name)
	if err != nil {
		panic(err)
	}
}

func xss(w http.ResponseWriter, r *http.Request) {
	//定义模版
	//解析模版前自定义一个函数
    
	t, err := template.New("xss.tmpl").Funcs(template.FuncMap{
		"safe": func(str string) template.HTML {
			return template.HTML(str)
		},
	}).ParseFiles("./xss.tmpl")//解析模版
	if err != nil {
		panic(err)
	}
	//渲染模版
	str1 := "<script></<script>>"
	str2 := "<a href='qq.com'>QQ</a>"
	t.Execute(w, map[string]string{
		"str1": str1,
		"str2": str2,
	})

}

func main() {
	http.HandleFunc("/index", index)
	http.HandleFunc("/xss", xss)

	err := http.ListenAndServe(":8080", nil)
	if err != nil {
		panic(err)
	}
}

这样我们只需要在模板文件不需要转义的内容后面使用我们定义好的safe函数就可以了。

safe前:全部转义

image-20240601213622233

safe后:只转义,我们想转义的

image-20240601213708358

Gin渲染

单个模版渲染

模版解析

r.LoadHTMLFiles("template/index.tmpl") 

模版渲染

Gin框架中使用LoadHTMLGlob()或者LoadHTMLFiles()方法进行HTML模板渲染。

r.GET("/index", func(c *gin.Context) {
    c.HTML(http.StatusOK, "index.tmpl", gin.H{
        "tittle": "qq.com",
    })
})

结果

image-20240601215739100

package main

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

func main() {
	r := gin.Default()
	//模版解析
	r.LoadHTMLFiles("./templates/index.tmpl")
	//模版渲染
	r.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.tmpl", gin.H{
			"tittle": "qq.com",
		})
	})
	//运行
	r.Run()
}
<!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>index</title>
</head>
<body>
{{ .tittle }}
</body>
</html>
多个模版渲染

image-20240601221432914

package main

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

func main() {
	r := gin.Default()
	//模版解析
	//r.LoadHTMLFiles("./templates/index.tmpl")
	r.LoadHTMLGlob("templates/**/*")
	//模版渲染
	r.GET("/posts/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
			"title": "/posts/index:qq.com",
		})
	})
	r.GET("/users/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
			"title": "/users/index:qq.com",
		})
	})
	//运行
	r.Run()
}

posts/index.tmpl

{{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">
    <title>index</title>
</head>
<body>
{{ .title }}
</body>
</html>
{{end}}

image-20240601221544263

users/index.tmpl

{{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>index</title>
</head>
<body>
{{ .title }}
</body>
</html>
{{end}}

image-20240601221516044

自定义模版函数
//gin框架中添加自定义函数
	r.SetFuncMap(template.FuncMap{
		"safe": func(str string) template.HTML {
			return template.HTML(str)
		},
	})
package main

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

func main() {
	r := gin.Default()
	//gin框架中添加自定义函数
	r.SetFuncMap(template.FuncMap{
		"safe": func(str string) template.HTML {
			return template.HTML(str)
		},
	})
	//模版解析
	//r.LoadHTMLFiles("./templates/index.tmpl")
	r.LoadHTMLGlob("templates/**/*")
	//模版渲染
	r.GET("/posts/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
			"title": "<a href='qq.com'>QQ</a>",
		})
	})
	r.GET("/users/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
			"title": "/users/index:qq.com",
		})
	})
	//运行
	r.Run()
}
<body>
{{ .title | safe }}
</body>

image-20240601234134563

静态文件的处理
//加载静态文件
	r.Static("/xxx", "./statics")
func main() {
	r := gin.Default()
	//加载静态文件
	r.Static("/xxx", "./statics")
	//gin框架中添加自定义函数
	r.SetFuncMap(template.FuncMap{
		"safe": func(str string) template.HTML {
			return template.HTML(str)
		},
	})
	//模版解析
	//r.LoadHTMLFiles("./templates/index.tmpl")
	r.LoadHTMLGlob("templates/**/*")
	//模版渲染
	r.GET("/posts/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{
			"title": "<a href='qq.com'>QQ</a>",
		})
	})
	r.GET("/users/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "users/index.tmpl", gin.H{
			"title": "/users/index:qq.com",
		})
	})
	//运行
	r.Run()
}

image-20240601235408996

css:

body {
    background-color: antiquewhite;
}

js:

alert(123);

tmpl:

{{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>index</title>
</head>
<body>
{{ .title | safe }}
<script src="/xxx/index.js"></script>
</body>
</html>
{{end}}

image-20240601235518665

模版继承

Gin框架默认都是使用单模板,如果需要使用block template功能,可以通过"github.com/gin-contrib/multitemplate"库实现。

例如:

在网上下载一个前端模版

把解压之后index.html改名为home.html,放在gin_demo7/templates/posts路径下,其他的放在gin_demo7/statics路径下。

image-20240602002616069

增加一个GET即可。

//从网上下载的模版
r.GET("/home", func(c *gin.Context) {
    c.HTML(http.StatusOK, "home.html", nil)
})

因为我们找静态资源是先去statics中寻找,但是我们设定了静态资源是在/xxx下寻找的,所以去下载的index.html中把一些css/改为/xxx/css

就可以呈现出:(当然,有点前端部分是不会显示的,ps:我不会改)

image-20240602003032014

Gin框架操作

返回JSON
package main

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

func main() {
	r := gin.Default()
	r.GET("/json", func(c *gin.Context) {
		data := map[string]interface{}{
			"name":   "张三",
			"age":    18,
			"gender": "男",
		}
		c.JSON(http.StatusOK, data)
	})
	r.Run()
}

image-20240602010120179

不用切片,也可以使用gin.H或者结构体

data := gin.H{
    "name":   "张三",
    "age":    18,
    "gender": "男",
}
--------------------------------------------
type msg struct {
    Name    string
    Message string
    Age     int
}
r.GET("/another_json", func(c *gin.Context) {
    data := msg{
        Name:    "张三",
        Message: "hello go",
        Age:     18,
    }
    c.JSON(http.StatusOK, data)
})

注意:结构体命名不能小写,比如Name不能写出name。

如果非得写小写,就使用tag即可。

type msg struct {
    Name    string `json:"name"`
    Message string
    Age     int
}
获取querystring参数
package main

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

func main() {
	r := gin.Default()
	r.GET("/web", func(c *gin.Context) {
		//获取浏览器那边发请求携带的query string参数
		name := c.Query("query")
		c.JSON(http.StatusOK, gin.H{
			"name": name,
		})
	})
	r.Run()
}

image-20240602013042495

设置默认值:

name := c.DefaultQuery("query", "李四")

image-20240602013303077

GetQuery

name, ok := c.GetQuery("query")
if !ok {
    name = "!ok"
}

image-20240602013535250

&英文AND符

func main() {
	r := gin.Default()
	r.GET("/web", func(c *gin.Context) {
		//获取浏览器那边发请求携带的query string参数
		//name := c.Query("query")
		//name := c.DefaultQuery("query", "李四")
		//name, ok := c.GetQuery("query")
		name := c.Query("query")
		age := c.Query("age")
		c.JSON(http.StatusOK, gin.H{
			"name": name,
			"age":  age,
		})
	})
	r.Run()
}

image-20240602014056574

获取form参数

PostForm

r.GET("/login", func(c *gin.Context) {
    c.HTML(http.StatusOK, "login.html", nil)
})
r.POST("/login", func(c *gin.Context) {
    username := c.PostForm("username")
    password := c.PostForm("password")
    c.HTML(http.StatusOK, "index.html", gin.H{
        "Name":     username,
        "Password": password,
    })
})

html

//login
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>
<form action="/login" method="post" novalidate autocomplete="off">
    <div>
        <label for="username">username:</label>
        <input type="text" name="username" id="username">
    </div>

    <div>
        <label for="password">password:</label></input>
        <input type="password" name="password" id="password">
    </div>
    <div>
        <input type="submit" value="登录">
    </div>
</form>
</body>
</html>
//index
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
<p>Hello, {{ .Name }} !</p>
<p>密码:{{ .Password }} </p>
</body>
</html>

image-20240602025235632

DefaultPostForm

我此时没有填密码,就返回一个空

r.GET("/login", func(c *gin.Context) {
    c.HTML(http.StatusOK, "login.html", nil)
})
r.POST("/login", func(c *gin.Context) {
    username := c.DefaultPostForm("username", "somebody")
    password := c.DefaultPostForm("password", "***")
    c.HTML(http.StatusOK, "index.html", gin.H{
        "Name":     username,
        "Password": password,
    })
})

image-20240602030840285

我此时没有填密码,找不到对应表单,就使用默认的***

username := c.DefaultPostForm("username", "somebody")
password := c.DefaultPostForm("xxx", "***")

image-20240602030931171

GetPostForm

r.POST("/login", func(c *gin.Context) {
    username, ok := c.GetPostForm("username")
    if !ok {
        username = "李四"
    }
    password, ok := c.GetPostForm("password")
    if !ok {
        password = "20"
    }
    println(username, "-----------", password)
    c.HTML(http.StatusOK, "index.html", gin.H{
        "Name":     username,
        "Password": password,
    })
})

image-20240602031227760

获取URI路径参数
func main() {
	r := gin.Default()
	r.GET("/:name/:age", func(c *gin.Context) {
		//获取路径参数
		name := c.Param("name")
		age := c.Param("age")
		c.JSON(http.StatusOK, gin.H{
			"name": name,
			"age":  age,
		})
	})
	r.Run()
}

image-20240602161834052

防止路由匹配冲突:

//Param返回的都是string类型
func main() {
	r := gin.Default()
	r.GET("/user/:name/:age", func(c *gin.Context) {
		//获取路径参数
		name := c.Param("name")
		age := c.Param("age")
		c.JSON(http.StatusOK, gin.H{
			"name": name,
			"age":  age,
		})
	})
	r.GET("/blog/:year/:month", func(c *gin.Context) {
		year := c.Param("year")
		month := c.Param("month")
		c.JSON(http.StatusOK, gin.H{
			"year":  year,
			"month": month,
		})
	})
	r.Run()
}

image-20240602162111449

image-20240602162143012

参数绑定

之前获取参数的方法

type User struct {
	Name     string
	Password string
}

func main() {
	r := gin.Default()
	r.GET("/user", func(c *gin.Context) {
		username := c.Query("username")
		password := c.Query("password")
		u1 := User{
			Name:     username,
			Password: password,
		}
		fmt.Println(u1)
		c.JSON(http.StatusOK, gin.H{
			"message": "ok",
		})
	})
}

image-20240602163543007

image-20240602163705748

但是当请求中携带的量比较大,就需要使用gin框架中的绑定函数:ShouldBind

var u User
c.ShouldBind(&u)

注意:记得传指针,否则对形参的修改,不会影响到实参。

c.ShouldBind通过反射获取结构体中的字段,所以结构体内变量首字母大写。

可以使用tag,让别人查反射时,可以一一对应起来。

type User struct {
	Username string `form:"username"`
	Password string `form:"password"`
}
func main() {
	r := gin.Default()
	r.GET("/user", func(c *gin.Context) {
		//username := c.Query("username")
		//password := c.Query("password")
		//u1 := User{
		//	username: username,
		//	password: password,
		//}
		//fmt.Printf("%#v\n", u1)
		var u User
		err := c.ShouldBind(&u)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
		} else {
			c.JSON(http.StatusOK, gin.H{
				"status": "ok",
			})
		}
	})
	r.Run()
}

image-20240602164622118

我们也可以通过获取post方法的form表单

下面演示ApiPost:

image-20240602165216463

image-20240602165226551

再来处理最常处理的json格式的数据

type User struct {
	Username string `form:"username" json:"username"`
	Password string `form:"password" json:"password"`
}
r.POST("/json", func(c *gin.Context) {
    var u User
    err := c.ShouldBind(&u)
    if err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
    } else {
        c.JSON(http.StatusOK, gin.H{
            "status": "ok",
        })
    }
    fmt.Printf("%#v\n", u)
})

image-20240602165605234

image-20240602165626731

文件上传

单个文件上传

//SaveUploadedFile
func main() {
	r := gin.Default()
	r.LoadHTMLGlob("index.html")
	r.GET("/index", func(c *gin.Context) {
		c.HTML(http.StatusOK, "index.html", nil)
	})
	r.POST("/upload", func(c *gin.Context) {
		//从请求中读取文件
		f, err := c.FormFile("f1")
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{
				"error": err.Error(),
			})
		} else {
			//把读取到的文件保存在服务器本地
			//filepath := fmt.Sprintf("./%s", f.Filename)
			filepath := path.Join("./", f.Filename)
			_ = c.SaveUploadedFile(f, filepath)
			c.JSON(http.StatusOK, gin.H{
				"status": "ok",
			})
		}
	})
	r.Run()
}
// 或者使用MultipartMemory
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20  // 8 MiB

image-20240602171326712

image-20240602171335078

服务器本地路径中就多了一个图片:

image-20240602171357685

多个文件上传

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("C:/tmp/%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()
}
重定向
  • HTTP重定向

例1:

func main() {
	r := gin.Default()
	r.GET("/index", func(c *gin.Context) {
		//c.JSON(http.StatusOK, gin.H{
		//	"status": "ok",
		//})
		c.Redirect(http.StatusMovedPermanently, "http://www.qq.com")
	})
	r.Run()
}

image-20240602172256356

回车

image-20240602172309650

image-20240602172635065

  • 路由重定向

例2:

func main() {
	r := gin.Default()
	r.GET("/index", func(c *gin.Context) {
		//c.JSON(http.StatusOK, gin.H{
		//	"status": "ok",
		//})
		c.Redirect(http.StatusMovedPermanently, "http://www.qq.com")
	})
	r.GET("/a", func(c *gin.Context) {
		//从a跳到b
		c.Request.URL.Path = "/b" // 修改请求的URI
		r.HandleContext(c)        //继续后面的处理
	})
	r.GET("/b", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"message": "你跳到了b",
		})
	})
	r.Run()
}

image-20240602172826371

image-20240602172856093

路由
  • 普通路由
func main() {
	r := gin.Default()
	r.GET("/index", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method": "GET",
		})
	})
	r.POST("/index", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method": "POST",
		})
	})
	r.DELETE("/index", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method": "DELETE",
		})
	})
	r.PUT("/index", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"method": "PUT",
		})
	})
	r.Run()
}

image-20240602174640057

image-20240602174702688

  • 匹配所有的请求方法:
r.Any("/user", func(c *gin.Context) {
    switch c.Request.Method {
        case http.MethodGet:
        c.JSON(http.StatusOK, gin.H{
            "method": "GET",
        })
        case http.MethodPost:
        r.POST("/index", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "method": "POST",
            })
        })
        case http.MethodDelete:
        r.DELETE("/index", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{
                "method": "DELETE",
            })
        })
        //...
    }
})
  • 匹配失败
r.NoRoute(func(c *gin.Context) {
    c.JSON(http.StatusNotFound, gin.H{
        "msg": "你访问的页面不存在",
    })
})

image-20240602174729647

image-20240602174759465

  • 路由组

假设一个视频网站的页面所需的路由:

r.GET("/video/a", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{ "msg": "/video/a" })
})
r.GET("/video/b", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{ "msg": "/video/b" })
})
r.GET("/video/c", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{ "msg": "/video/c" })
})
r.GET("/video/d", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{ "msg": "/video/d" })
})
//...

这样写,即繁琐又没有规矩,所以我们使用路由组:

//路由组
videoGroup := r.Group("/video")
{
    videoGroup.GET("/a", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"msg": "/video/a"})
    })
    videoGroup.GET("/b", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"msg": "/video/b"})
    })
    videoGroup.GET("/c", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"msg": "/video/c"})
    })
    videoGroup.GET("/d", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{"msg": "/video/d"})
    })
}

image-20240602175822922

image-20240602175833574

image-20240602175857841

Gin中间件
中间件

Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。

GET函数

// GET is a shortcut for router.Handle("GET", path, handlers).
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
	return group.handle(http.MethodGet, relativePath, handlers)
}

第二个参数必须是HandlerFunc类型

// HandlerFunc defines the handler used by gin middleware as return value.
type HandlerFunc func(*Context)

所以说

// handlerFunc
func indexHandler(c *gin.Context) {
	c.JSON(http.StatusOK, gin.H{
		"msg": "index",
	})
}

func main() {
	r := gin.Default()
	r.GET("/index", indexHandler)
	r.Run()
}

定义一个中间件m1,记录接口耗时

// handlerFunc
func indexHandler(c *gin.Context) {
	fmt.Println("indexHandler ...")
	c.JSON(http.StatusOK, gin.H{
		"msg": "home",
	})
}

// 中间件m1,统计耗时
func m1(c *gin.Context) {
	fmt.Println("m1 start...")
	// 计时
	start := time.Now()
	c.Next() // 调用该请求剩余的处理程序
	//c.Abort() // 不调用该请求的剩余处理程序
	cost := time.Since(start)
	fmt.Println(cost)
	fmt.Println("m1 end...")
}

func main() {
	r := gin.Default()
	r.GET("/home", m1, indexHandler)
	r.Run()
}

image-20240602190919424

中间件调用流程:

m1:start:time.Now() -> m1:c.Next() -> indexHandler() -> m1:cost:time.Since()

注册中间件
  • 全局注册中间接函数

例1:

func main() {
	r := gin.Default()
	r.Use(m1) //全局注册中间件函数m1
	r.GET("/home", m1, indexHandler)
	r.GET("/shop", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"msg": "shop",
		})
	})
	r.GET("/user", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"msg": "user",
		})
	})
	r.Run()
}

分别访问"/shop""/user"

image-20240602193737233

例2:

// 中间件m1,统计耗时
func m1(c *gin.Context) {
	fmt.Println("m1 start...")
	// 计时
	start := time.Now()
	c.Next() // 调用该请求剩余的处理程序
	//c.Abort() // 不调用该请求的剩余处理程序
	cost := time.Since(start)
	fmt.Println(cost)
	fmt.Println("m1 end...")
}
// 中间件m2,统计耗时
func m2(c *gin.Context) {
	fmt.Println("m2 start...")
	c.Next()
	fmt.Println("m2 end...")
}

func main() {
	r := gin.Default()
	r.Use(m1, m2) //全局注册中间件函数m1/m2
	r.GET("/home", m1, indexHandler)
	r.GET("/shop", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"msg": "shop",
		})
	})
	r.GET("/user", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"msg": "user",
		})
	})
	r.Run()
}

执行流程:main -> m1:Next() -> m2:Next() -> shop() -> m2 end.. -> m1 end..

image-20240602194516402

image-20240602195922743

abort()

// 中间件m2,统计耗时
func m2(c *gin.Context) {
	fmt.Println("m2 start...")
	//c.Next()
	c.Abort() //不调用该请求的剩余处理程序
	fmt.Println("m2 end...")
}

main -> m1:Next() -> m2:Abort -> m2 end.. -> m1 end..

image-20240602200615472

如果此时m2也不想执行,在Abort()后面加上return即可

// 中间件m2
func m2(c *gin.Context) {
	fmt.Println("m2 start...")
	//c.Next()
	c.Abort() //不调用该请求的剩余处理程序
	return
	fmt.Println("m2 end...")
}

image-20240602205457645

image-20240602205513542

  • 为某个路由单独注册
r.GET("/user", m1, func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "msg": "user",
    })
})
  • 为路由组注册中间件

写法1

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

写法2

shopGroup := r.Group("/shop")
shopGroup.Use(m1)
{
    shopGroup.GET("/index", func(c *gin.Context) {...})
    ...
}
中间件一般用法

写法:

// doCheck开关控制
func outMiddleWare(doCheck bool) gin.HandlerFunc {
	//连接数据库
	//或者其他工作
	return func(c *gin.Context) {
		if doCheck {
			//是否登录
			//if 是登录用户
			//c.Next
			//else
			//c.Abort
		} else {
			c.Next()
		}
	}
}

func main() {
	r := gin.Default()
	r.Use(m1, m2, outMiddleWare(false)) //全局注册中间件函数m1/m2
	r.GET("/home", m1, indexHandler)
	r.GET("/shop", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"msg": "shop",
		})
	})
	r.GET("/user", func(c *gin.Context) {
		c.JSON(http.StatusOK, gin.H{
			"msg": "user",
		})
	})
	r.Run()
}

用法:

从一个中间件设置信息,在另一个中间件拿到这个信息

c.Set("name", "张三")
///
name, ok := c.Get("name")
if !ok {
    name = "匿名用户"
}
// handlerFunc
func indexHandler(c *gin.Context) {
	fmt.Println("indexHandler ...")
	name, ok := c.Get("name")
	if !ok {
		name = "匿名用户"
	}
	c.JSON(http.StatusOK, gin.H{
		"name": name,
	})
}

// 中间件m1,统计耗时
func m1(c *gin.Context) {
	fmt.Println("m1 start...")
	// 计时
	start := time.Now()
	c.Next() // 调用该请求剩余的处理程序
	//c.Abort() // 不调用该请求的剩余处理程序
	cost := time.Since(start)
	fmt.Println(cost)
	fmt.Println("m1 end...")
}

// 中间件m2
func m2(c *gin.Context) {
	fmt.Println("m2 start...")
	c.Set("name", "张三")
	c.Next()
	//c.Abort() //不调用该请求的剩余处理程序
	return
	fmt.Println("m2 end...")
}

// doCheck开关控制
func outMiddleWare(doCheck bool) gin.HandlerFunc {
	//连接数据库
	//或者其他工作
	return func(c *gin.Context) {
		if doCheck {
			//是否登录
			//if 是登录用户
			//c.Next
			//else
			//c.Abort
		} else {
			c.Next()
		}
	}
}

func main() {
	r := gin.Default()
	r.Use(m1, m2, outMiddleWare(false)) //全局注册中间件函数m1/m2
	r.GET("/home", m1, indexHandler)
	r.Run()
}

image-20240602211334833

中间件注意事项

gin默认中间件

gin.Default()默认使用了LoggerRecovery中间件

  • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release
  • Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。
// Default returns an Engine instance with the Logger and Recovery middleware already attached.
func Default(opts ...OptionFunc) *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine.With(opts...)
}

如果想使用一个无中间件的路由:

r := gin.New()

gin中间件中使用goroutine

当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。(并发时不安全)

  • 8
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值