国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频

Gin框架源碼解析【建議收藏】

共 23023字,需瀏覽 47分鐘

 ·

2021-12-18 23:18

Gin框架是golang的一個常用的web框架,最近一個項目中需要使用到它,所以對這個框架進行了學(xué)習(xí)。gin包非常短小精悍,不過主要包含的路由,中間件,日志都有了。我們可以追著代碼思考下,這個框架是如何一步一步過來的。

從http包說起

基本上現(xiàn)在的golang的web庫都是從http上搭建起來,golang的http包的核心如下:

func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}

這里的Handler是一個接口

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

所以,這里就是我們的入口,這里我們需要有一個類來實現(xiàn)這個接口:Engine。

type Engine struct {

}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
}

這里ServeHTTP的方法傳遞的兩個參數(shù),一個是Request,一個是ResponseWriter,Engine中的ServeHTTP的方法就是要對這兩個對象進行讀取或者寫入操作。而且這兩個對象往往是需要同時存在的,為了避免很多函數(shù)都需要寫這兩個參數(shù),我們不如封裝一個結(jié)構(gòu)來把這兩個對象放在里面:Context

type Context struct {
writermem responseWriter
Request *http.Request
Writer ResponseWriter
...
}



type responseWriter struct {
http.ResponseWriter
size int
status int
}


這里有幾個需要討論的點:

Writer是否可以直接使用http包的ResponseWriter接口

type ResponseWriter interface {

Header() Header

Write([]byte) (int, error)

WriteHeader(statusCode int)
}

但是考慮到我們web框架的最重要的就是輸出數(shù)據(jù)給客戶端,這里的輸出邏輯我們極有可能需要自己封裝一些框架自帶的方法。所以我們不妨自定義一個結(jié)構(gòu)responseWriter,來實現(xiàn)基本的http.ResponseWriter。并且實現(xiàn)一些具體的其他方法。這些具體的其他方法都有哪些呢?我們使用gin包自帶的ResponseWriter接口來說明。

type ResponseWriter interface {
responseWriterBase

Pusher() http.Pusher
}


type responseWriterBase interface {
http.ResponseWriter
http.Hijacker
http.Flusher
http.CloseNotifier

Status() int

Size() int

WriteString(string) (int, error)

Written() bool

WriteHeaderNow()
}

為什么Context有writermem和Writer兩個實現(xiàn)了http.Response對象的結(jié)構(gòu)?

首先我們自帶的ResponseWriter必須實現(xiàn)比http.ResponseWriter更強大的接口功能,這個是毋庸置疑的。所以,我們不妨考慮下這里如果不是兩個writermem和Writer兩個的話,只有一個存在是否可能?

如果只有Writer接口存在,這個一定不可能,這個Writer實現(xiàn)的是我們gin自定義的接口,外部serveHTTP傳遞的是實現(xiàn)了http.ResponseWriter的類,并不能保證實現(xiàn)了gin自帶的ResponseWriter。更多視頻教程微信搜索【碼農(nóng)編程進階筆記】

如果只有writermen結(jié)構(gòu)存在,這個是可能的。外部傳遞的http.ResponseWriter就被藏在了這個對象里面。但是這樣就丟失了接口的靈活性。本質(zhì)還是對外暴露的是接口還是結(jié)構(gòu)的邏輯,設(shè)想一下如果使用這個框架的用戶要自己實現(xiàn)一個ResponseWriter,就需要繼承這個結(jié)構(gòu),而不是繼承接口。而具體的調(diào)用的方法就變成了被繼承結(jié)構(gòu)的方法了。例子如下:

package main

func main() {
customResp := new(customResponseWriter)

c := new(context)
c.Writermem = customResp.responseWriter
c.Writermem.call()
}

type context struct {
Writermem responseWriter
}

type customResponseWriter struct {
responseWriter
}

func (r *customResponseWriter)call() {

}

type responseWriter struct{}

func (r *responseWriter)call() {

}

所以這里的Context結(jié)構(gòu),對外暴露的是接口ResponseWriter,內(nèi)部的responseWriter結(jié)構(gòu)實現(xiàn)了ResponseWriter接口。在reset()的時候進行拷貝是合理的。

func (c *Context) reset() {
c.Writer = &c.writermem
c.Params = c.Params[0:0]
c.handlers = nil
c.index = -1
c.Keys = nil
c.Errors = c.Errors[0:0]
c.Accepted = nil
}

context就是某個請求的上下文結(jié)構(gòu),這個結(jié)構(gòu)當然是可以不斷new的,但是new這個對象的代價可以使用一個對象池進行服用,節(jié)省對象頻繁創(chuàng)建和銷毀的開銷。golang中的sync.Pool就是用于這個用途的。需要注意的是,這里的對象池并不是所謂的固定對象池,而是臨時對象池,里面的對象個數(shù)不能指定,對象存儲時間也不能指定,只是增加了對象復(fù)用的概率而已。

type Engine struct {
...
pool sync.Pool
...
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
c := engine.pool.Get().(*Context)
c.writermem.reset(w)
c.Request = req
c.reset()

engine.handleHTTPRequest(c)

engine.pool.Put(c)
}

這個Context是gin中最重要的數(shù)據(jù)結(jié)構(gòu)之一了,它既然已經(jīng)包了request了,那么從請求中獲取參數(shù)的各個接口它必然也需要包了。


func (c *Context) Param(key string) string
...

func (c *Context) Query(key string) string

func (c *Context) DefaultQuery(key, defaultValue string) string

...

func (c *Context) PostFormArray(key string) []string

路由

從http請求進來的邏輯理清楚了,下面就進入到了路由部分,路由其實還是分為兩個部分,一個是路由設(shè)置部分,一個是路由匹配部分。

路由其實并不僅僅是url,還包括HTTP的請求方法,而實現(xiàn)一個REST風格的http請求,需要支持REST支持的方法,比如GET,PUT,POST,DELETE,OPTION等。

