Go reflect 反射實(shí)例解析

Go reflect 反射實(shí)例解析

——教壞小朋友系列

0 FBI WARNING

對(duì)于本文內(nèi)容班利,看懂即可,完全不要求學(xué)會(huì)蔼水。


1 現(xiàn)象分析

網(wǎng)上關(guān)于golang反射的文章多如牛毛萍启,可實(shí)際用反射的人,寥寥可數(shù)肋杖。

我分析大家不用反射的原因溉仑,大概有兩點(diǎn):

  • 不會(huì)
  • 笨重

1.1 不會(huì)

網(wǎng)上關(guān)于反射的文章很多,可多數(shù)都是在分析原理状植,并沒有給出實(shí)例浊竟。而反射恰好處在一個(gè)很尷尬的點(diǎn)上:懂了原理不等于會(huì)用。

于是乎津畸,大多數(shù)人高喊口號(hào):“官方不推薦用這個(gè)庫逐沙!”是的,官方不推薦用的庫有兩個(gè):reflectunsafe(嚴(yán)格意義上來說,還有cgo)。

官方說法

It's a powerful tool that should be used with care and avoided unless strictly necessary.

可是我又發(fā)現(xiàn)一個(gè)奇怪現(xiàn)象个从,看看下面這段代碼:

func bytes2str(p []byte) string {
    return *(*string)(unsafe.Pointer(&p))
}

我不知道這段代碼是從哪里流傳出去的(疑似官方庫strings.Builder)宦赠,然后這段代碼就被玩爛了怠肋,說好的不推薦使用unsafe呢端朵?

善意的提醒:
上面這段代碼和bufio.Scannerbufio.Reader共用的時(shí)候宣决,將出現(xiàn)彩蛋穿挨〔腥啵看下面示例:

func main() {
    s := "aaa\nbbb\ncc"
    sc := bufio.NewScanner(strings.NewReader(s))
    sc.Buffer(make([]byte, 4), 4)

    sc.Scan()
    t := bytes2str(sc.Bytes())
    fmt.Println(t) // Output: aaa

    sc.Scan()
    fmt.Println(t) // Output: bbb

    sc.Scan()                                                                                 
    fmt.Println(t) // Output: ccb
}

和您想象中的結(jié)果一樣嗎胧后?如果一樣,恭喜您抱环。

書接前文壳快,言歸正傳。最后镇草,我想眶痰,大家嘴里說著不用不用,其實(shí)不是不想用梯啤,而是不會(huì)用竖伯。當(dāng)大家真正拿到實(shí)例,知道怎么用的時(shí)候因宇,“真香定律”便出現(xiàn)了七婴。

反射也是一樣,大家都說反射不好理解察滑、性能差打厘、容易出錯(cuò),其實(shí)還不是因?yàn)椴粫?huì)用贺辰?所以呢婚惫,今天我不講太多原理了,直接上干貨魂爪,給出一些實(shí)例或模板出來先舷,后面再遇到類似情況,就像上面bytes2str一樣滓侍,拿來套用即可蒋川。

又是一段善意的提醒:
建議大家要學(xué)好閉包接口,這樣在用反射的時(shí)候撩笆,將會(huì)更加得心應(yīng)手(如果您能掌握unsafe捺球,那真真兒是極好的)。

1.2 笨重

大家在使用反射時(shí)夕冲,都在默認(rèn)一個(gè)“無知”概念:我什么都不知道氮兵,所以我要枚舉所有可能,我要實(shí)現(xiàn)所有可能歹鱼。第一個(gè)“所有可能”要求我們檢查所有可能的輸入?yún)?shù)是否合法泣栈,第二個(gè)“所有可能”要求我們實(shí)現(xiàn)所有可能參數(shù)的處理邏輯。

為什么要仔細(xì)檢查所有可能輸入的參數(shù)?因?yàn)?code>reflect庫稍加不慎就會(huì)panic南片。

為什么要實(shí)現(xiàn)所有可能參數(shù)的處理邏輯呢掺涛?參照官方庫jsonreflect.DeepEqual的實(shí)現(xiàn)(其反射代碼之多疼进、之復(fù)雜薪缆,讓人望而卻步),反射不就應(yīng)該這樣用嗎伞广?

但仔細(xì)想想拣帽,我們真的需要這兩個(gè)“所有可能”嗎?不一定嚼锄。如果我們想做到“大而全”减拭,那確實(shí)需要兩個(gè)“所有可能”,但如果我們不要“大而全”呢灾票?我們拿反射處理特定情況行不行呢?

不得不說茫虽,正是這個(gè)無知概念刊苍,讓大家在使用反射時(shí),像是戴著鐐銬在針尖上跳舞濒析,如履薄冰正什,戰(zhàn)戰(zhàn)兢兢。今天号杏,我們暫且拋卻這個(gè)概念婴氮,化巨闕為瑞士軍刀。

從現(xiàn)在開始盾致,我們準(zhǔn)確的知道傳進(jìn)來的參數(shù)是什么主经。如果不知道,很簡單庭惜,限制死罩驻。


2 由高斯求和說起

請(qǐng)解決以下問題:寫程序輸出1+2+3+...+10的結(jié)果。

func main() {
    var sum int
    for i := 1; i <= 10; i++ {
        sum += i
    }
    println(sum)
}

如果把這段代碼抽成函數(shù)呢护赊?

func Gauss(n int) int {
    var sum int
    for i := 1; i <= n; i++ {
        sum += i
    }
    return sum
}

請(qǐng)問惠遏,Gauss函數(shù)為什么需要一個(gè)參數(shù)n?很簡單骏啰,這樣該函數(shù)不止可以求從1加到10的結(jié)果节吮,也可以求從1加到100,加到1000判耕,10000的結(jié)果透绩,而不用每次求和再重新寫段代碼。也就是說,該函數(shù)在某種情況下是通用的:求從1加到n的和渺贤。

通用性雏胃,使得同一段代碼可以在不同的地方被反復(fù)調(diào)用,而不用每次都重新寫一遍相同的邏輯志鞍。

仔細(xì)觀察瞭亮,你會(huì)發(fā)現(xiàn)一個(gè)秘密:99%的通用性代碼,是對(duì)“值”的通用固棚。值是什么统翩?比如var a int = 123,變量a的值是123此洲。

