Gin实战演练

Gin实战演练

1 gin的简单使用

package main

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

func main() {
   // Default方法的主要作用是实例化一个带有日志、故障恢复中间件的引擎。
   r := gin.Default() //实例化一个gin对象
   // 定义请求
   //定义一个GET请求的路由,参数一是路由地址,也就是在浏览器访问的相对路径,
   //                     参数二是一个匿名函数,函数内部用于业务逻辑处理。
   r.GET("/login", func(c *gin.Context) {
      c.JSON(200, gin.H{ //JSON内容可以通过gin提供的H方法来构建,非常方便。
         "msg": "login", //调用JSON方法返回数据。JSON的操作非常简单,参数一是状态码,参数二是JSON的内容。
      })
   })
   // Run方法最终会调用内置http库的ListenAndServe方法来监听端口,如果不传参数默认监听80端口,
   // 也可以通过参数来变更地址和端口。
   r.Run(":12005")
}

2 RESTful API

RESTful 是⽹络应⽤程序的⼀种设计⻛格和开发⽅式,每⼀个URI代表⼀种资源,客户端通过 POST 、 DELETE 、 PUT 、 GET 四种请求⽅式来对资源做增删改查的操作。

同样的,Gin框架给我们提供的除这4种动词外,还有 PATCH 、 OPTION 、 HEAD 等,详细内容可以查看 rentergroup.go ⽂件的IRoutes接⼝

// IRoutes defines all router handle interface.
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
}

例如接口:

func main() {
   router := gin.Default()
   // 请求动词的第一个参数是请求路径,第二个参数是用于逻辑处理的函数
   router.POST("/article", func(c *gin.Context) {
      c.String(200, "article post")
   })
   router.DELETE("/article", func(c *gin.Context) {
      c.String(200, "article delete")
   })
    
    router.GET("/article/:id/:action", func(c *gin.Context) {
        id := c.Param("id")
        action := c.Param("action")
        fmt.Printf("2 /article/:id->%s, action:%s\n", id, action)
        c.String(200, id+" "+action)
    })

    router.Run(":8080")
}
  • 通过web访问url

  • 使用curl命令来访问url

    / 测试方法
    // curl -X PUT http://localhost:8080/article
    // curl -X POST http://localhost:8080/article
    // curl -X GET http://localhost:8080/article
    // curl -X DELETE http://localhost:8080/article
    

路由参数

:路由

这种匹配模式是精确匹配的,只能匹配⼀个

访问:http://localhost:8080/users/123

输出:123

