gin 入門(mén)教程

引入gin

要求go1.14以上忧便,首先通過(guò)'go get -u github.com/gin-gonic/gin'下載gin包再引入,或者引入后通過(guò)'go mod tidy'安裝帽借。引入方式:

main.go中

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

一般還需要引入"net/http"珠增,返回時(shí)使用http.StatusOK

快速?lài)L試

在main.go中寫(xiě)入以下代碼然后啟動(dòng)

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() //默認(rèn)80端口,可通過(guò) r.Run(":8888")改為8888端口
}

// curl 127.0.0.1:8888/ping

路由

GET, POST, PUT, PATCH, DELETE, OPTIONS

func main() {
    // 默認(rèn)使用logger recovery 中間件砍艾, 等價(jià)下面注釋的代碼
    router := gin.Default()

    /*
    r := gin.New()
    r.Use(gin.Logger())
    r.Use(gin.Recovery())
    */

    router.GET("/someGet", getting)
    router.POST("/somePost", posting)
    router.PUT("/somePut", putting)
    router.DELETE("/someDelete", deleting)
    router.PATCH("/somePatch", patching)
    router.HEAD("/someHead", head)
    router.OPTIONS("/someOptions", options)

    router.Run()
}

參數(shù)獲取

路徑內(nèi)參數(shù)

    // 識(shí)別‘/user/john’ 不識(shí)別 ‘/user/ or /user’
    router.GET("/user/:name", func(c *gin.Context) {
        name := c.Param("name")
        c.String(http.StatusOK, "Hello %s", name)
    })

    // 識(shí)別 /user/john/蒂教,注意最后一個(gè)‘/’  /user/john/send
    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)
    })

    // c.FullPath()為ip:port后的所有
    router.POST("/user/:name/*action", func(c *gin.Context) {
        b := c.FullPath() == "/user/:name/*action" // true
        c.String(http.StatusOK, "%t", b)
    })

    // 明文定義路由
    router.GET("/user/groups", func(c *gin.Context) {
        c.String(http.StatusOK, "The available groups are [...]")
    })

通過(guò)query獲取參數(shù)

    //  /welcome?firstname=Jane&lastname=Doe
    router.GET("/welcome", func(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest") // 默認(rèn)值
        lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") 的縮減寫(xiě)法

        c.String(http.StatusOK, "Hello %s %s", firstname, lastname)
    })

form表單

    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,
        })
    })

query + form

    router.POST("/post", func(c *gin.Context) {

        id := c.Query("id")
        page := c.DefaultQuery("page", "0")
        name := c.PostForm("name")
        message := c.PostForm("message")

        fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message)
        // id: 1234; page: 1; name: manu; message: this_is_great
    })

請(qǐng)求接口和傳參

POST /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

name=manu&message=this_is_great

map做為參數(shù)

    router.POST("/post", func(c *gin.Context) {
        ids := c.QueryMap("ids")
        names := c.PostFormMap("names")

        fmt.Printf("ids: %v; names: %v", ids, names)
        // ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
    })

請(qǐng)求接口和傳參

POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
Content-Type: application/x-www-form-urlencoded

names[first]=thinkerou&names[second]=tianou

上傳文件

    // 設(shè)置處理文件內(nèi)存完残,不代表上傳文件大小艘刚, 默認(rèn)32M
    router.MaxMultipartMemory = 8 << 20  // 8 MiB
    router.POST("/upload", func(c *gin.Context) {
        // 單個(gè)文件
        file, _ := c.FormFile("file")
        log.Println(file.Filename)

        // 保存文件 dist := "./file_2022_04_12" + file.Filename
        // 最好別用file.Filename,首先這個(gè)值不是肯定有的窗悯,其次易導(dǎo)致文件名沖突
        c.SaveUploadedFile(file, dst)

        c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
    })
curl -X POST http://localhost:8888/upload \
  -F "file=@/Users/you/test.zip" \
  -H "Content-Type: multipart/form-data"

多文件上傳

    router.MaxMultipartMemory = 8 << 20  // 8 MiB
    router.POST("/upload", func(c *gin.Context) {
        // Multipart form
        form, _ := c.MultipartForm()
        files := form.File["upload[]"]

        for _, file := range files {
            log.Println(file.Filename)

            // Upload the file to specific dst.
            c.SaveUploadedFile(file, dst)
        }
        c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files)))
    })