還記得有種數(shù)據(jù)結(jié)構(gòu)叫哈希嗎厂汗?用哈希的時(shí)候,要拋棄傳統(tǒng)通過下標(biāo)找值的思維呜师,轉(zhuǎn)向通過值找下標(biāo)的思維娶桦,這樣哈希才能玩的賊6。

反射也是一樣汁汗,寫反射代碼時(shí)不要總想著對(duì)變量的值操作衷畦,這時(shí)候需要同時(shí)對(duì)變量的值和類型進(jìn)行操作。比如像上面的Gauss函數(shù)知牌,可以對(duì)不同的n值進(jìn)行求和祈争,n的限制是大于等于0。對(duì)變量值的操作角寸,大家都很熟悉菩混。學(xué)習(xí)反射,最主要的扁藕,是學(xué)會(huì)對(duì)變量類型的操作沮峡。本文中所有反射例子,都會(huì)對(duì)類型進(jìn)行限制亿柑,這也是用好反射的一個(gè)關(guān)鍵帖烘。

在接下的例子中,您可以試著從這個(gè)角度出發(fā)橄杨,去理解代碼的意圖秘症。


3 實(shí)例

以下實(shí)例,或模板式矫,都是針對(duì)官方庫的使用乡摹。大家可以重點(diǎn)關(guān)注一下對(duì)類型的操作。

3.1 http轉(zhuǎn)rpc模板

大家用go語言寫的最多的采转,應(yīng)該就是web應(yīng)用了聪廉,寫web應(yīng)用的時(shí)候瞬痘,大家又陷入一個(gè)怪圈中:有沒有一個(gè)好的框架?大家找來找去板熊,發(fā)現(xiàn)還是gin和echo好用框全。

這里,為大家提供一種基于反射的http模板干签,竊以為比gin和echo還要人性化一點(diǎn)津辩。

技能點(diǎn):

  • 閉包
  • 裝飾器
  • 反射

3.1.1 閉包

go語言中閉包有兩種形式:

  • 實(shí)例的方法
  • 函數(shù)嵌套
3.1.1.1 實(shí)例的方法
type T string

func (t T) greating() {
    println(t)
}

t := T("hello world!")
g := t.greating
g()

可以看到,變量gt已經(jīng)沒有關(guān)系容劳,但g還是可以訪問t的內(nèi)容喘沿,這是比較簡單的一類閉包實(shí)現(xiàn),但不常用竭贩,因?yàn)樗偸强梢员坏诙N形式替代蚜印。

3.1.1.2 函數(shù)嵌套

還是用一段廣為流傳的代碼作示例好了:

func Incr() func() int {
    var i int
    return func() int {
        i++
        return i
    }
}

incr := Incr()
println(incr()) // Output: 1
println(incr()) // Output: 2
println(incr()) // Output: 3

由此可見,閉包本身不難理解留量,是不是就像1+1=2一樣簡單窄赋?好了,下面我們將用它推導(dǎo)微積分(手動(dòng)狗頭)楼熄。

3.1.2 裝飾器

在go中忆绰,幾乎所有的接口,都可以使用裝飾器孝赫。比如常用的io.LimtedReader较木、context.Context等红符。http庫青柄,要處理http請(qǐng)求,就要實(shí)現(xiàn)http.Handler接口预侯,實(shí)現(xiàn)該接口的方式有很多致开,http庫給出了非常方便一種:http.HandlerFunc,接下來萎馅,我們用它來實(shí)現(xiàn)http裝飾器双戳。

http庫對(duì)于HandlerFunc的定義如下:

type HandlerFunc func(http.ResponseWriter, *http.Request)

這種形式的定義,注定我們將要用閉包的第二種形式:

func WithRecovery(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if e := recover(); e != nil {
                log.Printf("uri = %v, panic error: %v, stack: %s", r.URL.Path, e, debug.Stack())
                w.WriteHeader(http.StatusInternalServerError)
                w.Write([]byte(http.StatusText(http.StatusInternalServerError)))
            }   
        }()                                                                                   
        handler.ServeHTTP(w, r)
    })  
}

使用方法:

http.NewServeMux().Handle("/ping", WithRecovery(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("pong"))
})))

如果您看過ginecho的代碼糜芳,就會(huì)明白飒货,裝飾器(在http中或叫middleware)的實(shí)現(xiàn)方式大同小異,比如gin.New().Use()峭竣。這些第三方庫最大的好處在于塘辅,寫法上更簡捷易懂。

3.1.3 反射

不知道大家發(fā)現(xiàn)沒有皆撩,大多數(shù)的第三方庫扣墩,包括gin哲银、echo,他們都是在解決路由和中間件的問題呻惕。但大家在處理邏輯時(shí)荆责,拿到的還是最原始的http.Requesthttp.ResponseWriter,如果我們看過官方的rpc庫亚脆,就會(huì)想做院,能不能把http請(qǐng)求轉(zhuǎn)成rpc格式呢?答案是:當(dāng)然可以型酥,用反射山憨!我們先定義用法,再寫膠水代碼弥喉。假設(shè)用法如下:

type Context struct{}  // 一些http信息
type PingReq struct{}  // 參數(shù)
type PingResp struct{} // 業(yè)務(wù)返回值
// 最終業(yè)務(wù)邏輯函數(shù)格式:
func Ping(ctx *Context, req *PingReq, resp *PingResp) Error {
    return nil
}

參照上述格式郁竟,我們來完成膠水邏輯:

  • 獲取函數(shù)類型,從而獲取函數(shù)入?yún)㈩愋?
  • 生成*Context由境、*PingReq棚亩、*PingResp;
  • 將函數(shù)包裝成http.Handler;
  • 調(diào)用函數(shù),接收返回值;
  • 輸出結(jié)果虏杰。

請(qǐng)先在心里默念:“我知道參數(shù)是什么”讥蟆。OK,看主要代碼(后面有完整版):

