【實(shí)踐】Golang的goroutine和通道的8種姿勢(shì)

前言

如果說(shuō)php是最好的語(yǔ)言麻削,那么golang就是最并發(fā)的語(yǔ)言沟启。
支持golang的并發(fā)很重要的一個(gè)是goroutine的實(shí)現(xiàn)读规,那么本文將重點(diǎn)圍繞goroutine來(lái)做一下相關(guān)的筆記链峭,以便日后快速留戀拙徽。

10s后伸辟,以下知識(shí)點(diǎn)即將靠近:
1.從并發(fā)模型說(shuō)起
2.goroutine的簡(jiǎn)介
3.goroutine的使用姿勢(shì)
4.通道(channel)的簡(jiǎn)介
5.重要的四種通道使用
6.goroutine死鎖與處理
7.select的簡(jiǎn)介
8.select的應(yīng)用場(chǎng)景
9.select死鎖

正文

1.從并發(fā)模型說(shuō)起

看過(guò)很多大神簡(jiǎn)介幕侠,各種研究高并發(fā)靶累,那么就通俗的說(shuō)下并發(fā)侵歇。
并發(fā)目前來(lái)看比較主流的就三種:

1.多線程

每個(gè)線程一次處理一個(gè)請(qǐng)求骂澄,線程越多可并發(fā)處理的請(qǐng)求數(shù)就越多,但是在高并發(fā)下惕虑,多線程開(kāi)銷(xiāo)會(huì)比較大坟冲。

2.協(xié)程

無(wú)需搶占式的調(diào)度,開(kāi)銷(xiāo)小溃蔫,可以有效的提高線程的并發(fā)性健提,從而避免了線程的缺點(diǎn)的部分

3.基于異步回調(diào)的IO模型

說(shuō)一個(gè)熟悉的,比如nginx使用的就是epoll模型伟叛,通過(guò)事件驅(qū)動(dòng)的方式與異步IO回調(diào)私痹,使得服務(wù)器持續(xù)運(yùn)轉(zhuǎn),來(lái)支撐高并發(fā)的請(qǐng)求

為了追求更高效和低開(kāi)銷(xiāo)的并發(fā)统刮,golang的goroutine來(lái)了紊遵。

2.goroutine的簡(jiǎn)介

定義:在go里面,每一個(gè)并發(fā)執(zhí)行的活動(dòng)成為goroutine侥蒙。

詳解:goroutine可以認(rèn)為是輕量級(jí)的線程暗膜,與創(chuàng)建線程相比,創(chuàng)建成本和開(kāi)銷(xiāo)都很小鞭衩,每個(gè)goroutine的堆棧只有幾kb学搜,并且堆椡奚疲可根據(jù)程序的需要增長(zhǎng)和縮小(線程的堆棧需指明和固定),所以go程序從語(yǔ)言層面支持了高并發(fā)瑞佩。

程序執(zhí)行的背后:當(dāng)一個(gè)程序啟動(dòng)的時(shí)候聚磺,只有一個(gè)goroutine來(lái)調(diào)用main函數(shù),稱(chēng)它為主goroutine炬丸,新的goroutine通過(guò)go語(yǔ)句進(jìn)行創(chuàng)建瘫寝。

3.goroutine的使用姿勢(shì)

3.1單個(gè)goroutine創(chuàng)建

在函數(shù)或者方法前面加上關(guān)鍵字go,即創(chuàng)建一個(gè)并發(fā)運(yùn)行的新goroutine御雕。

上代碼:

package main

import (
    "fmt"
    "time"
)

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go HelloWorld()      // 開(kāi)啟一個(gè)新的并發(fā)運(yùn)行
    time.Sleep(1*time.Second)
    fmt.Println("我后面才輸出來(lái)")
}

以上執(zhí)行后會(huì)輸出:

Hello world goroutine
我后面才輸出來(lái)

需要注意的是矢沿,執(zhí)行速度很快滥搭,一定要加sleep酸纲,不然你一定可以看到goroutine里頭的輸出。

這也說(shuō)明了一個(gè)關(guān)鍵點(diǎn):當(dāng)main函數(shù)返回時(shí)瑟匆,所有的gourutine都是暴力終結(jié)的闽坡,然后程序退出。