curl -X POST http://localhost:8080/upload \
  -F "upload[]=@/Users/appleboy/test1.zip" \
  -F "upload[]=@/Users/appleboy/test2.zip" \
  -H "Content-Type: multipart/form-data"

路由分組

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

    // Simple group: v1
    v1 := router.Group("/v1")
    {
        v1.POST("/login", loginEndpoint)
        v1.POST("/submit", submitEndpoint)
        v1.POST("/read", readEndpoint)
    }

    // Simple group: v2
    v2 := router.Group("/v2")
    {
        v2.POST("/login", loginEndpoint)
        v2.POST("/submit", submitEndpoint)
        v2.POST("/read", readEndpoint)
    }

    router.Run(":8080")
}

中間件

r := gin.Default() 等同 r := gin.New() r.Use(gin.Logger()) r.Use(gin.Recovery())

分組+中間件方式


    authorized := r.Group("/")
    authorized.Use(AuthRequired())
    {
        authorized.POST("/login", loginEndpoint)
        authorized.POST("/submit", submitEndpoint)
        authorized.POST("/read", readEndpoint)

        // 嵌套 group
        testing := authorized.Group("testing")
        // visit 0.0.0.0:8080/testing/analytics
        testing.GET("/analytics", analyticsEndpoint)
    }

自定義異常捕獲

err, ok := recovered.(string) 中獲取的類(lèi)型需要與panic("foo")中類(lèi)型一致蜓谋,可使用空接口梦皮,處理好后續(xù)邏輯即可

    r := gin.New()

    r.Use(gin.Logger())

    // 處理異常
    r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
        if err, ok := recovered.(string); ok {
            c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
        }
        c.AbortWithStatus(http.StatusInternalServerError)
    }))

    // 注意,這段代碼不可出現(xiàn)在處理異常的下面桃焕, 否則上面代碼無(wú)效
    // r.Use(gin.Recovery())

    // 異常
    r.GET("/panic", func(c *gin.Context) {
        panic("foo")
    })

    r.GET("/", func(c *gin.Context) {
        c.String(http.StatusOK, "ohai")
    })

請(qǐng)求日志

func main() {
    // 向日志文件寫(xiě)入時(shí)不需要顏色
    gin.DisableConsoleColor()

    // 日志文件剑肯,每次重啟時(shí)會(huì)將現(xiàn)有文件內(nèi)容刪除
    f, _ := os.Create("gin.log")
    gin.DefaultWriter = io.MultiWriter(f)

    // 寫(xiě)入文件同時(shí)在IDE中輸出日志
    // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)

    router := gin.Default()
    router.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })

    router.Run(":8080")
}

自定義日志內(nèi)容

func main() {

    // 向日志文件寫(xiě)入時(shí)不需要顏色
    gin.DisableConsoleColor()

    // 日志文件,每次重啟時(shí)會(huì)將現(xiàn)有文件內(nèi)容刪除
    f, _ := os.Create("gin.log")
    gin.DefaultWriter = io.MultiWriter(f)

    // 寫(xiě)入文件同時(shí)在IDE中輸出日志
    // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)


    router := gin.New()

    // 通過(guò)中間件實(shí)現(xiàn)日志格式自定義
    // 默認(rèn)寫(xiě)入 gin.DefaultWriter = os.Stdout
    router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {

        // 自定義日志格式
        return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n",
                param.ClientIP,
                param.TimeStamp.Format(time.RFC1123),
                param.Method,
                param.Path,
                param.Request.Proto,
                param.StatusCode,
                param.Latency,
                param.Request.UserAgent(),
                param.ErrorMessage,
        )

        // 127.0.0.1 - [Tue, 12 Apr 2022 11:32:20 CST] "GET /ping HTTP/1.1 200 10.474μs "PostmanRuntime/7.29.0" " [GIN] 2022/04/12 - 11:32:20 | 200 |     149.416μs |       127.0.0.1 | GET      "/ping"

    }))
    router.Use(gin.Recovery())

    router.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })

    router.Run(":8080")
}

模型綁定

gin支持json覆旭、xml退子、yaml和標(biāo)準(zhǔn)表單值 (foo=bar&boo=baz)轉(zhuǎn)換為結(jié)構(gòu)模型
當(dāng)使用json時(shí)需要設(shè)置json解釋