// function should be like:
//     func Ping(*Context, *PingReq, *PingResp) Error { return nil }
func WithFunc(function interface{}) http.Handler {
    fn := reflect.ValueOf(function) // function代表Ping
    fnTyp := fn.Type() // 獲取函數(shù)類型纺阔,從類型出發(fā)瘸彤,創(chuàng)建變量
    // Context是http信息,所有業(yè)務(wù)邏輯共用的數(shù)據(jù)類型笛钝,可以不用反射
    // 這里獲取Type時(shí)解指針质况,下面reflect.New的時(shí)候拿到的是指針值
    arg1 := fnTyp.In(1).Elem() // type: PingReq
    arg2 := fnTyp.In(2).Elem() // type: PingResp

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := &Context{} // 填入http請(qǐng)求信息
        req := reflect.New(arg1) // 創(chuàng)建請(qǐng)求變量,type: *PingReq
        json.NewDecoder(r.Body).Decode(req.Interface()) // 這步看項(xiàng)目需要自行處理
        resp := reflect.New(arg2) // 創(chuàng)建返回值變量玻靡,type: *PingResp
        // 函數(shù)調(diào)用结榄,傳入預(yù)定的三個(gè)參數(shù),接收Error返回值
        out := fn.Call([]reflect.Value{reflect.ValueOf(ctx), req, resp})
        if ret := out[0].Interface(); ret != nil {
            err := ret.(Error)
            Render(w, err.Code(), err.Error(), nil)
            return
        }
        Render(w, 0, "", resp.Interface())
    })
}

完整代碼:

type Context struct {
    Trace  string
    API    string
    Func   string
    Header http.Header
    Query  url.Values
    Uid    string
}

type Error interface {
    error
    Code() int 
}

type Middleware func(*Context) Error

// 不相關(guān)邏輯從簡
func Render(w http.ResponseWriter, code int, msg string, data interface{}) {
    json.NewEncoder(w).Encode(map[string]interface{}{
        "errcode": code,
        "errmsg": msg,
        "data": data,
    })
}

// function should be like:
//     func Ping(*Context, *PingReq, *PingResp) Error { return nil }
func WithFunc(function interface{}, mws ...Middleware) http.Handler {
    fn := reflect.ValueOf(function)
    fnName := runtime.FuncForPC(fn.Pointer()).Name()
    fnTyp := fn.Type()
    arg1 := fnTyp.In(1).Elem()
    arg2 := fnTyp.In(2).Elem()

    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := &Context{
            Trace:  r.URL.Query().Get("trace_id"),
            API:    r.URL.Path,
            Func:   fnName,
            Header: r.Header,
            Query:  r.URL.Query(),
        }
        for _, mw := range mws {
            err := mw(ctx)
            if err != nil {
                mwName := runtime.FuncForPC(reflect.ValueOf(mw).Pointer()).Name()
                log.Printf("WithFunc middleware %v ctx %+v: %v", mwName, ctx, err)
                Render(w, err.Code(), err.Error(), nil)
                return
            }
        }

        req := reflect.New(arg1)
        json.NewDecoder(r.Body).Decode(req.Interface())
        resp := reflect.New(arg2)
        out := fn.Call([]reflect.Value{reflect.ValueOf(ctx), req, resp})
        if ret := out[0].Interface(); ret != nil {
            err := ret.(Error)
            Render(w, err.Code(), err.Error(), nil)
            return
        }
        Render(w, 0, "", resp.Interface())
    })
}

測(cè)試:

type PingReq struct{}
type PingResp struct{}
func Ping(ctx *Context, req *PingReq, resp *PingResp) Error {
    return nil
}

http.NewServeMux().Handle("/ping", WithFunc(Ping))

這樣寫起邏輯來囤捻,是不是感覺清爽多了臼朗,而且不用關(guān)心底層是不是http了?

3.2 簡易ORM

在一般項(xiàng)目中蝎土,用到數(shù)據(jù)庫视哑,比如mysql,是非常常見的事誊涯。大家用數(shù)據(jù)庫想到的第一件事挡毅,就是找個(gè)趁手的orm。網(wǎng)上比較流行的是gormxorm等醋拧。

那如果不用gorm和xorm慷嗜,自己能不能寫個(gè)簡單實(shí)用的orm呢淀弹?

技能點(diǎn):

  • 反射
  • 封裝

3.2.1 反射

官方庫database/sql提供的接口很原始,比如讀數(shù)據(jù)庆械,就像fmt.Scanf一樣用薇溃,但項(xiàng)目代碼中,用scanf的方式缭乘,會(huì)不會(huì)有點(diǎn)太原始了沐序?比如像下面一樣:

type T struct {
    Id   int64  `db:"id"`
    Name string `db:"name"`
    Age  int    `db:"age"`
}

func main() {
    db, _ := sql.Open("mysql", "xxx")
    row, _ := db.QueryRow("select (id, name, age) from tbl where id=123")
    var t T
    row.Scan(&t.Id, &t.Name, &t.Age)
    fmt.Println(t)
}

這樣寫一個(gè)兩個(gè)表還OK,多了之后肯定要崩潰堕绩,對(duì)每張表都要寫一套如何讀取數(shù)據(jù)的代碼策幼,明明邏輯都是一樣的。這種枯燥的工作就很容易出錯(cuò)奴紧。另外特姐,加個(gè)字段要考慮數(shù)據(jù)庫字段和程序字段的一一嚴(yán)格對(duì)應(yīng)。

我想黍氮,在多數(shù)人心目中唐含,這樣原始的接口不太好用。所以很多人選擇了orm沫浆。那如果沒有orm捷枯,我們能不能把原始接口變的好用起來呢?答案是:當(dāng)然可以专执。我們把這套相同的邏輯抽出來淮捆,用反射忽略類型hard code,即可適應(yīng)所有數(shù)據(jù)的讀取工作本股。先看相同的邏輯:

var t T
fields := []string{"id", "name", "age"}
values := []interface{}{&t.Id, &t.Name, &t.Age}
sql := fmt.Sprintf("select (`%v`) from tbl where id=123", strings.Join(fields, "`, `"))
row, _ := db.QueryRow(sql)
row.Scan(values...)

由上面邏輯攀痊,我們可以看出,我們需要的是fieldsvalues痊末,我們只需要通過反射拿到這兩個(gè)數(shù)組即可蚕苇。由此膠水邏輯為:

  • 獲取參數(shù)類型;
  • 遍歷參數(shù)struct每個(gè)字段;
  • 通過每個(gè)字段得到數(shù)據(jù)庫字段名和該字段地址;
  • 返回?cái)?shù)據(jù)庫字段名列表和字段地址列表哩掺。