3.2多個(gè)goroutine創(chuàng)建

package main

import (
    "fmt"
    "time"
)

func DelayPrint() {
    for i := 1; i <= 4; i++ {
        time.Sleep(250 * time.Millisecond)
        fmt.Println(i)
    }
}

func HelloWorld() {
    fmt.Println("Hello world goroutine")
}

func main() {
    go DelayPrint()    // 開(kāi)啟第一個(gè)goroutine
    go HelloWorld()    // 開(kāi)啟第二個(gè)goroutine
    time.Sleep(2*time.Second)
    fmt.Println("main function")
}

函數(shù)輸出:

Hello world goroutine
1
2
3
4
5
main function
1
2
3
4
5
6
7

有心的同學(xué)可能會(huì)發(fā)現(xiàn)愁溜,DelayPrint里頭有sleep疾嗅,那么會(huì)導(dǎo)致第二個(gè)goroutine堵塞或者等待嗎?
答案是:no
疑惑:當(dāng)程序執(zhí)行g(shù)o FUNC()的時(shí)候冕象,只是簡(jiǎn)單的調(diào)用然后就立即返回了代承,并不關(guān)心函數(shù)里頭發(fā)生的故事情節(jié),所以不同的goroutine直接不影響渐扮,main會(huì)繼續(xù)按順序執(zhí)行語(yǔ)句论悴。

4.通道(channel)的簡(jiǎn)介

4.1簡(jiǎn)介

如果說(shuō)goroutine是Go并發(fā)的執(zhí)行體,那么”通道”就是他們之間的連接墓律。
通道可以讓一個(gè)goroutine發(fā)送特定的值到另外一個(gè)goroutine的通信機(jī)制膀估。

4.2聲明&傳值&關(guān)閉

聲明

var ch chan int      // 聲明一個(gè)傳遞int類(lèi)型的channel
ch := make(chan int) // 使用內(nèi)置函數(shù)make()定義一個(gè)channel

//=========

ch <- value          // 將一個(gè)數(shù)據(jù)value寫(xiě)入至channel,這會(huì)導(dǎo)致阻塞耻讽,直到有其他goroutine從這個(gè)channel中讀取數(shù)據(jù)
value := <-ch        // 從channel中讀取數(shù)據(jù)察纯,如果channel之前沒(méi)有寫(xiě)入數(shù)據(jù),也會(huì)導(dǎo)致阻塞针肥,直到channel中被寫(xiě)入數(shù)據(jù)為止

//=========

close(ch)            // 關(guān)閉channel

有沒(méi)注意到關(guān)鍵字”阻塞“饼记?,這個(gè)其實(shí)是默認(rèn)的channel的接收和發(fā)送慰枕,其實(shí)也有非阻塞的具则,請(qǐng)看下文。

5.重要的四種通道使用

1.無(wú)緩沖通道

說(shuō)明:無(wú)緩沖通道上的發(fā)送操作將會(huì)被阻塞捺僻,直到另一個(gè)goroutine在對(duì)應(yīng)的通道上執(zhí)行接收操作乡洼,此時(shí)值才傳送完成崇裁,兩個(gè)goroutine都繼續(xù)執(zhí)行。

上代碼:

package main

import (
    "fmt"
    "time"
)
var done chan bool
func HelloWorld() {
    fmt.Println("Hello world goroutine")
    time.Sleep(1*time.Second)
    done <- true
}
func main() {
    done = make(chan bool)  // 創(chuàng)建一個(gè)channel
    go HelloWorld()
    <-done
}

輸出:

Hello world goroutine

由于main不會(huì)等goroutine執(zhí)行結(jié)束才返回束昵,前文專(zhuān)門(mén)加了sleep輸出為了可以看到goroutine的輸出內(nèi)容拔稳,那么在這里由于是阻塞的,所以無(wú)需sleep锹雏。

(小嘗試:可以將代碼中”done <- true”和”<-done”巴比,去掉再執(zhí)行,看看會(huì)發(fā)生啥礁遵?)

2.管道