func main() {
   r := gin.Default()
   r.GET("/users/:id", func(c *gin.Context) {
      id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
   r.Run(":8080")
}

*路由

还有⼀种不常⽤的就是 * 号类型的参数,表示匹配所有,结果是⼀个 / 开头的路径字符串

访问:http://localhost:8080/users/123

输出:/123

func main() {
   r := gin.Default()
   r.GET("/users/*id", func(c *gin.Context) {
      id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
   r.Run(":8080")
}

特别说明⼀点

访问 http://localhost:8080/users时候,会被重定向到 http://localhost:8080/users/,根本原因在于 /users 没有匹配的路由,但是有匹配 /users/ 的路由,所以就会被重定向 到 /users/ ,如下:

func main() {
   r := gin.Default()
   r.GET("/users/*id", func(c *gin.Context) {
      id := c.Param("id")
      c.String(200, "The user id is  %s", id)
   })
}

禁止重定向

r.RedirectTrailingSlash = false

加上如上设置之后,访问 http://localhost:8080/users,是访问不成功的,因为没有服务器去处理这个url

3 Gin获取查询参数

例如:

http://127.0.0.1:8080/users?k1=v1&k2=v2

以 ? 为起点,后⾯的 k=v&k1=v1&k2=v2 这样的字符串就是查询参数

上述案例中有2个参数键值对,通过&来连接:

k1=v1
k2=v2

可以使用gin框架中的如下接口来获取实际的参数值

// 3-2-url-param.go url参数获取
package main

import (
   "fmt"

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

func main() {
   r := gin.Default()
   r.GET("/", func(c *gin.Context) {
      c.DefaultQuery("id", "0")
      value, ok := c.GetQuery("id") // 适合用来判断是否存在该参数

      if ok {
         fmt.Println("id:", value)
      } else {
         fmt.Println("id: nil")
      }

      c.String(200, c.DefaultQuery("wechat", "default baidu_org"))
   })
   r.Run(":8080")
}

实际GetQuery具体实现:

func (c *Context) GetQuery(key string) (string, bool) {
   if values, ok := c.GetQueryArray(key); ok {
      return values[0], ok
   }
   return "", false
}

DefaultQuery的具体实现也是调用GetQuery:

func (c *Context) DefaultQuery(key, defaultValue string) string {
   if value, ok := c.GetQuery(key); ok {
      return value
   }
   return defaultValue
}

GetQuery 和 Query的区别

GetQuery中传入key值,会返回value,ok 若ok为true ,则value 有值

Query是直接返回字符串

可以⽤ GetQuery 来代替 Query ⽅法。 GetQuery ⽅法的底层实现其实是 c.Request.URL.Query().Get(key) ,通过 url.URL.Query() 来获取所有的参数键值对

仔细看GetQuery的具体使用方式

//本质上是调⽤的GetQueryArray,取的数组中第⼀个值
func (c *Context) GetQuery(key string) (string, bool) {
    if values, ok := c.GetQueryArray(key); ok {
        return values[0], ok
    }
    return "", false
}

func (c *Context) GetQueryArray(key string) ([]string, bool) {
    c.getQueryCache()  //得到缓存,这一点很关键,缓存所有的键值对
    if values, ok := c.queryCache[key]; ok && len(values) > 0 {
        return values, true
    }
    return []string{}, false
}

func (c *Context) getQueryCache() {
   if c.queryCache == nil {
      c.queryCache = c.Request.URL.Query()
   }
}

其中 c.Request.URL.Query() 这个⽅法就是把 ?k=v&k1=v1&k2=v2 这类查询键值对转换为

map[string][]string ,所以还是很耗性能的,这⾥ Gin 采⽤了缓存的做法提⾼了性能挺好,这也是 Gin 成为性能最快的Golang Web 框架的原因之⼀。

4 接收数组和 Map

QueryArray

例如实际业务中,URL⼤概是这样的 ?a=b&a=c&a=d , key 值都⼀ 样,但是对应的 value 不⼀样。

这类URL查询参数,就是⼀个数组,那么在Gin中我们如何获取它们呢?

// 在浏览器里访问http://localhost:8080/?media=blog&media=wechat 会看到如下信息:
// ["blog","wechat"]
func main() {
   r := gin.Default()
   r.GET("/", func(c *gin.Context) {
      fmt.Println("media:", c.QueryArray("media"))
      c.JSON(200, c.QueryArray("media"))
   })
   r.Run(":8080")
}

QueryArray ⽅法也有对应的 GetQueryArray ⽅法,区别在于返回对应的 key 是否存在

QueryMap

把满⾜⼀定格式的URL查询参数,转换为⼀个 map

例如:访问:http://localhost:8080/?ids[0]=a&ids[1]=b&ids[2]=c

输出:{"0":"a","1":"b","2":"c"}

func main() {

   r := gin.Default()

   r.GET("/", func(c *gin.Context) {
      fmt.Println("map:", c.QueryMap("ids"))
      c.JSON(200, c.QueryMap("ids"))
   })
   r.Run(":8080")
}

其中 QueryMap 的原理和具体源码实现:

// QueryMap returns a map for a given query key.
func (c *Context) QueryMap(key string) map[string]string {
   dicts, _ := c.GetQueryMap(key)
   return dicts
}

// GetQueryMap returns a map for a given query key, plus a boolean value
// whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
   c.getQueryCache()
   return c.get(c.queryCache, key)
}

// get is an internal method and returns a map which satisfy conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
    dicts := make(map[string]string)
    exist := false
    for k, v := range m {
        if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
            if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
                exist = true
                dicts[k[i+1:][:j]] = v[0]
            }
        }
    }
    return dicts, exist
}

5 Form 表单

待补充

6 上传⽂件

上传单个文件 FormFile

test目录下的html文件源码:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
</head>
<body>
    <form action="http://127.0.0.1:8080/upload" method="post" enctype="multipart/form-data">
        头像:
        <input type="file" name="file">
        <br>
        <input type="submit" value="提交">
    </form>
</body>
</html>
func main() {
   // 1创建路由,默认使用了两个中间件Logger(),Recovery()
   r := gin.Default()
   // 给表单限制上传大小 (默认 32 MiB)
   r.MaxMultipartMemory = 8 << 20 // 8 MiB
   r.Static("/", "./test")
   // 2绑定路由规则,
   // gin.Context,封装了request和respose
   r.POST("/upload", func(c *gin.Context) {
      

      file, _ := c.FormFile("file")
      log.Println("file:", file.Filename)
      c.SaveUploadedFile(file, "./"+"test/"+file.Filename) // 上传文件到指定的路径
      c.String(200, fmt.Sprintf("%s upload file!", file.Filename))
   })
   // 3监听端口,默认8080
   r.Run(":8080")
}

上传多个文件,就是在上传单个文件的基础上 循环遍历文件列表而已

public 下的html文件为


<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Multiple file upload</title>
</head>
<body>
<h1>Upload multiple files with fields</h1>

<form action="/upload" method="post" enctype="multipart/form-data">
    Name: <input type="text" name="name"><br>
    Email: <input type="email" name="email"><br>
    Files: <input type="file" name="files" multiple><br><br>
    <input type="submit" value="Submit">
</form>
</body>
</html>
func main() {
   router := gin.Default()
   // Set a lower memory limit for multipart forms (default is 32 MiB)
   router.MaxMultipartMemory = 8 << 20 // 8 MiB
   router.Static("/", "./public")
   router.POST("/upload", func(c *gin.Context) {

      name := c.PostForm("name")
      email := c.PostForm("email")

      // Multipart form
      form, err := c.MultipartForm()
      if err != nil {
         c.String(http.StatusBadRequest, fmt.Sprintf("get form err: %s", err.Error()))
         return
      }
      files := form.File["files"]

      for _, file := range files {
         log.Println("file:", file.Filename)
         filename := filepath.Base(file.Filename)
         if err := c.SaveUploadedFile(file, filename); err != nil {
            c.String(http.StatusBadRequest, fmt.Sprintf("upload file err: %s", err.Error()))
            return
         }
      }

      c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email))
   })
   router.Run(":8080")
}