后續(xù)邏輯會(huì)通過字段名和字段地址讀取數(shù)據(jù)庫數(shù)據(jù):

func SQLQueryFields(v interface{}) (fields []string, values []interface{}) {
    val := reflect.Indirect(reflect.ValueOf(v))
    typ := val.Type()
    for i := 0; i < val.NumField(); i++ {
        if !val.Field(i).CanInterface() {
            continue
        }
        // 數(shù)據(jù)庫字段名
        fields = append(fields, typ.Field(i).Tag.Get("db"))
        // 注意是指針凿叠,需要取址操作
        values = append(values, val.Field(i).Addr().Interface())
    }
    return
}

可見,代碼量非常之少嚼吞。誰說反射很笨重的盒件?來看使用方法:

func main() {
    var t T
    fields, values := SQLQueryFields(&t)
    fmt.Println(fields) // Output: [id name age]
    // fmt.Sscanf("1 eachain 26", "%d %s %d", &t.Id, &t.Name, &t.Age)
    fmt.Sscanf("1 eachain 26", "%d %s %d", values...)
    fmt.Println(t) // Output: {1 eachain 26}
}

接下來就是如何讀多條記錄的問題了:

  • 通過參數(shù)(列表)逐級(jí)獲取真實(shí)元素類型;
  • 生成一個(gè)新元素舱禽,按上面邏輯讀瘸吹蟆;
  • 將新元素追加到列表中誊稚。
// slice type: *[]T or *[]*T
func QueryRows(rows *sql.Rows, slice interface{}) error {
    ls := reflect.ValueOf(slice).Elem() // type: []T or []*T
    elemTyp := ls.Type().Elem() // type: T or *T
    isPtr := false
    if elemTyp.Kind() == reflect.Ptr {
        elemTyp = elemTyp.Elem() // type: T
        isPtr = true
    }

    for rows.Next() {
        elem := reflect.New(elemTyp) // type: *T
        _, fields := SQLQueryFields(elem.Interface())
        err := rows.Scan(fields...)
        if err != nil {
            return err 
        }
        // 以下代碼翻譯成正常代碼即: ls = append(ls, elem)
        if isPtr {
            ls.Set(reflect.Append(ls, elem))
        } else {
            ls.Set(reflect.Append(ls, elem.Elem()))
        }
    }

    return rows.Err()
}

是不是覺得寫個(gè)orm沒有想象中的辣么難翔始?

3.2.2 封裝

封裝不是本文重點(diǎn)罗心,這里不作具體介紹。但有了上面基礎(chǔ)城瞎,封裝一個(gè)類似gorm這樣xxx.Select(fields).From(table).Where(cond).OrderBy(order).Limit(limit).Rows(&slice)的鏈?zhǔn)秸{(diào)用渤闷,應(yīng)該不難。

同樣的insert脖镀、update飒箭、delete等操作是一樣的,這里不再重復(fù)蜒灰。

注意弦蹂,本節(jié)到此為止。
這只是個(gè)orm的雛形强窖,點(diǎn)到即可凸椿,要想得到一個(gè)完善的orm,最終還是要寫成類似標(biāo)準(zhǔn)庫encoding/json或三方庫gorm那種形式的翅溺。但那已經(jīng)不是本文要關(guān)心的點(diǎn)了削饵。

3.3 兼容不同格式的API返回結(jié)果

在對(duì)接一些系統(tǒng)的時(shí)候,會(huì)發(fā)現(xiàn)接口返回格式各異未巫,例如:

{"errcode": 1, "errmsg": "some error", "data": {"a": 789}}
{"err_code": 1, "err_msg": "some error", "data": {"a": 789}}
{"errcode": "1", "err_msg": "some error", "a": 789}

相信很多人遇到這種情況時(shí)窿撬,想到的第一件事不是怎么解決,而是WTF叙凡。這種結(jié)果劈伴,能不能兼容呢?

提醒:
這種情況用以下代碼是不可行的:

var errcode json.Number
var errmsg string
m := map[string]interface{}{
    "errcode":  &errcode,
    "err_code": &errcode,
    "errmsg":   &errmsg,
    "err_msg":  &errmsg,
}
json.Unmarshal(p, &m)

技能點(diǎn):

  • 接口
  • 反射

3.3.1 接口

由于這里主要在說json數(shù)據(jù)處理握爷,所以在這只提json.Unmarshaler接口了跛璧。

encoding/json中,Unmarshaler接口允許外部可以按自定義的格式解析json數(shù)據(jù)新啼,這給了我們兼容不同json格式的可能追城。比如json.Number。同樣的燥撞,我們也可以寫一個(gè)ErrCode以兼容數(shù)字座柱、字符串格式的json返回結(jié)果:

type ErrCode int 

func (ec *ErrCode) UnmarshalJSON(p []byte) (err error) {
    if p[0] == '"' {
        p = p[1 : len(p)-1]
    }   
    *(*int)(ec), err = strconv.Atoi(string(p))
    return
}

如果發(fā)現(xiàn)p是字符串,我們?nèi)サ綦p引號(hào)后按數(shù)字解析即可物舒。

3.3.2 反射

我們首先明確一下要解決的問題:

  • 兼容字段名errcode和err_code兩種格式色洞,同理errmsg;
  • data有時(shí)在json的data字段中冠胯,有時(shí)在外面平鋪展開火诸。

我們?cè)谝婚_始就提到了,用map是不能解決這個(gè)問題的荠察,那如果用struct呢置蜀?我們需要做什么:

  • 需要兩個(gè)字段奈搜,一個(gè)是errcode,一個(gè)是err_code盯荤,但由指針指向相同的地址媚污。這樣無論出現(xiàn)哪個(gè),最終都將解析到同一個(gè)值里面廷雅;
  • data放在json的data字段耗美,同時(shí)將data展開到外層。這樣無論哪種情況航缀,都會(huì)被解析到商架,另一種被忽略;
  • 生成一個(gè)全新的struct芥玉,將指針指向傳入?yún)?shù)的內(nèi)存地址蛇摸,并返回。

來看示例代碼:

type CallError struct {
    ErrCode int
    ErrMsg  string
}

func (ce CallError) Error() string {
    return strconv.Itoa(ce.ErrCode) + ": " + ce.ErrMsg
}