通道可以用來(lái)連接goroutine轻绞,這樣一個(gè)的輸出是另一個(gè)輸入。這就叫做管道佣耐。

例子:

package main

import (
    "fmt"
    "time"
)
var echo chan string
var receive chan string

// 定義goroutine 1 
func Echo() {
    time.Sleep(1*time.Second)
    echo <- "咖啡色的羊駝"
}

// 定義goroutine 2
func Receive() {
    temp := <- echo // 阻塞等待echo的通道的返回
    receive <- temp
}


func main() {
    echo = make(chan string)
    receive = make(chan string)

    go Echo()
    go Receive()

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

在這里不一定要去關(guān)閉channel政勃,因?yàn)榈讓拥睦厥諜C(jī)制會(huì)根據(jù)它是否可以訪問(wèn)來(lái)決定是否自動(dòng)回收它。(這里不是根據(jù)channel是否關(guān)閉來(lái)決定的)

3.單向通道類(lèi)型

當(dāng)程序則夠復(fù)雜的時(shí)候兼砖,為了代碼可讀性更高奸远,拆分成一個(gè)一個(gè)的小函數(shù)是需要的。

此時(shí)go提供了單向通道的類(lèi)型讽挟,來(lái)實(shí)現(xiàn)函數(shù)之間channel的傳遞懒叛。

上代碼:

package main

import (
    "fmt"
    "time"
)

// 定義goroutine 1
func Echo(out chan<- string) {   // 定義輸出通道類(lèi)型
    time.Sleep(1*time.Second)
    out <- "咖啡色的羊駝"
    close(out)
}

// 定義goroutine 2
func Receive(out chan<- string, in <-chan string) { // 定義輸出通道類(lèi)型和輸入類(lèi)型
    temp := <-in // 阻塞等待echo的通道的返回
    out <- temp
    close(out)
}


func main() {
    echo := make(chan string)
    receive := make(chan string)

    go Echo(echo)
    go Receive(receive, echo)

    getStr := <-receive   // 接收goroutine 2的返回

    fmt.Println(getStr)
}

程序輸出:

咖啡色的羊駝
4.緩沖管道

goroutine的通道默認(rèn)是是阻塞的,那么有什么辦法可以緩解阻塞耽梅?
答案是:加一個(gè)緩沖區(qū)薛窥。

對(duì)于go來(lái)說(shuō)創(chuàng)建一個(gè)緩沖通道很簡(jiǎn)單:

ch := make(chan string, 3) // 創(chuàng)建了緩沖區(qū)為3的通道

//=========
len(ch)   // 長(zhǎng)度計(jì)算
cap(ch)   // 容量計(jì)算

6.goroutine死鎖與友好退出

6.1 goroutine死鎖

來(lái)一個(gè)死鎖現(xiàn)場(chǎng)一:

package main

func main() {
    ch := make(chan int)
    <- ch // 阻塞main goroutine, 通道被鎖
}

輸出:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()

死鎖現(xiàn)場(chǎng)2:

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的數(shù)據(jù)沒(méi)有被其他goroutine讀取走,堵塞當(dāng)前goroutine
        chb <- 0
    }()

    <- chb // chb 等待數(shù)據(jù)的寫(xiě)
}

為什么會(huì)有死鎖的產(chǎn)生眼姐?

非緩沖通道上如果發(fā)生了流入無(wú)流出诅迷,或者流出無(wú)流入,就會(huì)引起死鎖妥凳。
或者這么說(shuō):goroutine的非緩沖通道里頭一定要一進(jìn)一出竟贯,成對(duì)出現(xiàn)才行。
上面例子屬于:一:流出無(wú)流入逝钥;二:流入無(wú)流出

當(dāng)然屑那,有一個(gè)例外:

func main() {
    ch := make(chan int)
    go func() {
       ch <- 1
    }()
}

執(zhí)行以上代碼將會(huì)發(fā)現(xiàn),竟然沒(méi)有報(bào)錯(cuò)艘款。
what持际?
不是說(shuō)好的一進(jìn)一出就死鎖嗎?
仔細(xì)研究會(huì)發(fā)現(xiàn)哗咆,其實(shí)根本沒(méi)等goroutine執(zhí)行完蜘欲,main函數(shù)自己先跑完了,所以就沒(méi)有數(shù)據(jù)流入主的goroutine晌柬,就不會(huì)被阻塞和報(bào)錯(cuò)

