gin 入门教程

引入gin

要求go1.14以上,首先通过'go get -u github.com/gin-gonic/gin'下载gin包再引入,或者引入后通过'go mod tidy'安装。引入方式:

main.go中

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

一般还需要引入"net/http",返回时使用http.StatusOK

快速尝试

在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() //默认80端口,可通过 r.Run(":8888")改为8888端口
}

// curl 127.0.0.1:8888/ping

路由

GET, POST, PUT, PATCH, DELETE, OPTIONS

func main() {
    // 默认使用logger recovery 中间件, 等价下面注释的代码
    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()
}

参数获取

路径内参数

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

    // 识别 /user/john/,注意最后一个‘/’  /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 [...]")
    })

通过query获取参数

    //  /welcome?firstname=Jane&lastname=Doe
    router.GET("/welcome", func(c *gin.Context) {
        firstname := c.DefaultQuery("firstname", "Guest") // 默认值
        lastname := c.Query("lastname") // c.Request.URL.Query().Get("lastname") 的缩减写法

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

请求接口和传参

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

name=manu&message=this_is_great

map做为参数

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

请求接口和传参

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

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

上传文件

    // 设置处理文件内存,不代表上传文件大小, 默认32M
    router.MaxMultipartMemory = 8 << 20  // 8 MiB
    router.POST("/upload", func(c *gin.Context) {
        // 单个文件
        file, _ := c.FormFile("file")
        log.Println(file.Filename)

        // 保存文件 dist := "./file_2022_04_12" + file.Filename
        // 最好别用file.Filename,首先这个值不是肯定有的,其次易导致文件名冲突
        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) 中获取的类型需要与panic("foo")中类型一致,可使用空接口,处理好后续逻辑即可

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

    // 注意,这段代码不可出现在处理异常的下面, 否则上面代码无效
    // r.Use(gin.Recovery())

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

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

请求日志

func main() {
    // 向日志文件写入时不需要颜色
    gin.DisableConsoleColor()

    // 日志文件,每次重启时会将现有文件内容删除
    f, _ := os.Create("gin.log")
    gin.DefaultWriter = io.MultiWriter(f)

    // 写入文件同时在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")
}

自定义日志内容

func main() {

    // 向日志文件写入时不需要颜色
    gin.DisableConsoleColor()

    // 日志文件,每次重启时会将现有文件内容删除
    f, _ := os.Create("gin.log")
    gin.DefaultWriter = io.MultiWriter(f)

    // 写入文件同时在IDE中输出日志
    // gin.DefaultWriter = io.MultiWriter(f, os.Stdout)


    router := gin.New()

    // 通过中间件实现日志格式自定义
    // 默认写入 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和标准表单值 (foo=bar&boo=baz)转换为结构模型
当使用json时需要设置json解释

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

// 支持 form、json、xml Password为空时不会报错
type Login struct {
    User     string `form:"user" json:"user" xml:"user"  binding:"required"`
    Password string `form:"password" json:"password" xml:"password" binding:"-"`
}

binding:"-":当参数值为空时不会报错
binding:"required":当参数值为空时会报错

绑定示例

// 支持 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
        // 根据 content-type header 推测绑定形式.
        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")
}

自定义模型转换时的验证

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验证是否晚于当前时间,gtfield=CheckIn : CheckOut晚于CheckIn, 时间格式’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"`
}

// 定义验证逻辑
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()

    // 注册验证,
    if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
        v.RegisterValidation("bookabledate", bookableDate)
    }
    router.GET("/bookable", getBookable)
    router.Run(":8050")
}

// 模型转换
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 仅解析查询参数,可解析出page,不可解析name和message
ShouldBind 可解析查询参数和post参数

// 使用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")
}

多端口号,启动多个服务,并使用端口号进行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)
    }
}

优雅启停

go1.8之前的可使用第三方库,或者自己编写内部包,1.8可使用http.Server的Shutdown函数

/*
1、新建协程启动服务
2、新建channel接收关闭信号
3、阻塞接收信号
4、接收到信号后执行关闭流程
*/


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...")

    // 通知服务器还有5秒关闭
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel() // 进入执行栈,不使用defer将会立刻关闭

    // 根据上下文继续执行请求,如果在5秒内执行不完则强制关闭
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }

    log.Println("Server exiting")
}

嵌套结构解析

嵌套结构,嵌套结构指针,匿名嵌套,请求方式一样,均可通过“Bind”函数自动解析

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"}}

设置获取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()
}

请求ip限制

router := gin.Default()
// 设置可访问的ip地址切片
router.SetTrustedProxies([]string{"192.168.1.2"})

CDN设置方式

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"

请求测试

编写请求接口,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")
}

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

推荐阅读更多精彩内容