// json
type Login struct {
    User     string `json:"user" binding:"required"`
    Password string `json:"password" binding:"required"`
}

// 支持 form、json型将、xml Password為空時(shí)不會(huì)報(bào)錯(cuò)
type Login struct {
    User     string `form:"user" json:"user" xml:"user"  binding:"required"`
    Password string `form:"password" json:"password" xml:"password" binding:"-"`
}

binding:"-":當(dāng)參數(shù)值為空時(shí)不會(huì)報(bào)錯(cuò)
binding:"required":當(dāng)參數(shù)值為空時(shí)會(huì)報(bào)錯(cuò)

綁定示例

// 支持 form寂祥、json、xml且不可為空
type Login struct {
    User     string `form:"user" json:"user" xml:"user"  binding:"required"`
    Password string `form:"password" json:"password" xml:"password" binding:"required"`
}


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

    // JSON ({"user": "manu", "password": "123"})
    router.POST("/loginJSON", func(c *gin.Context) {
        var json Login
        if err := c.ShouldBindJSON(&json); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        if json.User != "manu" || json.Password != "123" {
            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
            return
        }

        c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
    })

    //  XML (
    //  <?xml version="1.0" encoding="UTF-8"?>
    //  <root>
    //      <user>manu</user>
    //      <password>123</password>
    //  </root>)
    router.POST("/loginXML", func(c *gin.Context) {
        var xml Login
        if err := c.ShouldBindXML(&xml); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        if xml.User != "manu" || xml.Password != "123" {
            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
            return
        }

        c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
    })

    // form (user=manu&password=123)
    router.POST("/loginForm", func(c *gin.Context) {
        var form Login
        // 根據(jù) content-type header 推測(cè)綁定形式.
        if err := c.ShouldBind(&form); err != nil {
            c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
            return
        }

        if form.User != "manu" || form.Password != "123" {
            c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"})
            return
        }

        c.JSON(http.StatusOK, gin.H{"status": "you are logged in"})
    })

    router.Run(":8080")
}

自定義模型轉(zhuǎn)換時(shí)的驗(yàn)證

package main

import (
    "fmt"
    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
    "github.com/go-playground/validator/v10"
    "net/http"
    "time"
)

// Booking CheckIn CheckOut 均不可為空七兜,CheckIn基于bookabledate驗(yàn)證是否晚于當(dāng)前時(shí)間丸凭,gtfield=CheckIn : CheckOut晚于CheckIn, 時(shí)間格式’yyyy-mm-dd‘
type Booking struct {
    CheckIn  time.Time `form:"check_id" binding:"required,bookabledate" time_format:"2006-01-02"`
    CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
}

// 定義驗(yàn)證邏輯
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() {
    router := gin.Default()

    // 注冊(cè)驗(yàn)證腕铸,
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("bookabledate", bookableDate)
    }
    router.GET("/bookable", getBookable)
    router.Run(":8050")
}

// 模型轉(zhuǎn)換
func getBookable(c *gin.Context) {
    var b Booking
    if err := c.ShouldBindWith(&b, binding.Query); err == nil {
        fmt.Println(b)
        c.JSON(http.StatusOK, gin.H{"message": "booking dates are valid"})
    } else {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
    }
}

ShouldBindQuery & ShouldBind

ShouldBindQuery 僅解析查詢參數(shù)惜犀,可解析出page,不可解析name和message
ShouldBind 可解析查詢參數(shù)和post參數(shù)

// 使用form:"field-name"
type Person struct {
        Name       string    `form:"name"`
        Address    string    `form:"address"`
        Birthday   time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
        CreateTime time.Time `form:"createTime" time_format:"unixNano"`
        UnixTime   time.Time `form:"unixTime" time_format:"unix"`
}

POST /post?id=1234&page=1 HTTP/1.1
Content-Type: application/x-www-form-urlencoded

name=manu&message=this_is_great

ShouldBindUri

可解析出:name 和 :id

// 使用 uri:"field-name"
type Person struct {
    ID string `uri:"id" binding:"required,uuid"` // 限制為uuid
    Name string `uri:"name" binding:"required"`
}