6.2 goroutine的死鎖處理

有兩種辦法可以解決:

1.把沒(méi)取走的取走便是

package main

func main() {
    cha, chb := make(chan int), make(chan int)

    go func() {
        cha <- 1 // cha通道的數(shù)據(jù)沒(méi)有被其他goroutine讀取走姥份,堵塞當(dāng)前goroutine
        chb <- 0
    }()

    <- cha // 取走便是
    <- chb // chb 等待數(shù)據(jù)的寫(xiě)
}

2.創(chuàng)建緩沖通道

package main

func main() {
    cha, chb := make(chan int, 3), make(chan int)

    go func() {
        cha <- 1 // cha通道的數(shù)據(jù)沒(méi)有被其他goroutine讀取走郭脂,堵塞當(dāng)前goroutine
        chb <- 0
    }()

    <- chb // chb 等待數(shù)據(jù)的寫(xiě)
}

這樣的話(huà),cha可以緩存一個(gè)數(shù)據(jù)澈歉,cha就不會(huì)掛起當(dāng)前的goroutine了展鸡。除非再放兩個(gè)進(jìn)去,塞滿(mǎn)緩沖通道就會(huì)了埃难。

7.select的簡(jiǎn)介

定義:在golang里頭select的功能與epoll(nginx)/poll/select的功能類(lèi)似莹弊,都是堅(jiān)挺IO操作,當(dāng)IO操作發(fā)生的時(shí)候涡尘,觸發(fā)相應(yīng)的動(dòng)作忍弛。

select有幾個(gè)重要的點(diǎn)要強(qiáng)調(diào):

1.如果有多個(gè)case都可以運(yùn)行,select會(huì)隨機(jī)公平地選出一個(gè)執(zhí)行考抄,其他不會(huì)執(zhí)行

上代碼:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)

    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    case <-ch:
        fmt.Println("黃色的羊駝")
    }
}

輸出:

(隨機(jī))二者其一

1
2.case后面必須是channel操作细疚,否則報(bào)錯(cuò)。

上代碼:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    ch<-1
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    case 2:
        fmt.Println("黃色的羊駝")
    }
}

輸出報(bào)錯(cuò):

2 evaluated but not used
select case must be receive, send or assign recv
3.select中的default子句總是可運(yùn)行的座泳。所以沒(méi)有default的select才會(huì)阻塞等待事件

上代碼:

package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意這里備注了惠昔。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    default:
        fmt.Println("黃色的羊駝")
    }
}

輸出:

黃色的羊駝
4.沒(méi)有運(yùn)行的case幕与,那么江湖阻塞事件發(fā)生報(bào)錯(cuò)(死鎖)
package main

import "fmt"

func main() {
    ch := make (chan int, 1)
    // ch<-1   <= 注意這里備注了挑势。
    select {
    case <-ch:
        fmt.Println("咖啡色的羊駝")
    }
}

輸出報(bào)錯(cuò):

fatal error: all goroutines are asleep - deadlock!

8.select的應(yīng)用場(chǎng)景

1.timeout 機(jī)制(超時(shí)判斷)
package main

import (
    "fmt"
    "time"
)

func main() {
    timeout := make (chan bool, 1)
    go func() {
        time.Sleep(1*time.Second) // 休眠1s,如果超過(guò)1s還沒(méi)I操作則認(rèn)為超時(shí)啦鸣,通知select已經(jīng)超時(shí)啦~
        timeout <- true
    }()
    ch := make (chan int)
    select {
    case <- ch:
    case <- timeout:
        fmt.Println("超時(shí)啦!")
    }
}

以上是入門(mén)版潮饱,通常代碼中是這么寫(xiě)的:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make (chan int)
    select {
    case <-ch:
    case <-time.After(time.Second * 1): // 利用time來(lái)實(shí)現(xiàn),After代表多少時(shí)間后執(zhí)行輸出東西
        fmt.Println("超時(shí)啦!")
    }
}
2.判斷channel是否阻塞(或者說(shuō)channel是否已經(jīng)滿(mǎn)了)
package main