func Join(err *CallError, data interface{}) interface{} {
    var fields []reflect.StructField
    var values []reflect.Value

    // 第一個(gè)errcode灿巧,類型是*ErrCode赶袄,兼容數(shù)字和字符串,并指向err.ErrCode
    fields = append(fields, reflect.StructField{
        Name: "ErrCode1",
        Tag:  `json:"errcode"`,
        Type: reflect.TypeOf(new(ErrCode)), // 類型是指針
    })
    values = append(values, reflect.ValueOf((*ErrCode)(&err.ErrCode)))

    // 第二個(gè)err_code抠藕,類型同樣是*ErrCode饿肺,也指向err.ErrCode
    fields = append(fields, reflect.StructField{
        Name: "ErrCode2",
        Tag:  `json:"err_code"`,
        Type: reflect.TypeOf(new(ErrCode)),
    })
    values = append(values, reflect.ValueOf((*ErrCode)(&err.ErrCode)))

    // 第一個(gè)errmsg,指向err.ErrMsg
    fields = append(fields, reflect.StructField{
        Name: "ErrMsg1",
        Tag:  `json:"errmsg"`,
        Type: reflect.TypeOf(new(string)),
    })
    values = append(values, reflect.ValueOf(&err.ErrMsg))

    // 第二個(gè)err_msg盾似,也指向err.ErrMsg
    fields = append(fields, reflect.StructField{
        Name: "ErrMsg2",
        Tag:  `json:"err_msg"`,
        Type: reflect.TypeOf(new(string)),
    })
    values = append(values, reflect.ValueOf(&err.ErrMsg))

    // data字段敬辣,并將參數(shù)data置于此
    fields = append(fields, reflect.StructField{
        Name: "Data",
        Tag:  `json:"data"`,
        Type: reflect.TypeOf(new(interface{})).Elem(), // 類型是interface{}
    })
    values = append(values, reflect.ValueOf(data))

    // 將參數(shù)data所有字段展開放到最外層
    v := reflect.Indirect(reflect.ValueOf(data))
    if v.Kind() == reflect.Struct { // 標(biāo)注
        t := v.Type()
        for i := 0; i < t.NumField(); i++ {
            f := t.Field(i)
            f.Type = reflect.PtrTo(f.Type) // 類型是指針
            fields = append(fields, f)
            values = append(values, v.Field(i).Addr())
        }
    }

    // 生成新struct,并將指針指向參數(shù)
    dst := reflect.New(reflect.StructOf(fields))
    v = dst.Elem()
    for i := 0; i < len(values); i++ {
        v.Field(i).Set(values[i])
    }
    // 返回可被json.Umarshal的對(duì)象
    return dst.Interface()
}

上面標(biāo)注的地方是通用寫法零院,還有一種不通用的簡便寫法:

t := v.Type()
fields = append(fields, reflect.StructField{
    Anonymous: true,
    Name:      t.Name(),
    Type:      reflect.PtrTo(t),
})
values = append(values, v.Addr())

這種寫法要求data不能帶有Method溉跃,如果有,將panic告抄。

有一點(diǎn)我沒在注釋里面提及:所有字段均是以指針形式出現(xiàn)撰茎。為什么要這樣做?

設(shè)想:如果新struct各字段不用指針打洼,當(dāng)我們reflect.New(reflect.StructOf(fields))的時(shí)候龄糊,go會(huì)為該struct分配空間。當(dāng)我們解析json的時(shí)候拟蜻,最終結(jié)果都將解析到新生成的struct里面去绎签,也就是說枯饿,值會(huì)被寫入新分配的內(nèi)存空間酝锅,而不是我們傳入?yún)?shù)的內(nèi)存空間。竹籃打水一場(chǎng)空奢方,這不是我們想要的結(jié)果搔扁。

可如果我們用指針爸舒,我們可以任意指定指針指向的空間。并且指針符合反射第三定律“可寫”的條件稿蹲。最終我們將新生成struct的各個(gè)字段指向我們希望的內(nèi)存空間:傳入的參數(shù)扭勉。這樣,當(dāng)json解析的時(shí)候苛聘,會(huì)將結(jié)果寫入到我們想要的內(nèi)存空間中涂炎。

3.4 公共字段操作

前排警告:
本節(jié)內(nèi)容超綱,不要求看懂设哗。

請(qǐng)求的返回結(jié)果中唱捣,有一些相同字段,需要在接口返回的時(shí)候网梢,自動(dòng)填充這些字段的值震缭,比如:

type Resp struct {
    Time   int64  `json:"time"`
    Server string `json:"server"`
    Num    int    `json:"num"`
}

type Resp2 struct {
    Time   int64  `json:"time"`
    Server string `json:"server"`
    Name   string `json:"name"`
}

type Resp3 struct {
    Time   int64  `json:"time"`
    Server string `json:"server"`
    WTF    string `json:"wtf"`
}

要求:Time自動(dòng)填充當(dāng)前時(shí)間,Server自動(dòng)填充hostname战虏。
真實(shí)情況是:返回結(jié)果拣宰,有可能是指針*Resp,有可能不是指針Resp烦感。

如果是指針巡社,用反射很好解決,因?yàn)樽侄味际?code>CanSet()的手趣,但如果出現(xiàn)不是指針的情況呢重贺?為什么現(xiàn)實(shí)總是如此殘酷?也罷回懦,會(huì)反射的我們總是可以見招拆招气笙。

大家想到的第一個(gè)方案,很可能是深度拷貝(DeepCopy)怯晕,這段代碼在github上有人實(shí)現(xiàn)潜圃,通過深度拷貝,獲取一個(gè)可寫的reflect.Value舟茶,從而寫入TimeServer谭期。提前聲明,DeepCopy是正規(guī)解決方案吧凉。但本文一開始說了隧出,不會(huì)用這種巨無霸的實(shí)現(xiàn)方式,我們要另辟蹊徑阀捅。

附DeepCopy方案示例代碼:

func autoSetTimeAndServer(resp interface{}) {
    val := reflect.ValueOf(resp)
    if val.Kind() != reflect.Ptr {
        val = reflect.New(val.Type())
        DeepCopy(val.Interface(), resp)
    }
    val = val.Elem()

    val.FieldByName("Time").SetInt(time.Now().Unix())
    hostname, _ := os.Hostname()
    val.FieldByName("Server").SetString(hostname)
}

