前言
从Java转Go,初学Gin框架,最关心的操作就是接收请求与返回响应的问题,本文主要简单介绍一下Gin框架的基本使用。Gin是一个golang的微框架,封装比较优雅,API友好,源码注释比较明确,具有快速灵活,容错方便等特点,对于golang而言,web框架的依赖要远比Python,Java之类的要小。自身的net/http足够简单,性能也非常不错。借助框架开发,不仅可以省去很多常用的封装带来的时间,也有助于团队的编码风格和形成规范。
Go中下载Gin比较简单,通过go get
即可拉取到本地(前提是本地的工作区、代理等环境已经配置好)
go get -u github.com/gin-gonic/gin
一、路由
前端请求到达后端如何被处理,这就需要进行路由去匹配相应的handler方法执行了。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// 1.创建路由
r := gin.Default()
// 2.绑定路由规则,执行的函数
// gin.Context,封装了request和response
r.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "hello World!")
})
// 3.监听端口,默认在8080
r.Run(":8888")
}
二、Restful风格的API
gin支持Restful风格的API。即Representational State Transfer的缩写。直接翻译的意思是"表现层状态转化",是一种互联网应用程序的API设计理念:URL定位资源,用HTTP描述操作。
获取: /blog/getXxx -------------->Get blog/Xxx
新增: /blog/addXxx-------------->POST blog/Xxx
修改: /blog/updateXxx-------------->PUT blog/Xxx
删除: /blog/delXxxx -------------->DELETE blog/Xxx
三、参数获取
3.1 API参数
gin.Context中封装了request和response,API中的参数可以通过Context的Param方法来获取API参数。
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
//请求url为:localhost:8888/user/tonghua/233
r.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
//截取,去掉首尾的斜杠 /
action = strings.Trim(action, "/")
c.String(http.StatusOK, name+" is "+action)
})
//请求url为:localhost:8888/blog/2022/4
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,
})
})
//默认为监听8080端口,注意前面的冒号
r.Run(":8888")
}
3.2 URL参数
URL参数可以通过DefaultQuery()
或Query()
方法获取,其中DefaultQuery()
可指定默认参数,如果参数不存在则返回默认值。Query()
获取参数如果不存在,则会返回空串。
r.GET("/hello", func(c *gin.Context) {
name := c.Query("name")
//name := c.DefaultQuery("name","tonghua")
name, ok := c.GetQuery("name")
if !ok {
name = "tonghua" //取不到值,手动赋值
}
c.String(http.StatusOK, name)
})
3.3 表单参数
为了进行模拟这里写了两个简单的页面login.html(登录发送post请求携带表单参数)和index.html(用于登录成功后跳转示例):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form action="/login" method="post" novalidate autocomplete="on">
<div>
<label for="username">username:</label>
<input type="username" name="username" id="username">
</div>
<div>
<label for="password">password:</label>
<input type="password" name="password" id="password">
</div>
<div>
<input type="submit" value="登录">
</div>
</form>
</body>
</html>
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<h1>用户名:{{ .Username }}</h1>
<h1>密码是:{{ .Password }}</h1>
</body>
</html>
表单参数可通过PostForm
进行获取,获取不到返回空。GetPostForm
可以通过bool类型的返回值判断是否获取到参数值。当然也可使用DefaultPostForm
设置默认值,获取不到则返回默认值。默认解析的是x-www-form-urlencoded
或from-data
格式的参数。
r.LoadHTMLFiles("./login.html", "./index.html")
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
r.POST("/login", func(c *gin.Context) {
//获取form表单中的数据,PostForm获取不到数据返回nil
username := c.PostForm("username")
password := c.PostForm("password")
//username1, ok := c.GetPostForm("username") //通过ok判断是否获取到值
//if !ok {
// username1 = "dingli"
//}
//username = c.DefaultPostForm("username","defaultValue") //设置默认值
c.HTML(http.StatusOK, "index.html", gin.H{
"Username": username,
"Password": password,
})
})
HTTP数据传输的几种格式:
application/json
application/x-www-form-urlencoded
application/xml
multipart/form-data
3.4 参数绑定
很多时候,我们请求的参数在后端都是一个model,其实就是一个struct而已,只是前端传递时候参数格式和go中参数格式有些不一样,比如go内部往往为了结构体参数可见,字段的首字母大写(为了反射时能成功获取),而前端中使用的是小写,因此可以借助结构体的tag标签解决。为了能够更方便的获取请求相关参数,提高开发效率,我们可以基于请求的Content-Type
识别请求数据类型并利用反射机制自动提取请求中URI、form表单、JSON、QueryString、XML等参数到结构体中。
3.4.1 Json参数绑定
Json数据绑定有如下三种方式:
c.BindJSON(&userInfo)
c.ShouldBindJSON(&userInfo)
这两种方式进行两次绑定时候,第二次绑定失效。ShouldBindJSON方法是最常用解析JSON数据的方法之一,但在重复调用的情况下会出现EOF的报错,这个原因出在ShouldBindJSON在调用过一次之后context.request.body.sawEOF的值是false导致,所以如果要多次绑定多个变量,需要使用ShouldBindBodyWith,如c.ShouldBindWith(&userInfo,binding.JSON)
var userInfo UserInfo
_ = c.ShouldBindJSON(&userInfo)
var userInfo1 UserInfo
_ = c.ShouldBindJSON(&userInfo1) //第二次绑定会失败,ShouldBindJSON只能绑定一次
绑定JSON的示例:({"username": "tonghua", "password": "123456"})
可直接在postman的body中选择raw写原始json数据进行测试
type UserInfo struct {
Username string `form:"username" json:"username"` //设置form和json使用时名称等方便进行参数绑定 binding:"required"
Password string `form:"password" json:"password"`
}
func main() {
r := gin.Default()
// 绑定表单参数,使用Bind或者ShouldBind方法进行参数绑定
r.POST("/user", func(c *gin.Context) {
var userInfo UserInfo //go中都是值传递,因此需要改变传递地址
if err := c.ShouldBind(&userInfo); err == nil {
fmt.Printf("login info: %#v\n", userInfo)
c.JSON(http.StatusOK, gin.H{
"username": userInfo.Username,
"password": userInfo.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
r.Run(":8081")
}
3.4.2 表单参数绑定
利用Bind
方法即可绑定表单参数,绑定form表单示例 :(username=tonghua&password=123456)
// 定义接收数据的结构体
type Login struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// JSON绑定
r.POST("/loginForm", func(c *gin.Context) {
// 声明接收的变量
var form Login
// Bind()默认解析并绑定form格式
// 根据请求头中content-type自动推断
if err := c.Bind(&form); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if form.User != "root" || form.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
3.4.3 URI参数绑定
绑定URI类型参数可借助于ShouldBindUri
方法完成:
// 定义接收数据的结构体
type Login struct {
// binding:"required"修饰的字段,若接收为空值,则报错,是必须字段
User string `form:"username" json:"user" uri:"user" xml:"user" binding:"required"`
Pssword string `form:"password" json:"password" uri:"password" xml:"password" binding:"required"`
}
func main() {
// 1.创建路由
// 默认使用了2个中间件Logger(), Recovery()
r := gin.Default()
// JSON绑定
r.GET("/:user/:password", func(c *gin.Context) {
// 声明接收的变量
var login Login
// Bind()默认解析并绑定form格式
// 根据请求头中content-type自动推断
if err := c.ShouldBindUri(&login); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 判断用户名密码是否正确
if login.User != "root" || login.Pssword != "admin" {
c.JSON(http.StatusBadRequest, gin.H{"status": "304"})
return
}
c.JSON(http.StatusOK, gin.H{"status": "200"})
})
r.Run(":8000")
}
还有一种是QueryString
类型的参数,示例(/loginQuery?username=tonghua&password=123456)
,所有的绑定都可以使用ShouldBind
方法完成。
四、文件上传
数据格式中,multipart/form-data
格式用于文件上传。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件上传</title>
</head>
<body>
<form action="/single-upload" method="post" enctype="multipart/form-data">
<div><input type="file" name="f1"></div>
<div><input type ="submit" value="上传"></div>
</form>
</body>
</html>
4.1 单文件上传
处理multipart forms提交文件时默认的内存(缓冲区)限制是32MiB,超过了则会下载到一个临时文件里,服务端则会从临时文件中读取刷盘。
r.MaxMultipartMemory = 8<<20 //8MiB ,修改缓冲区大小
r.POST("/single-upload", func(c *gin.Context) {
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
log.Println(file.Filename)
dst := fmt.Sprintf("E:/5120154230GoCode/src/gin_parambind/%s", file.Filename) //路径需要存在
//dst = path.Join("./file/",file.Filename) //两种方式均可得到路径
err = c.SaveUploadedFile(file, dst) //上传文件到指定的目录
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' upload!", file.Filename),
})
})
4.2 多文件上传
多文件上传和单文件上传区别就在于需要循环处理文件。
//多文件上传
r.POST("/multi-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("E:/5120154230GoCode/src/gin_parambind/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
五、路由组
路由组主要是为了管理一些相同前缀的URL。
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) {})
// 嵌套路由组
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {})
}
六、代码合集
int和string相互转化的方法:
int, err := strconv.Atoi(string) //string转int
int64, err := strconv.ParseInt(string, 10, 64)// string转int64,string为string参数,10表示10进制,64表示精度这里为int64
string := strconv.FormatInt(int64,10) // int64转string
string := strconv.Itoa(int) //int转string
下面就是一些练习的代码了:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default() //返回一个默认的路由引擎
r.GET("/hello", func(c *gin.Context) {
//name := c.Query("name") 参数
//name := c.DefaultQuery("name","dingli")
//name, ok := c.GetQuery("name")
//if !ok {
// name = "xxx" //取不到值
//}
c.String(200, "lifangding.com")
})
// 方法1:直接返回json或者使用map返回
r.POST("/postRequest", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"method": "POST",
"name": "dingli",
})
})
r.GET("/data", func(c *gin.Context) {
data := map[string]interface{}{
"name": "ding",
"age": 23,
}
c.JSON(http.StatusOK, data)
})
// 方法二、结构体,注意字段首字母大写
type Msg struct {
Code int
Msg string
Data interface{}
}
type IUser struct {
Name string
Age int
Sex bool
}
r.GET("/hjson", func(c *gin.Context) {
msg := Msg{200, "ding", IUser{
Name: "dingli",
Age: 26,
Sex: true,
}}
c.JSON(http.StatusOK, msg)
})
r.GET("/info", func(c *gin.Context) {
var Info struct {
Tel int `json:"tel"` //当需要字段名为小写时候使用tag,表示使用json包操作该结构体的Tel字段为tel字段
Addr string
}
Info.Tel = 17781863895
Info.Addr = "四川泸州"
c.JSON(http.StatusOK, Info) //反射操作
})
//响应HTML页面
//r.Static("/static", "./static") //静态资源路径
r.LoadHTMLFiles("./login.html", "./index.html")
r.GET("/login", func(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", nil)
})
r.POST("/login", func(c *gin.Context) {
//获取form表单中的数据,PostForm获取不到数据返回nil
username := c.PostForm("username")
password := c.PostForm("password")
//username1, ok := c.GetPostForm("username") //通过ok判断是否获取到值
//if !ok {
// username1 = "dingli"
//}
//username = c.DefaultPostForm("username","defaultValue") //设置默认值
c.HTML(http.StatusOK, "index.html", gin.H{
"Username": username,
"Password": password,
})
})
//获取URI路径参数 如:login/username/password
r.GET("/:name/:age", func(c *gin.Context) {
name := c.Param("name")
age := c.Param("age") //string类型
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(":8080")
}
路由组、文件上传
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
type UserInfo struct {
Username string `form:"username" json:"username"` //设置form等方便进行参数绑定 binding:"required"
Password string `form:"password" json:"password"`
}
/**
如果是 GET 请求,只使用 Form 绑定引擎(query)。
如果是 POST 请求,首先检查 content-type(shouldBind去识别三种form、querystring、json) 是否为 JSON 或 XML,然后再使用 Form(form-data)。
*/
func main() {
r := gin.Default()
r.POST("/user", func(c *gin.Context) {
var userInfo UserInfo //go中都是值传递,因此需要改变传递地址
// 1、绑定JSON的示例 ({"user": "q1mi", "password": "123456"}) //postman的body中选择raw写原始json数据
//2、绑定form表单示例 (user=q1mi&password=123456)
//3、绑定QueryString示例 (/loginQuery?user=q1mi&password=123456)
if err := c.ShouldBind(&userInfo); err == nil {
fmt.Printf("login info: %#v\n", userInfo)
c.JSON(http.StatusOK, gin.H{
"username": userInfo.Username,
"password": userInfo.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
})
// 文件上传
r.LoadHTMLFiles("./index.html")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", nil)
})
//处理multipart forms提交文件时默认的内存(缓冲区)限制是32MiB,超过了则会下载一个临时文件里,服务端则会从临时文件中读取刷盘
r.MaxMultipartMemory = 8 << 20 //8MiB ,修改缓冲区大小
//单个文件上传
r.POST("/single-upload", func(c *gin.Context) {
file, err := c.FormFile("f1")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
log.Println(file.Filename)
dst := fmt.Sprintf("E:/5120154230GoCode/src/gin_parambind/%s", file.Filename) //路径需要存在
//dst = path.Join("./file/",file.Filename) //两种方式均可得到路径
err = c.SaveUploadedFile(file, dst) //上传文件到指定的目录
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("'%s' upload!", file.Filename),
})
})
//多文件上传
r.POST("/multi-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("E:/5120154230GoCode/src/gin_parambind/%s_%d", file.Filename, index)
// 上传文件到指定的目录
c.SaveUploadedFile(file, dst)
}
c.JSON(http.StatusOK, gin.H{
"message": fmt.Sprintf("%d files uploaded!", len(files)),
})
})
//HTTP的重定向
r.GET("/testHttpRedirect", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "http://www.sogo.com/")
})
//路由的重定向
r.GET("/testA", func(c *gin.Context) {
c.Request.URL.Path = "/testB"
r.HandleContext(c)
})
r.GET("/testB", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "ok",
})
})
// 默认无效请求处理
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "404.html", nil)
})
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) {})
// 嵌套路由组
xx := shopGroup.Group("xx")
xx.GET("/oo", func(c *gin.Context) {})
}
r.Run(":8081")
}