路由一定是有很多個路由路徑,可以使用數(shù)組存儲,但更巧妙的是,使用Redix樹結(jié)構(gòu)進行存儲。這樣尋找的方法更為高效。

首先我們會在Engine這個結(jié)構(gòu)中增加樹結(jié)構(gòu),并且提供增加路由的功能

type Engine struct {
...
pool sync.Pool
trees methodTrees
}

type methodTrees []methodTree

type methodTree struct {
method string
root *node
}

type node struct {
path string
indices string
children []*node
handlers HandlersChain
priority uint32
nType nodeType
maxParams uint8
wildChild bool
}

func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
assert1(path[0] == '/', "path must begin with '/'")
assert1(method != "", "HTTP method can not be empty")
assert1(len(handlers) > 0, "there must be at least one handler")

debugPrintRoute(method, path, handlers)
root := engine.trees.get(method)
if root == nil {
root = new(node)
engine.trees = append(engine.trees, methodTree{method: method, root: root})
}
root.addRoute(path, handlers)
}

其中我們可以看到engine.trees實際上是有多個樹組成,這里的每個樹都是根據(jù)HTTP method進行區(qū)分的。每增加一個路由,就往engine中對應(yīng)的method的樹中增加一個path和handler的關(guān)系。

這個樹是一個Redix樹,父節(jié)點存儲子節(jié)點的公共部分,子節(jié)點存在各自的特有路徑。
如圖:

那么具體往這個trees中增加路由怎么增加呢?

這里選擇使用一個結(jié)構(gòu)RouterGroup

type RouterGroup struct {
Handlers HandlersChain
basePath string
engine *Engine
root bool
}

type HandlerFunc func(*Context)
type HandlersChain []HandlerFunc

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}

func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("POST", relativePath, handlers)
}

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

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

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

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

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

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

// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
group.handle("GET", relativePath, handlers)
group.handle("POST", relativePath, handlers)
group.handle("PUT", relativePath, handlers)
group.handle("PATCH", relativePath, handlers)
group.handle("HEAD", relativePath, handlers)
group.handle("OPTIONS", relativePath, handlers)
group.handle("DELETE", relativePath, handlers)
group.handle("CONNECT", relativePath, handlers)
group.handle("TRACE", relativePath, handlers)
return group.returnObj()
}

那么Engine就繼承RouterGroup:

type Engine struct {
RouterGroup
...
pool sync.Pool
trees methodTrees
}

看到這里就有一點REST的味道了吧。

有人會問,為什么不把這些方法的具體實現(xiàn)放在Engine中呢?這里我考慮到是由于“路由”和“引擎”畢竟是兩個邏輯,使用繼承的方式有利于代碼邏輯分離。并且gin還定義了接口IRoutes來表示RouterGroup實現(xiàn)的方法。

type IRoutes interface {
Use(...HandlerFunc) IRoutes

Handle(string, string, ...HandlerFunc) IRoutes
Any(string, ...HandlerFunc) IRoutes
GET(string, ...HandlerFunc) IRoutes
POST(string, ...HandlerFunc) IRoutes
DELETE(string, ...HandlerFunc) IRoutes
PATCH(string, ...HandlerFunc) IRoutes
PUT(string, ...HandlerFunc) IRoutes
OPTIONS(string, ...HandlerFunc) IRoutes
HEAD(string, ...HandlerFunc) IRoutes

StaticFile(string, string) IRoutes
Static(string, string) IRoutes
StaticFS(string, http.FileSystem) IRoutes
}

將RouterGroup和Engine區(qū)分開,還有一個好處。我們有時候需要將一批路由加個統(tǒng)一前綴,這里需要用到方法:

使用例子如下:

v1 := router.Group("/v1")

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

這里再看一下RouterGroup的Group函數(shù)。

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
return &RouterGroup{
Handlers: group.combineHandlers(handlers),
basePath: group.calculateAbsolutePath(relativePath),
engine: group.engine,
}
}

它把RouterGroup暴露出來,而不是把Engine暴露出來,這樣整個邏輯就很清晰,我可以對這個RouterGroup進行各種自定義方法。在最后調(diào)用v1.GET的時候再將帶有絕對路徑的handler掛在engine上的tree上。更多視頻教程微信搜索【碼農(nóng)編程進階筆記】

在請求進來的時候,路由匹配,在engine的handleHTTPRequest

func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
path := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
path = c.Request.URL.RawPath
unescape = engine.UnescapePathValues
}

// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
handlers, params, tsr := root.getValue(path, c.Params, unescape)
if handlers != nil {
c.handlers = handlers
c.Params = params
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && path != "/" {
if tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
}
...
}

去Engine中的tree中調(diào)用getValue獲取出對應(yīng)的handlers進行處理。

中間件

下面就要聊到路由對應(yīng)的handlers是什么了?這里我們看到tree中路由對應(yīng)的是HandlersChain,實際就是[]HandlerFunc,所以一個路由,實際上會對應(yīng)多個handlers。

首先我們已經(jīng)把request和responseWriter封裝在context里面了,多個handler只要處理好這個context就可以了,所以是可以一個路由擁有多個handler的。

其次這里的handler是怎么來的呢?

每個路由的handler有幾個來源,第一個來源是在engine.GET的時候調(diào)用增加的。第二個來源是RouterGroup.GET的時候增加的,其實這兩種方式都是調(diào)用

func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle("GET", relativePath, handlers)
}

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers)
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) {
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}

從兩個copy的順序可以看出,group的handler高于自定義的handler。這里自定義的handler可以是多個,比如:

router.GET("/before", MiddleWare(), func(c *gin.Context) {
request := c.MustGet("request").(string)
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
})
})

func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("before middleware")
c.Set("request", "clinet_request")
c.Next()
fmt.Println("before middleware")
}
}

這里的/before實際上是帶了兩個handler。

第三種方法是使用Use增加中間件的方式:

router.Use(MiddleWare())