技能點(diǎn):

  • unsafe
  • 反射

3.4.1 unsafe

本文一開始就提到了unsafe胀瞪,即上文的bytes2str,要用unsafe,需要對(duì)go數(shù)據(jù)底層存儲(chǔ)有一定的認(rèn)知凄诞。比如為什么可以直接把[]byte通過unsafe轉(zhuǎn)成string圆雁,反過來由string轉(zhuǎn)[]byte行不行?

uintptrunsafe.Pointer有什么區(qū)別帆谍?

uintptr是一個(gè)變量伪朽,unsafe.Pointer是一個(gè)指針。這意味著如果GC在移動(dòng)內(nèi)存的時(shí)候汛蝙,會(huì)更新unsafe.Pointer烈涮,因?yàn)樗侵羔槨6粫?huì)修改uinptr窖剑,因?yàn)樗皇莻€(gè)普通變量跃脊。因此,不要用uintptr的變量保存內(nèi)存地址苛吱,但可以用它保存偏移量酪术。內(nèi)存地址可能會(huì)因?yàn)镚C而變化,偏移量不會(huì)翠储。

我們將要解決的這個(gè)問題绘雁,最終傳入的參數(shù),肯定是以interface{}的形式傳入援所,該interface{}里面可能是Resp庐舟、*RespResp2住拭、*Resp2等等各種情況挪略,那我們先看一下reflect庫中關(guān)于interface{}的定義:

type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

一個(gè)interface{}由兩部分組成:typval(word)滔岳。注意看杠娱,word是個(gè)指針!這意味著什么谱煤?CanSet摊求,是的,從某種意義上來說刘离,它是可以被修改的室叉。我們做個(gè)實(shí)驗(yàn):

type iface struct {
    typ unsafe.Pointer // 我們知道類型信息,忽略該值
    val unsafe.Pointer // 這里我將字段名換成了val
}

func echo(x interface{}) {
    println(x.(int)) // Output: 123
    *(*int)((*iface)(unsafe.Pointer(&x)).val) = 456 // 標(biāo)注
    println(x.(int)) // Output: 456
}

func main() {
    var i int = 123
    echo(i)
    println(i) // 想想輸出多少硫惕,為什么茧痕? // Output: 123
}

我們知道,如果將標(biāo)注的代碼換成reflect.ValueOf(x).SetInt(456)恼除,程序?qū)anic踪旷,因?yàn)樽兞?code>x不是CanSet的。所以我們可以看到,reflect不能做到事(指x.SetInt)埃脏,unsafe做到了搪锣。

3.4.2 反射

有了上面的基礎(chǔ)秋忙,相信這一步非常簡單了彩掐,我們可以得到*Resp的地址,要修改其中某個(gè)字段的值灰追,還需要一樣?xùn)|西:偏移量堵幽。我們將通過反射得到它:

func autoSetTimeAndServer(resp interface{}) {
    typ := reflect.TypeOf(resp)
    if typ.Kind() == reflect.Ptr {
        typ = typ.Elem()
    }
    timeField, _ := typ.FieldByName("Time")
    serverField, _ := typ.FieldByName("Server")
    i := (*iface)(unsafe.Pointer(&resp))
    *(*int64)(unsafe.Pointer(uintptr(i.val) + timeField.Offset)) = time.Now().Unix()
    *(*string)(unsafe.Pointer(uintptr(i.val) + serverField.Offset)), _ = os.Hostname()
}

func send(resp interface{}) {
    autoSetTimeAndServer(resp)
    p, _ := json.Marshal(resp)
    fmt.Println(string(p))
}

func main() {
    send(&Resp{Num: 123})
    // Output: {"time":1587802664,"server":"WTF","num":123}

    send(Resp{Num: 456})
    // Output: {"time":1587802664,"server":"WTF","num":456}
}

本例用到的反射知識(shí)不多,大多數(shù)是需要掌握unsafe才能完成的操作弹澎,但將兩者結(jié)合起來朴下,會(huì)有一定難度。所以這里我將項(xiàng)目中實(shí)際遇到的情況精簡了很多苦蒿,這樣大家可以更方便理解殴胧。

3.4.3 類型

前面小節(jié)中介紹的方法是非常容易理解的一種方法,不知道大家有沒有發(fā)現(xiàn):該方法是在忽略類型的情況下實(shí)現(xiàn)需求的佩迟。我們?cè)诘诙糠终f了团滥,反射最重要的是學(xué)會(huì)對(duì)類型的操作。下面我們將介紹對(duì)類型操作以實(shí)現(xiàn)需求报强。

在說具體操作前灸姊,我們需要先看reflect庫的源碼:

func TypeOf(i interface{}) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

簡化一下:

func TypeOf(i interface{}) Type {
    return (*emptyInterface)(unsafe.Pointer(&i)).typ
}

由此可見,一個(gè)interface{}中天然包含反射信息秉溉!下面我們自己組裝一個(gè)reflect.Type試試(注意reflect.Type本身是interface{}):

func autoSetTimeAndServer(resp interface{}) {
    typ := reflect.TypeOf("")
    t := (*iface)(unsafe.Pointer(&typ)) // 獲取*reflect.rtype類型
    r := (*iface)(unsafe.Pointer(&resp))
    // 生成一個(gè)新的interface{}:
    //   typ是*reflect.rtype,確保該interface{}實(shí)現(xiàn)了reflect.Type;
    //   val是resp的類型力惯,實(shí)際上我們是對(duì)該類型進(jìn)行操作.
    // 這個(gè)問題倒著想會(huì)比較輕松:
    //   將一個(gè)*reflect.rtype類型的值轉(zhuǎn)成interface{}
    //   iface的typ和val各是什么?
    typ = *(*reflect.Type)(unsafe.Pointer(&iface{t.typ, r.typ}))
    if typ.Kind() == reflect.Ptr {
        typ = typ.Elem()
    }

    timeField, _ := typ.FieldByName("Time")
    serverField, _ := typ.FieldByName("Server")
    *(*int64)(unsafe.Pointer(uintptr(r.val) + timeField.Offset)) = time.Now().Unix()
    *(*string)(unsafe.Pointer(uintptr(r.val) + serverField.Offset)), _ = os.Hostname()
}