func main() {
    route := gin.Default()
    route.GET("/:name/:id", func(c *gin.Context) {
        var person Person
        if err := c.ShouldBindUri(&person); err != nil {
            c.JSON(400, gin.H{"msg": err.Error()})
            return
        }
        c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID})
    })
    route.Run(":8088")
}



$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 // 成功
$ curl -v localhost:8088/thinkerou/not-uuid // 失敗

ShouldBindHeader:


// header:"field-name"
type testHeader struct {
    Rate   int    `header:"Rate"`
    Domain string `header:"Domain"`
}

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        h := testHeader{}

        if err := c.ShouldBindHeader(&h); err != nil {
            c.JSON(200, err)
        }

        fmt.Printf("%#v\n", h)
        c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
    })

    r.Run()

// client
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
// output
// {"Domain":"music","Rate":300}
}

ShouldBind -- 文件上傳

type ProfileForm struct {
    Name   string                `form:"name" binding:"required"`
    Avatar *multipart.FileHeader `form:"avatar" binding:"required"` // 單文件
    // Avatars []*multipart.FileHeader `form:"avatar" binding:"required"` // 多文件
}

func main() {
    router := gin.Default()
    router.POST("/profile", func(c *gin.Context) {
        // 使用c.ShouldBindWith(&form, binding.Form) 或者 c.ShouldBind(&form)
        if err := c.ShouldBind(&form); err != nil {
            c.String(http.StatusBadRequest, "bad request")
            return
        }

        err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
        if err != nil {
            c.String(http.StatusInternalServerError, "unknown error")
            return
        }

        // db.Save(&form)

        c.String(http.StatusOK, "ok")
    })
    router.Run(":8080")
}

SecureJSON

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

    // You can also use your own secure json prefix
    // r.SecureJsonPrefix(")]}',\n")

    r.GET("/someJSON", func(c *gin.Context) {
        names := []string{"lena", "austin", "foo"}

        // 返回  while(1);["lena","austin","foo"]
        c.SecureJSON(http.StatusOK, names)
    })

    r.Run(":8080")
}

多端口號(hào)狠裹,啟動(dòng)多個(gè)服務(wù)虽界,并使用端口號(hào)進(jìn)行API隔離

package main

import (
    "github.com/gin-gonic/gin"
    "golang.org/x/sync/errgroup"
    "log"
    "net/http"
    "time"
)

var (
    g errgroup.Group
)

func router1() 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 1",
            })
    })
    return e
}

func router2() 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 2",
            })
    })
    return e
}

func main() {
    server1 := &http.Server{
        Addr:         ":8050",
        Handler:      router1(),
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    server2 := &http.Server{
        Addr:         ":8051",
        Handler:      router2(),
        ReadTimeout:  time.Second * 5,
        WriteTimeout: time.Second * 10,
    }

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

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

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

優(yōu)雅啟停

go1.8之前的可使用第三方庫(kù),或者自己編寫(xiě)內(nèi)部包涛菠,1.8可使用http.Server的Shutdown函數(shù)

/*
1莉御、新建協(xié)程啟動(dòng)服務(wù)
2撇吞、新建channel接收關(guān)閉信號(hào)
3、阻塞接收信號(hào)
4礁叔、接收到信號(hào)后執(zhí)行關(guān)閉流程
*/


package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"

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

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

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

    go func() {
        if err := srv.ListenAndServe(); err != nil && errors.Is(err, http.ErrServerClosed) {
            log.Printf("listen: %s\n", err)
        }
    }()

    quit := make(chan os.Signal)
    // kill (no param) default send syscall.SIGTERM
    // kill -2 is syscall.SIGINT
    // kill -9 is syscall.SIGKILL but 捕捉不到, 不必添加
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    // 阻塞
    <-quit
    log.Println("Shutting down server...")

    // 通知服務(wù)器還有5秒關(guān)閉
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // 進(jìn)入執(zhí)行棧牍颈,不使用defer將會(huì)立刻關(guān)閉

    // 根據(jù)上下文繼續(xù)執(zhí)行請(qǐng)求,如果在5秒內(nèi)執(zhí)行不完則強(qiáng)制關(guān)閉
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }

    log.Println("Server exiting")
}

嵌套結(jié)構(gòu)解析

嵌套結(jié)構(gòu)琅关,嵌套結(jié)構(gòu)指針煮岁,匿名嵌套,請(qǐng)求方式一樣涣易,均可通過(guò)“Bind”函數(shù)自動(dòng)解析

