要实现一个 API 服务器,首先要考虑两个方面:API 风格和媒体类型。Go 语言中常用的 API 风格是 RPC 和 REST,常用的媒体类型是 JSON、XML 和 Protobuf。在 Go API 开发中常用的组合是 gRPC+Protobuf 和 REST+JSON。
1. 安装Gin是一个用 Go(Golang)编写的 web框架。要安装 Gin包,你需要先安装 Go并设置你的 Go工作空间。
首先需要安装 Go(需要1.13以上版本),然后你可以使用下面的 Go命令来安装 Gin。
go get -u github.com/gin-gonic/gin
导入包
import "github.com/gin-gonic/gin"
如果使用 http.StatusOK这样的常数,就需要导入 net/http。
import "net/http"2. 快速上手
Gin 框架中按主要功能分有以下几个部分:引擎(Engine)、路由(RouterGroup)、上下文(Context)、渲染(Render)、绑定(Binding)。
Engine 是 Gin 框架中非常核心的结构体,由 Engine 生成框架的实例,它包含多路复用器,中间件和路由信息表等。Gin 提供了两种方法来生成一个 Engine 实例:
router := gin.New()
和
router := gin.Default()
上面代码中的 router 就是一个 Engine 实例,这两个函数最终都会生成一个 Engine 实例。唯一区别是 gin.Default() 函数在 gin.New()函数基础上,使用 gin.Use() 函数,加入了两个中间件即日志中间件 Logger() 和异常恢复中间件 Recovery() 这两个中间件。在 Gin 中,中间件一般情况下会对每个请求都会有效。
在 Gin 框架中,系统自带了异常恢复 Recovery中间件,这个中间件在处理程序出现异常时会在异常链中的任意位置恢复程序, 并打印堆栈的错误信息。
创建一个目录,并初始化,此处我们使用 go mod 初始化一个工程。
$ mkdir webserver $ cd webserver $ go mod init webserver go: creating new go.mod: module webserver
Gin 框架支持 GET、POST、PUT、PATCH、HEAD、OPTIONS、DELETe 等 HTTP 方法,所以 HTTP 请求传递到 Gin 框架时,HTTP 请求路由器会根据具体 HTTP 方法先确定该方法的路由信息表,再匹配路径来返回不同的处理程序信息。下面代码表示 HTTP 的 GET 方法请求,如果通过 POST 方法发送请求将不会得到正确的响应。
新建 main.go代码,并填写如下内容:
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.Run() // listen and serve on 0.0.0.0:8080
}
- Default() 默认情况下,返回一个已经连接了日志和恢复中间件的引擎实例。
- GET 第一个参数为相对路径,第二个参数为该相对路径对应的响应函数。
- *gin.Context上下文是 gin中最重要的部分。它允许我们在中间件之间传递变量,管理流程,验证请求的 JSON,并渲染一个 JSON响应。
- gin.H 是 map[string]interface{} 的快捷方式,用于定义 key-value 结构。
- r.Run()将路由器连接到 http.Server上,并开始监听和服务 HTTP请求。它是http.ListenAndServe(addr, router)的一个快捷方式 注意:除非发生错误,否则这个方法将无限期地阻塞调用的 goroutine。
而 func(c *gin.Context) 定义了处理程序 HandlerFunc,类型定义如下:
type HandlerFunc func(*gin.Context*Context)
通过 Context,开发人员还可以处理参数变量,文件上传等,上面使用 Context 的 c.JSON() 方法返回状态码以及响应的字符串,用户在浏览器中可看到响应的字符串。除了字符串,还可以返回 String、HTML 等形式,这里称为渲染 render。
也可以定制一个服务,然后运行 ListenAndServe(),如下所示:
func main() {
router := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: router,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
此时 gin.Default()方法产生的引擎实例 router 作为服务的 Handler,还可以指定端口等定制服务配置,配置好服务后就可以运行 ListenAndServe() 了。
执行该段代码
$ go run main.go [GIN-debug] [WARNING] Now Gin requires Go 1.13+. [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /ping --> main.main.func1 (3 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Environment variable PORT is undefined. Using port :8080 by default [GIN-debug] Listening and serving HTTP on :8080 [GIN] 2021/11/30 - 12:05:27 | 200 | 40.446µs | 127.0.0.1 | GET "/ping" [GIN] 2021/11/30 - 12:05:46 | 200 | 111.251µs | 127.0.0.1 | GET "/ping"
发送 HTTP 的 Get 请求
$ curl http://127.0.0.1:8080/ping
{"message":"pong"}
2.1 ASCII JSON
使用 ASCII JSON生成只有 ASCII的 JSON,并转义非 ASCII字符。
func main() {
r := gin.Default()
r.GET("/ascii_json", func(c *gin.Context) {
data := map[string]interface{}{
"lang": "Go语言",
"tag": "
",
}
c.AsciiJSON(http.StatusOK, data)
// output: {"lang":"Gou8bedu8a00","tag":"u003cbru003e"}
})
r.Run()
}
输出结果:
$ curl http://127.0.0.1:8080/ascii_json
{"lang":"Gou8bedu8a00","tag":"u003cbru003e"}
。
因为要返回指定的格式,可以通过统一的返回函数 SendResponse来格式化返回
type Response struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
}
func SendResponse(c *gin.Context, err error, data interface{}) {
code := xxxx
message := xxxx
// always return http.StatusOK
c.JSON(http.StatusOK, Response{
Code: code,
Message: message,
Data: data,
})
}
2.2 自定义结构体绑定表单数据请求
Bind():将消息体作为指定的格式解析到 Go struct 变量中。而绑定(Binding)是通过一系列方法可以将请求体中参数自动绑定到自定义的结构体中,从而可以简单快速地得到对应的参数值。
package main
import (
"github.com/gin-gonic/gin"
)
type StructA struct {
FieldA string `form:"field_a"`
}
type StructB struct {
StructAValue StructA
FieldB string `form:"field_b"`
}
type StructC struct {
StructAPointer *StructA
FieldC string `form:"field_c"`
}
type StructD struct {
AnonyStruct struct {
FieldX string `form:"field_x"`
}
FieldD string `form:"field_d"`
}
func GetDataB(c *gin.Context) {
var b StructB
c.Bind(&b)
c.JSON(200, gin.H{
"a": b.StructAValue,
"b": b.FieldB,
})
}
func GetDataC(c *gin.Context) {
var cStruct StructC
c.Bind(&cStruct)
c.JSON(200, gin.H{
"a": cStruct.StructAPointer,
"c": cStruct.FieldC,
})
}
func GetDataD(c *gin.Context) {
var d StructD
c.Bind(&d)
c.JSON(200, gin.H{
"x": d.AnonyStruct,
"d": d.FieldD,
})
}
func main() {
r := gin.Default()
r.GET("/getb", GetDataB)
r.GET("/getc", GetDataC)
r.GET("/getd", GetDataD)
r.Run()
}
输出结果:
$ curl "http://localhost:8080/getb?field_a=hello&field_b=world"
{"a":{"FieldA":"hello"},"b":"world"}
$ curl "http://localhost:8080/getc?field_a=hello&field_c=world"
{"a":{"FieldA":"hello"},"c":"world"}
$ curl "http://localhost:8080/getd?field_x=hello&field_d=world"
{"d":"world","x":{"FieldX":"hello"}}
2.3 绑定查询字符串或POST数据
package main
import (
"log"
"time"
"github.com/gin-gonic/gin"
)
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}
func main() {
r := gin.Default()
r.GET("/testing", startPage)
r.Run()
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
}
c.String(200, "Success")
}
输出结果:
$ curl -X GET "localhost:8080/testing?name=wohu&address=city&birthday=1992-03-15" Success2.4 绑定 URI
package main
import "github.com/gin-gonic/gin"
type Person struct {
ID string `uri:"id" binding:"required,uuid"`
Name string `uri:"name" binding:"required"`
}
func main() {
r := gin.Default()
r.GET("/:name/:id", func(c *gin.Context) {
var person Person
if err := c.ShouldBindUri(&person); err != nil {
c.JSON(400, gin.H{"msg": err})
return
}
c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
})
r.Run(":8080")
}
输出结果:
$ curl localhost:8080/wohu/987fbc97-4bed-5078-9f07-9141ba07c9f3
{"name":"wohu","uuid":"987fbc97-4bed-5078-9f07-9141ba07c9f3"}
$ curl localhost:8080/wohu/uuid
{"msg":[{}]}
2.5 自定义 HTTP 配置
func main() {
r := gin.Default()
http.ListenAndServe(":8080", r)
}
或者
func main() {
r := gin.Default()
s := &http.Server{
Addr: ":8080",
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
}
2.6 自定义 log 文件
package main
import (
"fmt"
"time"
"github.com/gin-gonic/gin"
)
func main() {
router := gin.New()
// LoggerWithFormatter middleware will write the logs to gin.DefaultWriter
// By default gin.DefaultWriter = os.Stdout
router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// your custom format
return fmt.Sprintf("%s - [%s] "%s %s %s %d %s "%s" %s"n",
param.ClientIP,
param.TimeStamp.Format(time.RFC3339Nano),
param.Method,
param.Path,
param.Request.Proto,
param.StatusCode,
param.Latency,
param.Request.UserAgent(),
param.ErrorMessage,
)
}))
router.Use(gin.Recovery())
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
输出打印
[GIN-debug] Listening and serving HTTP on :8080 127.0.0.1 - [2021-11-30T20:19:02.531273713+08:00] "GET /ping HTTP/1.1 200 10.108µs "curl/7.58.0" "2.7 自定义中间件
为 Gin 框架定义一个中间件比较简单,只需要返回 gin.HandlerFunc 类型,且中间件有调用这个函数类型的 c.Next() 方法(以便能传递 Handler 的顺序调用),中间件返回的 gin.HandlerFunc 就是 func(c *gin.Context),这和路由中路径对应的处理程序即 func(c *gin.Context) 一致,所以前面把它们的组合称为处理程序集。
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// Set example variable
c.Set("example", "12345")
// before request
c.Next()
// after request
latency := time.Since(t)
log.Print(latency)
// access the status we are sending
status := c.Writer.Status()
log.Println(status)
}
}
// 下面程序使用了上面的自定义中间件,
func main() {
r := gin.New()
r.Use(Logger())
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
// it would print: "12345"
log.Println(example)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
输出打印
2021/11/30 20:24:23 12345 2021/11/30 20:24:23 58.673µs 2021/11/30 20:24:23 2002.8 自定义校验
package main
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-playground/validator/v10"
)
// Booking contains binded and validated data.
type Booking struct {
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn,bookabledate" time_format:"2006-01-02"`
}
var bookableDate validator.Func = func(fl validator.FieldLevel) bool {
date, ok := fl.Field().Interface().(time.Time)
if ok {
today := time.Now()
if today.After(date) {
return false
}
}
return true
}
func main() {
route := gin.Default()
if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}
route.GET("/bookable", getBookable)
route.Run(":8085")
}
func getBookable(c *gin.Context) {
var b Booking
if err := c.ShouldBindWith(&b, binding.Query); err == nil {
c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
输出结果:
$ curl "localhost:8085/bookable?check_in=2118-04-16&check_out=2118-04-17"
{"message":"Booking dates are valid!"}
$ curl "localhost:8085/bookable?check_in=2118-03-10&check_out=2118-03-09"
{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
2.9 定义路由的日志格式
如果想以给定的格式(如JSON、键值或其他)记录这些信息,那么可以用 gin.DebugPrintRouteFunc定义这种格式。在下面的例子中,我们用标准的日志包来记录所有的路线,但你可以使用其他适合你需要的日志工具。
import (
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
log.Printf("endpoint %v %v %v %vn", httpMethod, absolutePath, handlerName, nuHandlers)
}
r.POST("/foo", func(c *gin.Context) {
c.JSON(http.StatusOK, "foo")
})
r.GET("/bar", func(c *gin.Context) {
c.JSON(http.StatusOK, "bar")
})
r.GET("/status", func(c *gin.Context) {
c.JSON(http.StatusOK, "ok")
})
// Listen and Server in http://0.0.0.0:8080
r.Run()
}
2.10 中间件内使用 Goroutines
当在一个中间件或处理程序内启动新的 Goroutines时,你不应该使用里面的原始上下文,你必须使用一个只读的副本。
func main() {
r := gin.Default()
r.GET("/long_async", func(c *gin.Context) {
// create copy to be used inside the goroutine
cCp := c.Copy()
go func() {
// simulate a long task with time.Sleep(). 5 seconds
time.Sleep(5 * time.Second)
// note that you are using the copied context "cCp", IMPORTANT
log.Println("Done! in path " + cCp.Request.URL.Path)
}()
})
r.GET("/long_sync", func(c *gin.Context) {
// simulate a long task with time.Sleep(). 5 seconds
time.Sleep(5 * time.Second)
// since we are NOT using a goroutine, we do not have to copy the context
log.Println("Done! in path " + c.Request.URL.Path)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8080")
}
2.11 优雅的关闭和启动
We can use fvbock/endless to replace the default ListenAndServe. Refer issue #296 for more details.
router := gin.Default()
router.GET("/", handler)
// [...]
endless.ListenAndServe(":4242", router)
An alternative to endless:
- manners: A polite Go HTTP server that shuts down gracefully.
- graceful: Graceful is a Go package enabling graceful shutdown of an http.Handler server.
- grace: Graceful restart & zero downtime deploy for Go servers.
2.12 路由组在 Gin 框架中,RouterGroup 结构体用于配置路由,RouterGroup 配置 HTTP 请求方法、路径与处理程序(以及中间件)之间的关联关系。字段 RouterGroup.Handlers 保存该组路由所有的中间件处理程序,通过 RouterGroup.engine 的 addRoute 方法,把路径、HTTP 请求方法和处理程序(含中间件)的路由信息写入到对应 HTTP 方法的路由信息表。
在 Gin 中,在路由中引入了组的概念。使用
Group(relativePath string, handlers ...HandlerFunc)
方法可以增加分组,第一个参数作为整个组的基础路径,第二个参数可选加入适用于本组的中间件。路由分组的目的是为了方便 URL 路径的管理。
func loginEndpoint(c *gin.Context) {
c.JSON(http.StatusOK, "login")
}
func submitEndpoint(c *gin.Context) {
c.JSON(http.StatusOK, "submit")
}
func readEndpoint(c *gin.Context) {
c.JSON(http.StatusOK, "read")
}
func main() {
router := gin.Default()
// Simple group: v1
v1 := router.Group("/v1")
{
v1.GET("/login", loginEndpoint)
v1.GET("/submit", submitEndpoint)
v1.GET("/read", readEndpoint)
}
// Simple group: v2
v2 := router.Group("/v2")
{
v2.GET("/login", loginEndpoint)
v2.GET("/submit", submitEndpoint)
v2.GET("/read", readEndpoint)
}
router.Run(":8080")
}
运行输出:
$ go run main.go [GIN-debug] [WARNING] Now Gin requires Go 1.13+. [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached. [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production. - using env: export GIN_MODE=release - using code: gin.SetMode(gin.ReleaseMode) [GIN-debug] GET /v1/login --> main.loginEndpoint (3 handlers) [GIN-debug] GET /v1/submit --> main.submitEndpoint (3 handlers) [GIN-debug] GET /v1/read --> main.readEndpoint (3 handlers) [GIN-debug] GET /v2/login --> main.loginEndpoint (3 handlers) [GIN-debug] GET /v2/submit --> main.submitEndpoint (3 handlers) [GIN-debug] GET /v2/read --> main.readEndpoint (3 handlers) [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value. Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details. [GIN-debug] Listening and serving HTTP on :8080 [GIN] 2021/12/01 - 07:50:01 | 200 | 24.264µs | 127.0.0.1 | GET "/v1/login" [GIN] 2021/12/01 - 07:50:08 | 200 | 88µs | 127.0.0.1 | GET "/v2/login" [GIN] 2021/12/01 - 07:50:16 | 200 | 44.254µs | 127.0.0.1 | GET "/v2/read"2.13 如何写日志文件
func main() {
// 禁用控制台颜色,将日志写入文件时不需要控制台颜色。
gin.DisableConsoleColor()
// Logging to a file.
f, _ := os.Create("gin.log")
gin.DefaultWriter = io.MultiWriter(f)
// 如果需要同时将日志写入文件和控制台,请使用以下代码。
// gin.DefaultWriter = io.MultiWriter(f, os.Stdout)
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(200, "pong")
})
router.Run(":8080")
}
2.14 URL Query 字符串参数或表单参数映射到字典
在 Gin 框架中 PostFormMap(), QueryMap() 等方法在某些情况下非常有用,下面对参数映射到字典做了简单说明,
func main() {
router := gin.Default()
router.POST("/post", func(c *gin.Context) {
ids := c.QueryMap("ids")
names := c.PostFormMap("names")
fmt.Printf("ids: %v; names: %v", ids, names)
})
router.Run(":8080")
}
查询
POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1 Content-Type: application/x-www-form-urlencoded names[first]=thinkerou&names[second]=tianou
curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "names[first]=thinkerou&names[second]=tianou" -g "http://localhost:8080/post?ids[a]=1234&ids[b]=hello"
输出:
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]2.15 Multipart Urlencoded 绑定
type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
}
func main() {
router := gin.Default()
router.POST("/login", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration:
// c.ShouldBindWith(&form, binding.Form)
// or you can simply use autobinding with ShouldBind method:
var form LoginForm
// in this case proper binding will be automatically selected
if c.ShouldBind(&form) == nil {
if form.User == "user" && form.Password == "password" {
c.JSON(200, gin.H{"status": "you are logged in"})
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
}
})
router.Run(":8080")
}
输出结果:
$ curl --form user=user --form password=password http://localhost:8080/login
{"status":"you are logged in"}
2.16 Multipart/Urlencoded 表单
表单提交方法为 POST时,enctype 属性为 application/x-www-form-urlencoded 或 multipart/form-data 的差异:
func main() {
router := gin.Default()
router.POST("/form_post", func(c *gin.Context) {
message := c.PostForm("message")
nick := c.DefaultPostForm("nick", "anonymous")
c.JSON(200, gin.H{
"status": "posted",
"message": message,
"nick": nick,
})
})
router.Run(":8080")
}
输出结果:
可以看到在简单的键值对传递时,属性为 application/x-www-form-urlencoded 或 multipart/form-data 基本不存在差异。都能正常返回 JSON:
curl -H "Content-Type:multipart/form-data" -X POST -d "nick=manu&message=this_is_great" "http://localhost:8080/form_post"
curl -H "Content-Type:application/x-www-form-urlencoded" -X POST -d "nick=manu&message=this_is_great" "http://localhost:8080/form_post"
$ curl -X POST --form message=message --form nick=nick http://localhost:8080/form_post
{"message":"message","nick":"nick","status":"posted"}
2.17 只绑定查询字符串
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.Any("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
if c.ShouldBindQuery(&person) == nil {
log.Println("====== Only Bind By Query String ======")
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
2.18 匹配路径参数 URI 路由参数
对于类似 /user/:firstname/:lastname,:lastname 是 Gin 框架中路由参数的一种写法,表示 lastname 为任意的字符串,访问时使用具体值。
func main() {
router := gin.Default()
router.GET("/user/:firstname/:lastname", func(c *gin.Context) {
fname := c.Param("firstname")
lname := c.Param("lastname")
c.String(http.StatusOK, "Hello %s %s ", fname, lname)
})
router.Run(":8080")
}
程序运行在 Debug 模式时,通过浏览器访问
http://localhost:8080/user/wohu/1104
func main() {
router := gin.Default()
// This handler will match /user/john but will not match /user/ or /user
router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name") // name == "john"
c.String(http.StatusOK, "Hello %s", name)
})
// However, this one will match /user/john/ and also /user/john/send
// If no other routers match /user/john, it will redirect to /user/john/
router.GET("/user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is " + action
c.String(http.StatusOK, message)
})
router.Run(":8080")
}
上面代码路由路径中带参数的方式有 :和 *两种,不同符号代表不同含义,通过 Param() 方法取得对应的字符串值。
- :表示参数值不为空,且不以 /结尾;
- *表示参数可为空,可为任意字符包括 /;
Param() 方法能快速返回路由 URI 指定名字参数的值,它是 c.Params.ByName(key) 方法的简写。如路由定义为: “/user/:id”,则返回 id := c.Param("id")。
在