如果您已經(jīng)理解了上述操作,馬上您就會(huì)意識(shí)到召嘶,沒必要那么麻煩父晶,我們直接修改resp的類型不就行了:

func autoSetTimeAndServer(resp interface{}) {
    typ := reflect.TypeOf(resp)
    if typ.Kind() != reflect.Ptr {
        // 將類型變?yōu)橹羔槪允箁esp CanSet
        typ = reflect.PtrTo(typ)
        (*iface)(unsafe.Pointer(&resp)).typ =
            (*iface)(unsafe.Pointer(&typ)).val
    }
    // 現(xiàn)在resp肯定是指針弄跌,即val.CanSet()肯定為true
    val := reflect.ValueOf(resp).Elem()

    val.FieldByName("Time").SetInt(time.Now().Unix())
    hostname, _ := os.Hostname()
    val.FieldByName("Server").SetString(hostname)
}

既然已經(jīng)到這種程度了诱建,那直接修改reflect.Value可不可行呢?當(dāng)然也可以碟绑,這里不再贅述俺猿,只給出示例:

type rvalue struct {
    typ  unsafe.Pointer
    ptr  unsafe.Pointer
    flag uintptr
}

func autoSetTimeAndServer(resp interface{}) {
    val := reflect.ValueOf(resp)
    if val.Kind() != reflect.Ptr {
        typ := reflect.PtrTo(val.Type())
        rv := (*rvalue)(unsafe.Pointer(&val))
        rv.typ = (*iface)(unsafe.Pointer(&typ)).val
        rv.flag = uintptr(reflect.Ptr)
    }
    val = val.Elem()

    val.FieldByName("Time").SetInt(time.Now().Unix())
    hostname, _ := os.Hostname()
    val.FieldByName("Server").SetString(hostname)
}

4 反射練習(xí)

關(guān)于反射的理論文章很多,大家在看完理論后格仲,會(huì)產(chǎn)生一種“我覺得我又行了”的錯(cuò)覺押袍。當(dāng)實(shí)際用到反射的時(shí)候,卻發(fā)現(xiàn)凯肋,理論谊惭,離真刀真槍的實(shí)戰(zhàn)還有一定距離。所以,我在這里給出一種練習(xí)方式:把普通代碼翻譯成反射代碼圈盔。

4.1 chan操作

本例旨在:
體現(xiàn)普通代碼和反射代碼別無二致豹芯。(反例例外

我們經(jīng)常會(huì)用chan當(dāng)緩沖隊(duì)列用,舉個(gè)最簡單的例子:

ch := make(chan int)

go func() {
    for i := 0; i < 3; i++ {
        ch <- i
    }
    close(ch)
}() 

for n := range ch {
    println(n)
}

將上面代碼翻譯成反射代碼驱敲,該怎么寫呢铁蹈?很簡單,不需要解釋:

ch := reflect.MakeChan(reflect.ChanOf(reflect.BothDir,
    reflect.TypeOf(int(0))), 0)

go func() {
    for i := 0; i < 3; i++ {
        ch.Send(reflect.ValueOf(i))
    }
    ch.Close()
}()

for {
    n, ok := ch.Recv()
    if !ok {
        break
    }
    println(n.Int())
}

下面給出一個(gè)反例众眨,用正常代碼不好實(shí)現(xiàn)握牧,用反射卻異常輕松的一段代碼(非重點(diǎn),不解釋):

func send(chs interface{}, value interface{}) int {
    val := reflect.ValueOf(value)
    ls := reflect.Indirect(reflect.ValueOf(chs))
    n := ls.Len()
    sc := make([]reflect.SelectCase, 0, n)
    for i := 0; i < n; i++ {
        sc = append(sc, reflect.SelectCase{
            Chan: ls.Index(i),
            Dir:  reflect.SelectSend,
            Send: val,
        })  
    }   
    sc = append(sc, reflect.SelectCase{
        Dir:  reflect.SelectDefault,
    })  
    i, _, _ := reflect.Select(sc)
    if i == len(sc)-1 {
        return -1 // default case
    }
    return i
}

func recv(chs interface{}, dst interface{}) int {
    ls := reflect.Indirect(reflect.ValueOf(chs))
    n := ls.Len()
    sc := make([]reflect.SelectCase, 0, n)
    for i := 0; i < n; i++ {
        sc = append(sc, reflect.SelectCase{
            Chan: ls.Index(i),
            Dir:  reflect.SelectRecv,
        })
    }
    sc = append(sc, reflect.SelectCase{
        Dir:  reflect.SelectDefault,
    })
    i, v, ok := reflect.Select(sc)
    if !ok {
        return -1
    }
    reflect.ValueOf(dst).Elem().Set(v)
    return i
}

func main() {
    chs := make([]chan int, 10)
    for i := 0; i < len(chs); i++ {
        chs[i] = make(chan int, 1)
    }

    println("send to:", send(chs, 123))
    // Output: send to 7

    var v int
    println("recv from:", recv(chs, &v))
    // Output: recv from 7

    println(v)
    // Output: 123
}

如果用正常代碼娩梨,上面的邏輯怎么實(shí)現(xiàn)沿腰?要知道數(shù)組chs是變長的,有可能增長狈定,也可能縮短颂龙,這種情況下,用正常代碼的select寫法將會(huì)很難纽什,比較簡單的一種實(shí)現(xiàn)方式是:

func init() {
    rand.Seed(time.Now().UnixNano())
}

// 注意重新洗牌時(shí)要記錄原來的下標(biāo)
func shuffle(chs []chan int) ([]chan int, []int) {
    tmp := make([]chan int, len(chs))
    idx := make([]int, len(chs))
    copy(tmp, chs)
    for i := 0; i < len(idx); i++ {
        idx[i] = i 
    }   
    rand.Shuffle(len(tmp), func(i, j int) {
        tmp[i], tmp[j] = tmp[j], tmp[i]
        idx[i], idx[j] = idx[j], idx[i]
    })
    return tmp, idx 
}

func send(chs []chan int, v int) int {
    chs, idx := shuffle(chs) // 每次都要重新洗牌
    for i, ch := range chs {
        select {
        case ch <- v:
            return idx[i]
        default:
        }
    }
    return -1
}

func recv(chs []chan int, v *int) int {
    chs, idx := shuffle(chs) // 每次都要重新洗牌
    for i, ch := range chs {
        select {
        case *v = <-ch:
            return idx[i]
        default:
        }
    }
    return -1
}

4.2 用反射寫鏈表

本例旨在:
反射代碼只是把普通代碼完全展開措嵌,完整的寫法。

一種經(jīng)典的鏈表寫法:

type node struct {
    Data interface{}
    Next *node
}

type list *node

func makeList(ls *list, data ...interface{}) {
    for _, d := range data {
        *ls = &node{d, nil}
        ls = (*list)(&(*ls).Next)
    }
}

func main() {
    var ls list
    makeList(&ls, 1, true, "Hello world")
    for p := ls; p != nil; p = p.Next {
        fmt.Println(p.Data)
    }
    // Output:
    // 1
    // true
    // Hello world
}

請(qǐng)將上面代碼makeList函數(shù)用反射實(shí)現(xiàn):

func makeList(ls interface{}, data ...interface{}) {
    p := reflect.ValueOf(ls).Elem()
    typ := p.Type().Elem()
    for _, d := range data {
        e := reflect.New(typ)
        e.Elem().FieldByName("Data").Set(reflect.ValueOf(d))
        p.Set(e)
        p = e.Elem().FieldByName("Next")
    }
}

4.3 鉆空子

看下面這段代碼:

var t struct{ a int }
println(reflect.ValueOf(&t).Elem().Field(0).CanAddr())
// Output: true

看到這的時(shí)候稿湿,是不是有人動(dòng)了歪心思:如果CanAddr铅匹,那是不是說可以通過它使未導(dǎo)出的字段可寫?很不幸饺藤,官方告訴你:不行包斑。

var t struct{ a int }
v := reflect.ValueOf(&t).Elem().Field(0)
println(v.CanAddr())              // Output: true
println(v.Addr().Elem().CanSet()) // Output: false
println(v.Addr().CanInterface())  // Output: false

官方使未導(dǎo)出字段可讀已經(jīng)是突破下限的仁慈了:

var t struct{ a int }
t.a = 123
println(reflect.ValueOf(&t).Elem().Field(0).Int())
// Output: 123

如果你就是想寫未導(dǎo)出的字段,能不能做到呢涕俗?參考前面unsafe罗丰。

最后善意的提醒:
unsafe修改未導(dǎo)出字段,這種做法將破壞原有邏輯再姑,十分危險(xiǎn)萌抵!

type Buffer struct {
    buf       []byte
    off       int
    bootstrap [64]byte
    lastRead  uint8
}

buf := bytes.NewBuffer(nil)
buf.WriteString("1234567890")
println(buf.String()) // Output: 1234567890
(*Buffer)(unsafe.Pointer(buf)).off = 5
println(buf.String()) // Output: 67890

如果是接口,記得要先用iface過渡一下:

type iface struct {
    typ unsafe.Pointer
    val unsafe.Pointer
}

type digest struct {
    h   [5]uint32
    x   [64]byte
    nx  int 
    len uint64
}

hs := sha1.New()
hs.Write([]byte("1234567890"))
d := (*digest)((*iface)(unsafe.Pointer(&hs)).val)
println(d.len) // Output: 10

4.4 小結(jié)

結(jié)合上述例子來看元镀,反射其實(shí)沒有什么神秘之處绍填,反射代碼和普通代碼在邏輯上是完全一致的,唯一不太一樣的點(diǎn)是:type栖疑,普通代碼中的類型是直接聲明出來的采桃,反射代碼中的類型需要自己推導(dǎo)盖溺。


5 結(jié)語

如果您已經(jīng)看懂了本文所涉及的一些案例儒将,恭喜您粘勒,反射對(duì)您已經(jīng)不再神秘揭糕。反射已經(jīng)由一把重劍巨闕化為靈巧翻飛的瑞士軍刀。

關(guān)于反射的例子還有很多锻霎,但在這里不再過多涉及著角,因?yàn)榇蠖嗬雍捅疚睦佑挟惽ぶ睢?/p>