type StructA struct {
    FieldA string `form:"field_a"`
}

type StructB struct {
    NestedStruct StructA
    FieldB string `form:"field_b"`
}

type StructC struct {
    NestedStructPointer *StructA
    FieldC string `form:"field_c"`
}

type StructD struct {
    NestedAnonyStruct 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.NestedStruct,
        "b": b.FieldB,
    })
}

func GetDataC(c *gin.Context) {
    var b StructC
    c.Bind(&b)
    c.JSON(200, gin.H{
        "a": b.NestedStructPointer,
        "c": b.FieldC,
    })
}

func GetDataD(c *gin.Context) {
    var b StructD
    c.Bind(&b)
    c.JSON(200, gin.H{
        "x": b.NestedAnonyStruct,
        "d": b.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"}}

設(shè)置獲取cookie

c.Cookie 和 c.SetCookie

func main() {

    router := gin.Default()

    router.GET("/cookie", func(c *gin.Context) {

        cookie, err := c.Cookie("gin_cookie")

        if err != nil {
            cookie = "NotSet"
            c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true)
        }

        fmt.Printf("Cookie value: %s \n", cookie)
    })

    router.Run()
}

請(qǐng)求ip限制

router := gin.Default()
// 設(shè)置可訪問(wèn)的ip地址切片
router.SetTrustedProxies([]string{"192.168.1.2"})

CDN設(shè)置方式

router := gin.Default()
// Use predefined header gin.PlatformXXX
router.TrustedPlatform = gin.PlatformGoogleAppEngine
// Or set your own trusted request header for another trusted proxy service
// Don't set it to any suspect request header, it's unsafe
router.TrustedPlatform = "X-CDN-IP"

請(qǐng)求測(cè)試

編寫(xiě)請(qǐng)求接口画机,main.go

package main

func setupRouter() *gin.Engine {
    r := gin.Default()
    r.GET("/ping", func(c *gin.Context) {
        c.String(200, "pong")
    })
    return r
}

func main() {
    r := setupRouter()
    r.Run(":8080")
}

同包下另一個(gè)文件,編寫(xiě)測(cè)試代碼, main_test.go

package main

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/stretchr/testify/assert"
)

func TestPingRoute(t *testing.T) {
    router := setupRouter()

    w := httptest.NewRecorder()
    req, _ := http.NewRequest("GET", "/ping", nil)
    router.ServeHTTP(w, req)

    assert.Equal(t, 200, w.Code)
    assert.Equal(t, "pong", w.Body.String())
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末都毒,一起剝皮案震驚了整個(gè)濱河市色罚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌账劲,老刑警劉巖戳护,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異瀑焦,居然都是意外死亡腌且,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)榛瓮,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)铺董,“玉大人,你說(shuō)我怎么就攤上這事禀晓【” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵粹懒,是天一觀的道長(zhǎng)重付。 經(jīng)常有香客問(wèn)我,道長(zhǎng)凫乖,這世上最難降的妖魔是什么确垫? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮帽芽,結(jié)果婚禮上删掀,老公的妹妹穿的比我還像新娘。我一直安慰自己导街,他們只是感情好披泪,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著搬瑰,像睡著了一般付呕。 火紅的嫁衣襯著肌膚如雪计福。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天徽职,我揣著相機(jī)與錄音,去河邊找鬼佩厚。 笑死姆钉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的抄瓦。 我是一名探鬼主播潮瓶,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钙姊!你這毒婦竟也來(lái)了毯辅?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤煞额,失蹤者是張志新(化名)和其女友劉穎思恐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體膊毁,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡胀莹,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了婚温。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片描焰。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖栅螟,靈堂內(nèi)的尸體忽然破棺而出荆秦,到底是詐尸還是另有隱情,我是刑警寧澤力图,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布步绸,位于F島的核電站,受9級(jí)特大地震影響搪哪,放射性物質(zhì)發(fā)生泄漏靡努。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一晓折、第九天 我趴在偏房一處隱蔽的房頂上張望惑朦。 院中可真熱鬧,春花似錦漓概、人聲如沸漾月。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)梁肿。三九已至蜓陌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間吩蔑,已是汗流浹背钮热。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烛芬,地道東北人隧期。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像赘娄,于是被迫代替她去往敵國(guó)和親仆潮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容