7 分组路由

⽐如基于模块化,把同样模块的放在⼀起,⽐如 基于版本,把相同版本的API放⼀起,便于使⽤。在有的框架中,分组路由也被称之为命名空间

url分组,可以是分版本 等等

func main() {
    r := gin.Default()
    //路由组注册中间件方法1:
    xx1Group := r.Group("/xx1", func(c *gin.Context) { fmt.Println("/xx1中间件") })
    {
        xx1Group.GET("/index", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"msg": "xx1Group"})
        })
        xx1Group.GET("/index2", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"msg": "2222xx1Group"})
        })
    }
    //路由组注册中间件方法2:
    xx2Group := r.Group("/xx2")
    xx2Group.Use(authMiddleware(true))
    {
        xx2Group.GET("/index", func(c *gin.Context) {
            c.JSON(http.StatusOK, gin.H{"msg": "xx2Group"})
        })
    }
    r.Run(":8080")
}

路由中间件

通过 Group ⽅法的定义,我们可以看到,它是可以接收两个参数的:

func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup

第⼀个就是我们注册的分组路由(命名空间);第⼆个是⼀个 ...HandlerFunc ,可以把它理解为这个 分组路由的中间件,所以这个分组路由下的⼦路由在执⾏的时候,都会调⽤它

如上述代码,访问xx1/index2 或者 xx1/index 都会打印出 /xx1中间件

分组路由嵌套

和上述分组的做法是一致

原理解析

以get为例

注意第⼀个参数 relativePath ,这是⼀个相对路径,也就是我们传给 Gin 的是⼀个相对路径,那么是 相对谁的呢?

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

通过这句 absolutePath := group.calculateAbsolutePath(relativePath) 代码,我们可以 看出是相对当前的这个 group (⽅法接收者)的。 现在 calculateAbsolutePath ⽅法的源代码我们暂时不看,回过头来看 Group 这个⽣成分组路由的 ⽅法。

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) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup {
   return &RouterGroup{
      Handlers: group.combineHandlers(handlers),
      basePath: group.calculateAbsolutePath(relativePath),
      engine:   group.engine,
   }
}

这⾥要注意的是,我们通过 gin.Default() ⽣成的 gin.Engine 其实包含⼀个 RouterGroup (嵌套组 合),所以它可以⽤ RouterGroup 的⽅法。 Group ⽅法⼜⽣成了⼀个 *RouterGroup ,这⾥最重要的就是 basePath ,它的值是 group.calculateAbsolutePath(relativePath) ,和我们刚刚暂停的分析的⽅法⼀样,既然这 样,就来看看这个⽅法吧。

func (group *RouterGroup) calculateAbsolutePath(relativePath string) string {
   return joinPaths(group.basePath, relativePath)
}