這里的會把這個中間件(實際上也是一個handler)存放到routerRroup上。所以中間件是屬于groupHandlers的。

在請求進來的時候是如何調(diào)用的呢?

答案還是在handleHTTPRequest中

func (engine *Engine) handleHTTPRequest(c *Context) {
...
handlers, params, tsr := root.getValue(path, c.Params, unescape)
if handlers != nil {
c.handlers = handlers
c.Params = params
c.Next()
c.writermem.WriteHeaderNow()
return
}
..
}

func (c *Context) Next() {
c.index++
for s := int8(len(c.handlers)); c.index < s; c.index++ {
c.handlers[c.index](c)
}
}

每個請求進來,匹配好路由之后,會獲取這個路由最終combine的handlers,把它放在全局的context中,然后通過調(diào)用context.Next()來進行遞歸調(diào)用這個handlers。當然在中間件里面需要記得調(diào)用context.Next() 把控制權(quán)還給Context。

靜態(tài)文件

golang的http包中對靜態(tài)文件的讀取是有封裝的:

func ServeFile(w ResponseWriter, r *Request, name string)

routerGroup也是有把這個封裝成為方法的

func (group *RouterGroup) Static(relativePath, root string) IRoutes {
return group.StaticFS(relativePath, Dir(root, false))
}

func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRoutes {
...
handler := group.createStaticHandler(relativePath, fs)
...
}

func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc {
...
fileServer.ServeHTTP(c.Writer, c.Request)
...
}

所以調(diào)用應(yīng)該像這樣:

router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("my_file_system"))
router.StaticFile("/favicon.ico", "./resources/favicon.ico")

其中的StaticFS的第二個參數(shù)可以是實現(xiàn)了http.FileSystem的任何結(jié)構(gòu)。

綁定

參數(shù)一個一個獲取是很麻煩的,我們一般還會把參數(shù)賦值到某個struct中,這個時候解析參數(shù),賦值的過程很繁瑣。我們是不是提供一個自動綁定的方法來操作呢?

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() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}

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:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15"

這個是不是很方便?它是怎么實現(xiàn)的呢?

首先參數(shù)解析是和http請求的content-type頭有關(guān),當content-type頭為application/json的時候,我們會在body中傳遞json,并且應(yīng)該解析請求body中的json,而content-type頭為application/xml的時候,我們會解析body中的xml。更多視頻教程微信搜索【碼農(nóng)編程進階筆記

我們之前說了,這些解析的行為應(yīng)該都是Context包了的。所以這些方法都定義在Context中

func (c *Context) ShouldBind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.ShouldBindWith(obj, b)
}

// ShouldBindJSON is a shortcut for c.ShouldBindWith(obj, binding.JSON).
func (c *Context) ShouldBindJSON(obj interface{}) error {
return c.ShouldBindWith(obj, binding.JSON)
}

// ShouldBindXML is a shortcut for c.ShouldBindWith(obj, binding.XML).
func (c *Context) ShouldBindXML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.XML)
}

// ShouldBindQuery is a shortcut for c.ShouldBindWith(obj, binding.Query).
func (c *Context) ShouldBindQuery(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Query)
}

// ShouldBindWith binds the passed struct pointer using the specified binding engine.
// See the binding package.
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}

這里binding這塊應(yīng)該怎么設(shè)計呢?其實知道了具體的解析方式,就知道如何綁定,比如知道了這個是json解析,我就可以很方便將參數(shù)直接json.Decode,如果知道這個是query解析,我可以直接從URL.Query中獲取請求串,如果知道這個是表單form,我就可以直接request.ParseForm來解析。

所以,這個還是一個接口,多個結(jié)構(gòu)實現(xiàn)的設(shè)計。

定義一個接口:

type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}

定一個多個結(jié)構(gòu):

type formBinding struct{}

func (formBinding) Bind(req *http.Request, obj interface{}) error {
if err := req.ParseForm(); err != nil {
return err
}
req.ParseMultipartForm(defaultMemory)
if err := mapForm(obj, req.Form); err != nil {
return err
}
return validate(obj)
}

type jsonBinding struct{}

func (jsonBinding) Bind(req *http.Request, obj interface{}) error {
return decodeJSON(req.Body, obj)
}
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
MsgPack = msgpackBinding{}
)
...

在使用綁定解析的時候,我們可以使用ShouldBindWith來指定我們要使用的是哪些解析方式。

參數(shù)驗證

我們希望在綁定參數(shù)的時候,也能給我做一下驗證,有點像laravel里面的Validater一樣,我在綁定的對象設(shè)置一下這個字段是否可以為空,是否必須是int等。官網(wǎng)的例子:

package main

import (
"net/http"
"reflect"
"time"

"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v8"
)

// 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" time_format:"2006-01-02"`
}

func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
)
bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
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()})
}
}

這種需要怎么做呢?

首先當然在上面說的Bind的函數(shù)里面需要加上驗證的邏輯,比如像jsonBinding:

func decodeJSON(r io.Reader, obj interface{}) error {
decoder := json.NewDecoder(r)
if EnableDecoderUseNumber {
decoder.UseNumber()
}
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
}

這里的validate:

func validate(obj interface{}) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}

var Validator StructValidator = &defaultValidator{}

調(diào)用了一個全局的defaultValidator:

type defaultValidator struct {
once sync.Once
validate *validator.Validate
}

這里的defaultValidator的ValidateStruct()最終調(diào)用的就是validator.v8包的Stuct方法

func (v *defaultValidator) ValidateStruct(obj interface{}) error {
...
if err := v.validate.Struct(obj); err != nil {
return err
}
...
}

同樣的,gin為了不讓Validator綁死在validator.v8上,這個default的Validator不是寫死是validator.v8的結(jié)構(gòu),而是自己定義了一個接口:

type StructValidator interface {

ValidateStruct(interface{}) error

Engine() interface{}
}

如果你想用其他的validator,或者自定義一個validator,那么只要實現(xiàn)了這個接口,就可以把它賦值到Validator就可以了。