在本文中,您看到的反射代碼旋恼,都相當(dāng)?shù)暮唵卫艨冢钪匾那疤嵩谟冢何覀儗?duì)類型的限制。拋開大而全的理念蚌铜,去做小而美锨侯,將更容易發(fā)揮反射的魅力嫩海。

不知道您有沒有注意到冬殃,在案例中,反射和非反射代碼叁怪,兩種寫法是混合交叉著在用的审葬。這樣做的好處在于,非反射代碼能在一定程度上限制反射的使用奕谭,并保證反射代碼的正確性涣觉。

大家會(huì)看到,我只是給出了實(shí)現(xiàn)方案血柳,而沒有給出優(yōu)化方案官册,相信已經(jīng)理解了本文內(nèi)容的您,優(yōu)化方案將很容易得到难捌。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末膝宁,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子根吁,更是在濱河造成了極大的恐慌员淫,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件击敌,死亡現(xiàn)場(chǎng)離奇詭異介返,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)沃斤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門圣蝎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人衡瓶,你說我怎么就攤上這事徘公。” “怎么了鞍陨?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵步淹,是天一觀的道長从隆。 經(jīng)常有香客問我,道長缭裆,這世上最難降的妖魔是什么键闺? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮澈驼,結(jié)果婚禮上辛燥,老公的妹妹穿的比我還像新娘。我一直安慰自己缝其,他們只是感情好挎塌,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著内边,像睡著了一般榴都。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上漠其,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天嘴高,我揣著相機(jī)與錄音,去河邊找鬼和屎。 笑死拴驮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的柴信。 我是一名探鬼主播套啤,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼随常!你這毒婦竟也來了潜沦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤线罕,失蹤者是張志新(化名)和其女友劉穎止潮,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钞楼,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡喇闸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了询件。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片燃乍。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖宛琅,靈堂內(nèi)的尸體忽然破棺而出刻蟹,到底是詐尸還是另有隱情,我是刑警寧澤嘿辟,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布舆瘪,位于F島的核電站片效,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏英古。R本人自食惡果不足惜淀衣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望召调。 院中可真熱鬧膨桥,春花似錦、人聲如沸唠叛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽艺沼。三九已至册舞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間澳厢,已是汗流浹背环础。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來泰國打工囚似, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剩拢,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓饶唤,卻偏偏與公主長得像徐伐,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子募狂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355