GIn中间件

Gin框架允许开发者在处理请求的过程中,加⼊⽤户⾃⼰的钩⼦(Hook)函数。这个钩⼦函数就叫中间件,中间件适合处理⼀些公共的业务逻辑,⽐如登录认证、权限校验、数据分⻚、记录⽇志、耗时统计等

在Gin中,我们可以通过Gin提供的默认函数,来构建⼀个⾃带默认中间件的 *Engine 。

 r := gin.Default()

Default 函数会默认绑定两个已经准备好的中间件,它们就是Logger 和 Recovery,帮助我们打印⽇志 输出和 painc 处理。

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

从中我们可以看到,Gin的中间件是通过 Use ⽅法设置的,它接收⼀个可变参数,所以我们同时可以设置 多个中间件。

func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
   engine.RouterGroup.Use(middleware...)
   engine.rebuild404Handlers()
   engine.rebuild405Handlers()
   return engine
}

其实就是Gin定义的⼀个 HandlerFunc ,⽽它在我 们Gin中经常使⽤

r.GET("/", func(c *gin.Context) {
    fmt.Println("HandlerFunc") 
    c.JSON(200, "HandlerFunc")
    })

后⾯的 func(c *gin.Context) 这部分其实就是⼀个 HandlerFunc

中间件实现HTTP Basic Authorization

HTTP Basic Authorization 是HTTP常⽤的认证⽅案,它通过Authorization 请求消息头含有服务器⽤于 验证⽤户代理身份的凭证,格式为:

Authorization: Basic <credentials>

如果认证不成功,服务器返回401 Unauthorized 状态码以及WWW-Authenticate 消息头,让客户端输⼊

⽤户名和密码进⼀步认证。

在Gin中,为我们提供了 gin.BasicAuth 帮我们⽣成基本认证的中间件,⽅便我们的开发。

基本认证的中间件可以用在分组路由中,在特定的url下进行认证

func main() {
   r := gin.Default()
   r.Use(gin.BasicAuth(gin.Accounts{
      "admin": "123456",
   }))
   

   r.GET("/", func(c *gin.Context) {
      body, _ := ioutil.ReadAll(c.Request.Body)
      fmt.Println("---body--- \r\n " + string(body))
      fmt.Println("---header--- \r\n")
      for k, v := range c.Request.Header {
         fmt.Println(k, v)
      }
      fmt.Println("进入主页")
      c.JSON(200, "首页")
   })

   r.Run(":8080")
}
image

中间件注意事项

gin.Default()

gin.Default()默认使⽤了Logger和Recovery中间件,其中:Logger中间件将⽇志写⼊ gin.DefaultWriter,即使配置GIN_MODE=release。Recovery中间件会recover任何panic。如果有 panic的话,会写⼊500响应码。如果不想使⽤上⾯两个默认的中间件,可以使⽤gin.New()新建⼀个没有 任何默认中间件的路由。

gin中间件中使⽤goroutine

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

image-20210321212453452

gin框架中间件c.Next()理解

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

   mid1 := func(c *gin.Context) {
      fmt.Println("mid1 start")
      c.Next()
      fmt.Println("mid1 end")
   }
   mid2 := func(c *gin.Context) {
      fmt.Println("mid2 start")
      c.Next()
      fmt.Println("mid2 end")
   }
   mid3 := func(c *gin.Context) {
      fmt.Println("mid3 start")
      c.Next()
      fmt.Println("mid3 end")
   }
   router.Use(mid1, mid2)
   router.Use(mid3)
   router.GET("/index", func(c *gin.Context) {
      fmt.Println("process get request")
      c.JSON(http.StatusOK, "hello")
      fmt.Println("JSON after") //
      // c.Next() // 这里加是没有用
   })

   router.Run(":8080")
}
  • 正常写next是如下打印 ,类似于递归,洋葱模型

    mid1 start
    mid2 start
    mid3 start
    process get request
    JSON after
    mid3 end
    mid2 end
    mid1 end
    
  • 如果注释掉3个中间件中的c.Next(),则执⾏情况如下,顺序调用每一个中间件

    mid1 start
    mid1 end
    mid2 start
    mid2 end
    mid3 start
    mid3 end
    process get request
    JSON after
    
  • 只在m1中写入c.Next()

    mid1 start
    mid2 start
    mid2 end
    mid3 start
    mid3 end
    process get request
    JSON after
    mid1 end
    

总结:

最后的get路由处理函数可以理解为最后的中间件,在不是调⽤c.Abort()的情况下,所有的中间件 都会被执⾏到。当某个中间件调⽤了c.Next(),则整个过程会产⽣嵌套关系。如果某个中间件调⽤了 c.Abort(),则此中间件结束后会直接返回,后⾯的中间件均不会调⽤

8 json、struct、xml、yaml、protobuf渲染

各种数据格式的响应

func main() {
    r := gin.Default()
    //1. json响应
    r.GET("/someJSON", func(c *gin.Context) {
        c.JSON(200, gin.H{"message": "someJSON", "status": 200})
    })
    //2. 结构体响应
    r.GET("/someStruct", func(c *gin.Context) {
        var msg struct {
            Name    string
            Message string
            Number  int
        }
        msg.Name = "root"
        msg.Message = "message"
        msg.Number = 123
        c.JSON(200, msg)
    })

    //3. XML
    r.GET("/someXML", func(c *gin.Context) {
        c.XML(200, gin.H{"message": "abc"})
    })

    //4. YAML响应
    r.GET("/someYAML", func(c *gin.Context) {
        c.YAML(200, gin.H{"name": "you"})
    })

    //5.Protobuf格式,谷歌开发的高效存储读取的工具
    r.GET("/someProtoBuf", func(c *gin.Context) {
        reps := []int64{int64(1), int64(2)}
        //定义数据
        label := "label"
        //传protobuf格式数据
        data := &protoexample.Test{
            Label: &label,
            Reps:  reps,
        }
        c.ProtoBuf(200, data)
    })

    r.Run(":8080")
}

9 HTML模板渲染

  • gin⽀持加载HTML模板,然后根据模板参数进⾏配置并返回响应的数据,本质上就是字符串替换

  • LoadHTMLGlob()⽅法可以加载模板⽂件

正常渲染html模板

image
func main() {
   r := gin.Default()
   r.LoadHTMLGlob("view/*")
   r.GET("/index", func(c *gin.Context) {
      c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是gin", "name": "you"})
   })
   r.GET("/", func(c *gin.Context) {
      c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是gin", "name": "you"})
   })
   r.Run(":8080")
}

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body bgcolor="#E6E600">
<h1>{{.title}}</h1>
name : {{.name}}
</body>
</html>

将html文件头尾分离

image
func main() {
   r := gin.Default()
   r.LoadHTMLGlob("view2/**/*")
   r.GET("/index", func(c *gin.Context) {
      c.HTML(http.StatusOK, "user/index.html", gin.H{"title": "我是gin", "name": "you2"})
   })
   r.Run()
}

index.html

{{ define "user/index.html" }}
    {{template "public/header" .}}
    name: {{.name}}
    {{template "public/footer" .}}
{{ end }}

header.html

{{define "public/header"}}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>{{.title}}</title>
</head>
<body>
{{end}}

footer.html

{{define "public/footer"}}
      </body>
      </html>
  {{end}}

url重定向

访问http://127.0.0.1:8080/ 会 自动重定向到 http://127.0.0.1:8080/index

func main() {
   r := gin.Default()
   r.LoadHTMLGlob("view/*")
   r.GET("/index", func(c *gin.Context) {
      c.HTML(http.StatusOK, "index.html", gin.H{"title": "我是gin", "name": "you"})
   })
   r.GET("/", func(c *gin.Context) {
      c.Redirect(http.StatusMovedPermanently, "/index")  // 重定向
   })
   r.Run(":8080")
}

静态⽂件⽬录

需要引⼊静态⽂件可以定义⼀个静态⽂件⽬录

r.Static("/assets", "./assets")

10 异步协程

  • goroutine机制可以⽅便地实现异步处理
  • 另外,在启动新的goroutine时,不应该使⽤原始上下⽂,必须使⽤它的只读副本。
func main() {
   r := gin.Default()
   //1. 异步
   r.GET("/long_async", func(c *gin.Context) {
      //需要搞一个副本
      copyContext := c.Copy()
      //异步处理
      go func() {
         time.Sleep(3 * time.Second)
         log.Println("异步执行:" + copyContext.Request.URL.Path)
         // copyContext.JSON(200, gin.H{"message": "someJSON", "status": 200})
      }()
   })

   //2. 同步
   r.GET("/long_sync", func(c *gin.Context) {
      time.Sleep(3 * time.Second)
      log.Println("同步执行:" + c.Request.URL.Path)
   })
   r.Run()
}

作者:小魔童哪吒

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容