這種用接口隔離第三方庫的方式確實很巧妙。

Logger中間件

既然有中間件機制,我們可以定義幾個默認的中間件,日志Logger()是一個必要的中間件。

這個Logger中間件的作用是記錄下每個請求的請求地址,請求時長等:

[GIN] 2018/09/18 - 11:37:32 | 200 |     413.536μs |             ::1 | GET      /index

具體實現(xiàn)追下去看就明白了,請求前設(shè)置開始時間,請求后設(shè)置結(jié)束時間,然后打印信息。更多視頻教程微信搜索【碼農(nóng)編程進階筆記

Recovery中間件

Recovery也是一個必要的中間件,試想一下,如果某個業(yè)務(wù)邏輯出現(xiàn)panic請求,難道整個http server就掛了?這是不允許的。所以這個Recovery做的事情是捕獲請求中的panic信息,吧信息打印到日志中。

func RecoveryWithWriter(out io.Writer) HandlerFunc {
var logger *log.Logger
if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
if logger != nil {
stack := stack(3)
httprequest, _ := httputil.DumpRequest(c.Request, false)
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", timeFormat(time.Now()), string(httprequest), err, stack, reset)
}
c.AbortWithStatus(http.StatusInternalServerError)
}
}()
c.Next()
}
}

logger和Recovery這兩個中間件在生成默認的Engine的時候已經(jīng)加上了。

func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}

總結(jié)

gin是個很精致的框架,它的路由,參數(shù)綁定,中間件等邏輯使用非常方便,擴展性也是設(shè)計的非常好,沒有多余的耦合。

附錄

帶個我從各個地方搜索出來的demo例子

package main

import (
"github.com/gin-gonic/gin"
"net/http"
"log"
"fmt"
"time"
"gopkg.in/go-playground/validator.v8"
"reflect"
"github.com/gin-gonic/gin/binding"
)

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

router.Use()
router.GET("/", func(c *gin.Context) {
c.String(http.StatusOK, "It works")
})

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.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)

c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename))
})

router.LoadHTMLGlob("templates/*")
router.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", gin.H{})
})
router.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", gin.H{
"title": "Main website",
})
})

router.GET("/redict/google", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "https://google.com")
})

v1 := router.Group("/v1")

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

v2 := router.Group("/v2")

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

router.Use(MiddleWare())

router.GET("/before", MiddleWare(), func(c *gin.Context) {
request := c.MustGet("request").(string)
c.JSON(http.StatusOK, gin.H{
"middile_request": request,
})
})

router.GET("/sync", func(c *gin.Context) {
time.Sleep(5 * time.Second)
log.Println("Done! in path" + c.Request.URL.Path)
})

router.GET("/async", func(c *gin.Context) {
cCp := c.Copy()
go func() {
time.Sleep(5 * time.Second)
log.Println("Done! in path" + cCp.Request.URL.Path)
}()
})

router.GET("/user/:name", func(c *gin.Context) {
name := c.Param("name")
c.String(http.StatusOK, "Hello %s", name)
})

router.GET("/welcome", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

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

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.GET("/welcome2", func(c *gin.Context) {
firstname := c.DefaultQuery("firstname", "Guest")
lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname")

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

router.Static("/assets", "./assets")
router.StaticFS("/more_static", http.Dir("my_file_system"))
router.StaticFile("/favicon.ico", "./resources/favicon.ico")

router.GET("/testing", startPage)

if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
v.RegisterValidation("bookabledate", bookableDate)
}

router.GET("/bookable", getBookable)

router.Run(":8001")
}

func MiddleWare() gin.HandlerFunc {
return func(c *gin.Context) {
fmt.Println("before middleware")
c.Set("request", "clinet_request")
c.Next()
fmt.Println("before middleware")
}
}

func startPage(c *gin.Context) {
var person Person
if c.ShouldBind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
log.Println(person.Birthday)
}

c.String(200, "Success")
}

type Person struct {
Name string `form:"name"`
Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
}

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" time_format:"2006-01-02"`
}

func bookableDate(
v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value,
field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string,
)
bool {
if date, ok := field.Interface().(time.Time); ok {
today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() {
return false
}
}
return true
}

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


瀏覽 49
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報
評論
圖片
表情
推薦
點贊
評論
收藏
分享

手機掃一掃分享

分享
舉報

感谢您访问我们的网站,您可能还对以下资源感兴趣:

国产秋霞理论久久久电影-婷婷色九月综合激情丁香-欧美在线观看乱妇视频-精品国avA久久久久久久-国产乱码精品一区二区三区亚洲人-欧美熟妇一区二区三区蜜桃视频 日韩在线99| 激情五月天影院| 国产高潮白浆喷| 中文字幕人妻丝袜二区电影| 99久久精彩视频| 色就是色欧美成人网| 操b视频网站| 中文字幕在线观| 91丨熟女丨对白| 人妻无码一区二区三区| 日韩超清无码| 在线A片免费观看| 亚洲国产成人在线| 日韩东京热中文字幕| 天天日天天舔| ThePorn人妻白浆| 91人妻人人澡人人爽人人精品乱| 翔田千里无码播放| 亚洲免费黄| 国产成人精品免高潮在线观看 | 亚洲天堂网在线观看视频| 亚洲av资源在线观看| 亚洲人操逼| 色aV牛牛在线观看| 99这里只有精品| 欧美A√| 熟练中出-波多野结衣| 九色PORN视频成人蝌蚪自拍| 二区不卡| 日韩精品成人免费观看视频| 日本色天堂| 人妻久操| 成人一区二区电影| 熟女啪啪| 91精品国产成人www| 在线不卡免费Av| 日韩激情无码| 高hnp| 国语对白做受欧美| 操人人| 天天干天天日天天| 日本色色网站| 在线看国产| 91免费成人电影| 精品aaa| 免费69视频看片| 亚洲欧洲天堂| 无码一区二区黑人猛烈视频网站| 91嫖妓站街埯店老熟女| 我要看黄色一级片| 久久久久久久久成人| 亚洲九九在线| 天天日夜| 国产黄色片网站| 人妻无码蜜桃视频| 手机AV网站| 2025AV中文字幕| 亚洲黄色电影| 欧美撒色逼撒| 成人大战香蕉最新视频| 爆操太妹| 人妻电影亚洲av| 成年人在线观看| 六月婷婷五月丁香| 色男人的天堂| 黄网站免费观看| 免费成人黄色| 97在线观看视频| 肏屄网站| 爽好紧别夹喷水欧美| 激情五月综合| 日韩国产欧美| 欧美黄色片| 成人久久| 丁香婷婷色五月| 97毛片| 中文字幕在线第一页| 操比视频| 亚洲色图在线观看| 视频一区在线观看| 91在线观看| 影音先锋资源| 亚洲秘AV无码一区二区qq群| 亚洲天堂人妻少妇| 国产操女人| 亚洲香蕉av| 欧美区在线观看| 三级网址大全| 欧洲精品在线视频| 久久私拍视频| 日韩综合区| 日韩做爱网站| 美女被操网站免费| 欧美久久久久久久| 岛国AV片| 日韩人妻丝袜中文字幕| 欧美一级视频在线观看| 人人操综合| 亚洲AV无码久久精品色无码蜜桃| 尤物最新网址| 狠狠干影院| 淫揉BBB揉揉揉BBBBB| 亚洲V视频| 天天噜| www日韩| 久久久大香蕉| 玖玖爱AV| 亚洲国产成人在线| 超碰997| 少妇人妻一级A毛片| 人妻丰满精品一区二区| 日逼黄色视频| 青青草原视频在线| 免费播放婬乱男女婬视频国产| 日韩精品免费观看| 草莓视频在线播放| 九九这里有精品| 日本成人视频| 国产xxxxx| 无码中文字幕高清| 久久天堂影院| 西西337| 蜜桃91精品| 中文字幕五码| 在线播放内射| A片免费播放| 欧美第一区| 日韩黄色AV| www.骚逼| 天堂在线社区| 夜夜骑免费视频| 免费观看一级黄片| 久久艹伊人| 少妇婷婷| 国产精品99久久久久久成人| 成人三级无码| 天天撸视频| 色悠悠久久| 日韩黄色免费视频| 688AV秘无码一区二区| 俺去也俺去啦| 成人图片小说| 人妻av中文字幕| 西西4444www大胆无| 日韩午夜成人| 婷婷亚洲综合| 先锋影音资源站av每日资源在线 | 国产主播av| 婷婷在线观看免费| 成人精品一区日本无码网站suv/| 蜜桃成人无码区免费视频网站| a级网站| 亚洲最大无码| 欧美激情一区二区三区| 中文字幕在线观看福利视频| 国产特黄级AAAAA片免| 日韩中文字幕在线高清| 影音先锋国产在线| 激情无码国产| 日本精品一区二区三区四区的功能| 玖玖在线| 老熟女AV| 日韩午夜在线观看| 欧美日韩国产性爱| 亚洲三级片在线| 久久久久久成人电影| 精品国产av| 老司机精品在线观看| 东北骚妇大战黑人视频| 成人毛片18| 微熟女地址导航| 操逼影片| 国产熟妇码视频黑料| 91av电影网| 午夜尤物| 操逼逼综合网| 91人妻在线视频| 日韩视频免费观看| caobi999| 99免费视频在线| 午夜天堂在线观看| 一二三区视频| 亚洲视频第一页| а√天堂中文最新版8| 超碰碰碰| 亚洲大逼| 欧美综合在线观看| 黄色一区二区三区| 蜜桃视频免费网站| 欧美人成人无码| 97日韩| 婷婷av在线| 国产精品视频一区二区三| 亚洲成a人无码| 一个人看的www日本高清视频| 欧美毛片在线观看| 欧美在线综合| 6969电视影片最新更新| 中文字幕在线播放第一页| 乱伦激情| 色婷婷一区二区| 黄色电影免费看| 日韩黄色电影在线| 欧美日韩一级毛| 婷婷色在线播放| 最好看的MV中文字幕国语电影| 国产精品久久久久久亚洲毛片 | 最新国产视频| 日韩人妻精品无码久久| 蜜桃视频91| 亚洲欧美v在线视频| 中文字幕+乱码+中文字幕电视剧| 美女做爱网站| 松岛枫在线视频| 大香蕉在线视频网| 特大妓女BBwBBWBBw| 日韩特黄| 亚洲AAA电影| 日本精品在线播放| 中文字幕无码高清| 国产成人AV| 夜夜精品视频| 91在线综合| 插吧插吧网| 天天干天天上| 天天爽夜夜爽夜夜爽精品| 亚洲中文字幕免费视频| 大香蕉视频在线观看| 五月天四房播播| www.人人摸| 一区二区三区高清| 奶大丰满一乱一视频一区二区三区在| 蜜臀精品色无码蜜臀AV| 欧美日一区二区三区| 17.3c一起起草| 亚洲成人AV一区二区| 色香蕉影院| 国产免费AV在线| 在线观看黄a| 日本黄色三级| 精品视频国产| 福利视频网亚洲| 欧美亚洲视频在线观看| 尤物在线| 在线观看无码| 91视频在线观看免费大全| 一道本高清无码视频| 久久精品一区二区| 超碰碰碰| 老熟妇一区二区三区啪啪| 久操综合| 国产suv精品一区二区6精华液| 99热99re6国产线播放| 先锋资源在线视频| 国产成人无码A片免费看| 久久久综合网| 欧美精品系列| AV高清无码| 亚洲国产成人精品女人久久| 中文字幕15页| 日韩人妻中文字幕| 亚洲视频在线观| 青草青在线视频| 99热在线免费观看| 久久b| 18禁网站禁片免费观看| AV免费激情影院| 国产高清无码福利| 少妇搡BBBB搡BBB搡造水爽| 色婷婷视频一区二区| 激情五月激情综合网| 人成在线免费视频| 亚洲aaa在线| 可以在线观看的AV| 一区二区AV| 在线视频福利| 日韩毛片中文字幕| 国产乱伦不卡| 亚洲男人的天堂av| 亚洲精品福利视频| 夜夜嗨AV一区二区三区啊| 99久久99久久久精品棕色圆| 超碰99在线| 午夜私人福利| 国产91精品看黄网站在线观看 | 羞羞色院91蜜桃| 蜜桃视频无码区在线观看| 久久精品大香蕉| 波多野结衣av在线观看窜天猴| 久久午夜无码鲁丝片主演是谁| 亚韩无码| 色视频网| 中文字幕99| 欧美性爱天天| ThePorn-成人网站入口| 91欧美日韩| 成人大香蕉视频| 美女啪啪视频| 97精品人妻麻豆一区二区| 国产夫妻自拍AV| 中文久久| 国产又粗又黄| 中文字幕永久免费| 成年人免费公开视频| 中文字幕在线观看网址最新地址| 在线观看三级网址| 午夜福利小视频| 91亚洲国产| 亚洲成a人| 97久久久| 日韩在线综合网| 无码一卡| 五月天婷婷色播| 无码爱爱| xiuxiuav| 人人妻人人插| 亚洲免费无码| 悠悠久久久| 亚洲中文无码在线| 国产精品久久久久久99| 亚洲日本欧美| 操久在线| 日韩草逼| 亚洲狼人久久久精品| 人人操久久| 亚洲精品国产成人| 91麻豆精品国产91久久久吃药 | 成人在线网址| 国产A片免费看| jlzz18| 91.xxxxx| 免费观看A级毛片| 内射无码专区久久亚洲| 粉嫩99精品99久久久久久特污兔| 蜜桃av秘无码一区二区三欧 | 蜜桃精品久久久| 密臀av在线| 无码成人视频| 美女掰穴| 韩国三级中文字幕HD久久精品| 大香蕉1024| 国产婷婷久久Av免费高清| 三级黄色视频在线观看| 国产精品一区二区三区四区| 无码人妻精品一区二区蜜桃漫画| www.四虎成人网站| 亚洲第一综合| 波多野结衣无码高清| 亚洲A片电影| 成人毛片网站| 精品人妻一区二区三区阅读全文| 青青草无码成人AV片| 亚洲免费成人网| H片在线观看| 免费二区| 亚洲Japanese办公室制服| 欧美日韩黄色片| 黑人无码一二三四五区| 西西444WWW无码精品| 青青娱乐亚洲无| 久久国内| 国产一毛a一毛a在线观看| 99热热久久| 综合av| 北条麻妃人妻中文无码| 色哟哟网站| 一本道高清无码视频| 伊人久久五月| 五月天婷婷色| 精品国产va久久久久久久| 91精品婷婷国产综合久久韩漫| 亚洲人成人无码一区二区三区| yOujiZZ欧美精品| 日本视频一区二区| 人人妻人人操人人爱| 婷婷五月天综合| 国产精品无码成人AV电影| 一区日韩| 国产精品美女久久久久久久久| 成人黄片免费看| 91AV在线免费观看| 三级久久| 69视频免费观看| 日韩中文字幕无码人妻| 日本黄色视频网址| 不卡无码中文字幕| 99色视频| 久热免费视频在线观看| 韩日美女性爱| 国产99久久九九精品无码免费 | 国产操逼免费看| 真实野外打野视频| 99视频自拍| 亚洲日韩高清无码| 日韩一区二区三区无码| 羞羞色院91蜜桃| 亚洲性天堂| 大香蕉伊人免费| 午夜丁香| 黄色福利视频在线观看| av一区二区三区| 超碰性爱| 日韩一区二区三区在线视频| 东北女人操逼| 天堂网av2014| 中文字幕精品在线免费视频观看视频 | 97人人爽| 天堂网av2014| 屌国产精品| 免费一级A片在线播放| 男女抽插视频| 欧美色图在线播放| AV无码免费| 免费看AV大片| A视频在线免费观看| 91天天操| 青娱乐亚洲| 日皮视频在线| 亚洲色成人网站www永久四虎| 在线看黄片| 亚洲第一中文字幕| 黄色视频在线免费观看高清视频| 美女黄色免费网站| 黄色国产在线| 综合伊人| 成人高清无码视频| 国产精品国产三级国产专区52| 国产三级无码| 色五月AV| 成年人免费黄色视频| 久久草视频| 69人妻人人澡人人爽久久| 国产毛片久久久久久国产毛片| 日本韩国无码| 亚洲最大网站| 五月天激情午夜福利| 极品久久久| 国产在线观看无码| 人人妻人人澡人人爽久久con| 欧美a级视频| 一本久道无码| 91视频网站| 91调教视频| 黄色一级大片在线免费看国产| 日韩性爱视频| 欧美丰满老熟妇XXXXX性| 欧产日产国产swag| 雾水情缘电影港片| 亚洲视频在线免费播放| 一级AV在线| 中出欧美亚洲| 欧美日韩无码| 国产亚洲成人综合| 麻豆91久久久| www.51av| 在线网址你懂的| 特级西西444www高清大胆免费看| 国产精品一二三区| 欧美一级视频在线观看| 色97| 东京热无码视频| 逼特逼视频在线观看| 爱搞搞搞搞| 美女黄色视频永费在线观看网站| 伊人激情五月天| 天堂a√在线8| 日韩人妻精品一区二区| 加勒比精品| 天天高清无码| 黄色小说在线看| 午夜操日在线| 国产精品观看| 91AV一区二区| 欧美精品欧美精品系列| 免费成人黄色| 亚洲无码高清视频在线| 午夜偷拍视频| 国产A级成人婬片1976| 亚洲日韩中文字幕在线| 国产AV无码高清| 色欲色欲一区二区三区| 人人摸人人看| 亚洲男人天堂av| 黄色免费在线网站| 天天搞搞| 男人天堂网站| 人人综合| 性爱一级片| 精品多人P群无码视频| 日韩毛片在线观看| 丰满少妇一区二区三区| 日本理论片一道本| 久久性爱免费视频| 国产xxxxx| 日皮视频免费在线观看| 国产一级a一片成人AV| 亚洲国产成人av| 国产xxxxx| 精品成人在线| 夜夜撸天天干| 欧美在线操| 人人操人人爱人人拍| 一级A片60分钟免费看| 九九九九色| 操老女人逼视频| 麻豆艾秋MD0056在线| 99在线免费观看| 色色777| 日韩专区在线观看| 黄色高清视频在线观看| 人妻精品一卡二卡| 亚洲毛片在线观看| 五月天激情导航| www.五月天.con| 先锋影音亚洲AV每日资源网站| 麻豆91麻豆国产传媒| 黄片免费在线播放| 欧美婬乱片A片AAA毛片地址| 高清无码内射视频| 全部免费黄色视频| 欧美老女人操逼| 无码免费视频观看| 黄在观看线| 四虎亚洲| 国产成人精品无码片区在线观91 | 91无码人妻一区二区成人AⅤ| 伊人逼逼| 国产精品免费人成网站酒店| 北条麻妃一区二区三区-免费免费高清观看| 国产调教视频| 18AV在线观看| 亚洲在线成人| 91AV免费| 亚洲精品成人av| 国产1区| 视色网| 日本九九视频| 天天搞天天干| 欧美系列在线| 亚洲黄色无码视频| 欧美黄网站| 人妻无码一区二区三区摄像头| 中日韩在线| 成人免费黄色视频网站| 影音先锋91视频| 六月丁香五月| 五月婷婷激情| 婷婷五月天亚洲| 国产精品久久久精品cos| jlzzzjlzzz国产免费观看 | 偷拍综合| 天天射天天爽| 视频一区18| 日韩在线你懂的| 亚洲无码理论片| 国产无遮挡又黄又爽在线观看| 日韩在线视频免费观看| 啪啪网站免费| 中文字幕无码视频在线观看| 啪啪啪免费视频| 日韩精品无码一区二区三区 | 欧美福利在线观看| 久久动态图| 无码人妻丰满熟妇区毛片蜜桃麻豆| 亚洲成人影片| 久色伊人| 国产又爽又黄免费视频免费| 天天撸一撸视频| 草逼逼| 91妻人人澡人人爽人人精品 | 国产视频激情| 亚洲AV秘无码不卡在线观看| 蜜桃在线无码| 久久亚洲Aⅴ成人无码国产丝袜 | 毛片小电影| 黄色a片网站| 无码啪啪| 色色一区| 内射精品| 青青草青娱乐| 水密桃网站| 亚洲精品色图| 在线免费观看无码视频| 欧美久久久久久久| 色欲AV秘无码一区二区三区| 在线日韩| 日本三级黄色视频| 西西掰穴| AV毛片| 亚洲日韩成人| 日本精品一区二区三区四区的功能| 国产足交| 一道本视频| 蜜桃视频在线观看视频| 色婷婷久久综合| 免费视频亚洲| 少妇性受XXXX黑人XYX性爽| 精品交换一区二区三区无码| 人妻精品电影| 国内成人精品网站| 欧美操操操| 超碰9| 欧一美一婬一伦一区二区三区自慰| 欧日无码| av天堂中文字幕| 一级片在线视频| 影音先锋av资源在线| 成人片毛片| 激情操逼视频| 啪啪免费网| 欧美MV日韩MV国产网站| 婷婷丁香花| 久久久久国产精品视频| 3d动漫一区二区| 韩国三级中文字幕HD久久精品| 日本操逼在线播放| 成人网站在线看| 波多野结衣不卡| 天天干天天草| 婷婷av在线| 在线a视频| 白嫩外女BBWBBWBBW| 亚洲无码专区在线观看| 亚洲AV无码成人精品区大猫| 成人精品视频网站| 日本黄色录像| 日韩中文字幕无码中字字幕| 精品视频网| 永久久久久久久| 日韩成人免费在线观看| 亚洲一级黄色大片| 九色PORNY丨自拍蝌蚪| 操逼超碰| 狠狠噜噜| 91高清无码视频| 一级AV在线| 夜夜撸天天操| 天天日av| 91人妻中文字幕| 人人操人人操人人操人人操| 国产老女人农村HD| 欧美AAAAAAAAAA特级| 激情五月天激情网| 久久久久一区二区三区| 无套内射在线播放| 欧美一区二区三区在线播放 | 综合色亚洲| 五月婷婷操逼| www.簧片| 猫咪AV大香蕉| 亚洲另类天堂| 日韩一级片在线播放| 五月婷婷丁香在线| 亚洲AV无码国产综合专区| 久久午夜福利电影| 亚洲精品中文字幕在线观看| 91人妻最真实刺激绿帽| 手机在线观看av| 欧美成人精品激情在线视频| 色视频在线| 97午夜| 国产精品久久久久久久久久九秃 | 三洞齐开Av在线免费观看| 亚洲AV无码成人精品区天堂小说 | 青久久久| 蜜桃AV无码一区二区三区| 自拍偷拍一区二区| 成年人黄色视频在线观看| 26∪u∪成人网站| 亚洲欧美日韩电影| 被男友内S~高H文| 午夜婷婷| 操逼视频网| 国产精品视频| 国产中文| 巨い巨乳の少妇あジed2k| 土耳其电影《爱与罚》| 色色色色色色色色欧美| 北条麻妃电影九九九| 美女黄色视频永费在线观看网站| 婷婷综合缴情亚洲另类在线| 天天操人人妻| 99热6| 波多野结衣成人视频| 超碰成人福利| 福利二区| 中文字幕免费视频在线播放| 亚洲香蕉av| 91丨熟女露脸| 动漫操逼视频| 中文字幕中文字幕无码| 亚洲欧洲精品视频| 182在线视频| 四虎AV在线| 精品久久成人| 99久久精品国产精品有折扣吗 | 影音先锋资源| 精品人妻一区二区三区四区 | 永久免费视频| 成人香蕉网| 爱爱打炮影院| 韩国深夜福利视频| 无码精品一区二区三区在线播放 | 一区二区三区三级片| 无套内射在线播放| 亚洲第一色婷婷| 国产一级片电影| 成人无码电影在线观看| 五月中文字幕| 曰韩毛片| 亚洲精品秘一区二区三线观看| 黄色在线免费看| 中文字幕东京热加勒比| 先锋影音男人| 影音先锋AV天堂| 久久精品苍井空免费一区| 手机av在线观看| 北条麻妃av在线播放| 丰满人妻一区二区三区不卡二| 思思99热| 黄色片免费观看| 鸭子AV| 亚洲视频在线观看中文字幕| 97婷婷五月天| 中文字幕内射| 18AV在线观看| 无码AⅤ一区二区三区| 久草这里只有精品| 日本一级一片免费视频| 国产在线久久久| 亚洲激情欧美激情| 波多野结衣天堂| 国产一区二区电影| jjzz国产| 久久伊人影院| 婷婷激情综合| 亚洲乱码一区| 99天天视频| 美日韩无码视频| 国产av日韩| 午夜不卡视频| 六月色| 黄色视频一区二区| 杨幂操逼视频| av乱伦小说| 一道本无码在线视频| 国产欧美精品一区二区色综合| 日韩中文字幕无码| 北条麻妃无码视频| 狠操网| 欧美亚洲在线观看| 91成人视频在线免费观看| 99热这里有精品| 日韩av电影在线观看| 狠狠的操| 日本成人免费电影| 99久久婷婷国产综合| 韩国三级HD久久精品HD| 亚洲视频免费完整版在线播放| 免费成人黄色| 中文字幕乱码视频32| 国产91丝袜在线播放| 在线观看视频无码| 国产精品无码7777777| AV麻豆| 国产偷拍精品视频| 久久蜜桃成人| 欧洲一区在线观看| 国产亲子乱XXXXinin| 国产女18毛片多18精品| 久久永久免费精品人妻专区| 性爱xxxxx| 丁香五月婷婷中文字幕| 无码区一区二区三区| 色婷五月天| 热久久亚洲中文字幕| 久久高清亚洲| 亚洲天堂视频在线播放| 国产av一级片| 午夜福利码一区二区| 久草新| 污视频网站免费观看| 天天躁日日躁狠狠| 视色网| 亚洲欧美色图| 亚洲砖区| 69Av视频| 午夜成人无码| 刘玥91精品一区二区三区| 亚洲欧美精品AAAAAA片| 国精产品一区一区三区有限公司杨| 午夜久久电影| 亚洲精品一区二三区不卡| 底流量AV电影在线| 91大屁股| 草草视频在线观看| 五月天国产精品| 成人福利视频| 亚洲一级一级黄色| 大香蕉性爱网| 国产一级片在线播放| 国产激情无码| 婷婷黄色网| 成人午夜精品无码区| 3344gc在线观看入口| 操逼视频在线观看| 在线播放毛片| 日韩无码不卡视频| 一区二区三区成人电影| 午夜亚洲| 日韩AⅤ无码一区二区三区| 久操免费观看| 六月激情婷婷| 黑人AV七| 午夜试看120秒体验区的特点 | 翔田千里在线播放| 欧美成人性爱视频| 色色色色色色色色欧美| 天天日天天日天天操| 午夜福利视频91| 翔田千里珍藏版无码| 91丨牛牛丨国产人妻| 免费在线观看视频a| 婷婷丁香五月社区亚洲| 波多野结衣成人视频| 婷婷五月天丁香成人社区| 日韩一区二区三区无码| 99精品免费| 亚洲三级视频| av无码免费在线观看| 艹逼免费视频| 免费观看无码视频| 91精品国产一区| а中文在线天堂精品| 无码AV在线观看| 久色无码| 男人视频网站| 高清无码激情| 人人操在线| 精品无人区无码乱码毛片国产| 日本黄色录像| 拍拍视频| 91在线无码精品秘软件| 亚洲片在线观看| 黑人丰满大荫蒂| 大鸡巴影院| 美日韩在线| 国产精品自拍小视频| 亚洲成人性爱av| 精品欧美成人片在线| 国产灬性灬淫灬欲水灬| 中文字幕永久在线| 青青操日日干| 色婷婷在线无码精品秘人口传媒 | 五月天婷婷小说| JlZZJLZZ亚洲美女18| 久久不卡| 午夜福利免费在线观看| 日啪| 内射学生妹| 大鸡巴操小逼视频| 国产三级片在线观看视频| 成人AV无码| 国产日韩欧美久久| 激情人妻AV| 国产乱子伦日B视频| 亚洲日本无码50p| 欧美少妇视频| 亚洲天堂在线观看视频| 91大神网址| 久草青青草| 午夜成人三级| 国产免费观看视频| 亚洲天堂2015| 国产乱在线| 精品人伦一区二区三区| 黄色成人网站在线免费观看| 无码视频日韩| 91在线无码精品秘国产-百度| 蜜臀久久99精品久久久久久酒店 | 成人在线黄色视频| 国产成人69免费看| 十八禁无码网站在线观看| 视频一区在线观看| 91无码精品国产AⅤ| 日本在线小视频| 综合影院| 大香蕉国产在线视频| 亚洲福利片| 三级视频网址| 色鬼综合| 大香蕉av一区二区三区在线观看 | PORNY九色视频9l自拍| 波多野结衣在线网站| 长泽梓黑人初解禁BDD07| 日韩无码视屏| 国产精品一区二区三| 国产综合色网| 青青青国产| mm131亚洲国产精品久久|