import (
    "fmt"
)

func main() {
    ch := make (chan int, 1)  // 注意這里給的容量是1
    ch <- 1
    select {
    case ch <- 2:
    default:
        fmt.Println("通道channel已經(jīng)滿(mǎn)啦诫给,塞不下東西了!")
    }
}
3.退出機(jī)制
package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {
        DONE: 
        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                break DONE // 跳出 select 和 for 循環(huán)
            default:
            }
        }
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

輸出:

1532390471
1532390472
1532390473
stop
1532390474

這邊要強(qiáng)調(diào)一點(diǎn):退出循環(huán)一定要用break + 具體的標(biāo)記香拉,或者goto也可以。否則其實(shí)不是真的退出中狂。

package main

import (
    "fmt"
    "time"
)

func main() {
    i := 0
    ch := make(chan string, 0)
    defer func() {
        close(ch)
    }()

    go func() {

        for {
            time.Sleep(1*time.Second)
            fmt.Println(time.Now().Unix())
            i++

            select {
            case m := <-ch:
                println(m)
                goto DONE // 跳出 select 和 for 循環(huán)
            default:
            }
        }
        DONE:
    }()

    time.Sleep(time.Second * 4)
    ch<-"stop"
}

輸出:

1532390525
1532390526
1532390527
1532390528
stop
9.select死鎖

select不注意也會(huì)發(fā)生死鎖凫碌,前文有提到一個(gè),這里分幾種情況胃榕,重點(diǎn)再次強(qiáng)調(diào):

1.如果沒(méi)有數(shù)據(jù)需要發(fā)送盛险,select中又存在接收通道數(shù)據(jù)的語(yǔ)句,那么將發(fā)送死鎖

package main
func main() {  
    ch := make(chan string)
    select {
    case <-ch:
    }
}

預(yù)防的話(huà)加default勋又。

空select苦掘,也會(huì)引起死鎖

package main

func main() {  
    select {}
}

版權(quán)聲明:本文為CSDN博主「咖啡色的羊駝」的原創(chuàng)文章,遵循CC 4.0 by-sa版權(quán)協(xié)議楔壤,轉(zhuǎn)載請(qǐng)附上原文出處鏈接及本聲明鹤啡。
原文鏈接:https://blog.csdn.net/u011957758/article/details/81159481

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蹲嚣,隨后出現(xiàn)的幾起案子递瑰,更是在濱河造成了極大的恐慌祟牲,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件抖部,死亡現(xiàn)場(chǎng)離奇詭異疲眷,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)您朽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)狂丝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人哗总,你說(shuō)我怎么就攤上這事几颜。” “怎么了讯屈?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵蛋哭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我涮母,道長(zhǎng)谆趾,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任叛本,我火速辦了婚禮沪蓬,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘来候。我一直安慰自己跷叉,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布营搅。 她就那樣靜靜地躺著云挟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪转质。 梳的紋絲不亂的頭發(fā)上园欣,一...
    開(kāi)封第一講書(shū)人閱讀 52,682評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音休蟹,去河邊找鬼沸枯。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鸡挠,可吹牛的內(nèi)容都是我干的辉饱。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拣展,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彭沼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起备埃,我...
    開(kāi)封第一講書(shū)人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤姓惑,失蹤者是張志新(化名)和其女友劉穎褐奴,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體于毙,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡敦冬,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了唯沮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片脖旱。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖介蛉,靈堂內(nèi)的尸體忽然破棺而出萌庆,到底是詐尸還是另有隱情,我是刑警寧澤币旧,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布践险,位于F島的核電站,受9級(jí)特大地震影響吹菱,放射性物質(zhì)發(fā)生泄漏巍虫。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一鳍刷、第九天 我趴在偏房一處隱蔽的房頂上張望占遥。 院中可真熱鬧,春花似錦倾剿、人聲如沸筷频。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至担忧,卻和暖如春芹缔,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背瓶盛。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工最欠, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人惩猫。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓芝硬,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親轧房。 傳聞我的和親對(duì)象是個(gè)殘疾皇子拌阴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

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