Go语言之接口

接口

接口类型是对其他类型行为的概括与抽象。通过使用接口,我们可以写出更加灵活和通用的函数,这些函数不用绑定在一个特定类型的实现上。

很多面向对象的编程语言都有接口的概念,Go语言的接口的独特之处在于它是隐式实现的。换句话说,对于一个具体的类型,无需声明它实现了哪些接口,只要提供接口所必需的方法即可。这种设计让你无需改变已有的类型的实现,就可以为这些类型创建新的接口,对于那些不能修改包的类型,这一点特别有用。

声明接口并实现

声明一个Car接口,并new出三种车辆去实现这个接口,代码如下:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main

import (
"fmt"
)

type Car interface {
run()
}

type BuckCar struct {
}

type AudiCar struct {
}

type BMWCar struct {
}

func (buck BuckCar) run() {
fmt.Println("I am BuckCar, I can run!")
}

func (audicar AudiCar) run() {
fmt.Println("I am AudiCar, I can run!")
}

func (bmwCar BMWCar) run() {
fmt.Println("I am BMWCar, I can run!")
}

func main() {
var car Car

car = new(BuckCar)
car.run()

car = new(AudiCar)
car.run()

car = new(BMWCar)
car.run()

}` </pre>

接口即约定

接口是一种抽象类型,它并没有暴露所含数据的布局或者内部结构,当然也没有那些数据的基本操作,它所提供的仅仅是一些方法而已。

我们平时使用较多的fmt.Printf()fmt.Sprintf(),前者负责把结果发到标准输出(标准输出其实是一个文件),后者把结果以string类型返回。格式化是这两个函数中最复杂的部分,这两个函数的很类似,但是是有差异的,但是如果因为这一点差异就把这两个函数的格式化重新写一遍,那就太繁琐了!

恰好接口机制就可以解决这个问题。下图就是,Go SDK关于fmt.Printf()fmt.Sprintf(),封装fmt.Fprintf()的代码。

image.png

Fprintf的前缀F指文件,表示格式化的输出会写入第一个实参所指代的文件。对于Pringtf,第一个实参就是os.Stdout,它属于*os.File类型。对于Sprintf,尽管第一个实参不是文件,但它模拟了一个文件:&buf就是一个指向内存缓冲区的指针,与文件类似,这个缓冲区也可以写入多个字节。

io.Writer接口定义了Fprintf和调用者之间的约定。一方面,这个约定要求调用者提供的具体类型(比如:*os.File或者*byte.Buffer)包含一个与其签名和行为一致的Writer方法。另一方面,这个约定保证了Fprintf能使用任何满足io.Writer接口的参数。Fprintf只需要能调用参数的Write函数,无需假设它写入的是一个文件还是一段内存。因为fmt.Fprintf仅依赖io.Writer接口约定的方法,对参数的具体类型没有要求。

接口类型

一个接口类型定义了一套方法,如果一个具体类型要实现该接口,那么必须实现接口类型中的所有方法。

io.Writer是一个广泛使用的接口,它负责所有可以写入字节的类型抽象,包括文件、内存缓冲区、网络连接、HTTP客户端、打包器(archiver)、散列器(hasher)等。io包还定义了很多接口。Reader就抽象了所有可以读取字节的类型,Closer就抽象了所有可以关闭的类型,比如文件或者网络连接。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package io

type Reader interface{
Read(p []byte)(n int,err error)
}

type Closer interface{
Closer() error
}` </pre>

另外,我们还可以发现通过组合一有借口得到新的接口,比如:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`type ReadWriter interface{
Reader
Writer
}

type ReadWriteClooser interface{
Reader
Writer
Closer
}` </pre>

如上的语法称为嵌入式接口,与嵌入式结构类似,让我们可以直接使用一个接口,而不用逐一写出这个接口所包含的方法。

实现接口

如果一个类型实现了一个接口要求的所有方法,那么这个类型实现了这个接口。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main

import (
"bytes"
"io"
"os"
"time"
)

var w io.Writer

func main() {
w = os.Stdout // OK os.File有write方法
w = new(bytes.Buffer) // OK /
bytes.Buffer有write方法
w = time.Second // 编译错误,time.Duration缺少write方法
}` </pre>

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main

import (
"bytes"
"io"
"os"
)

var rwc io.ReadWriteCloser

func main() {

rwc = os.Stdout //OK io.ReadWriteCloser有write方法
rwc = new(bytes.Buffer) // 编译错误,
bytes.Buffer缺少Close方法
}` </pre>

当右侧表达式也是一个接口时,该规则也有效:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main

import (
"bytes"
"io"
"os"
"time"
)

var w io.Writer

var rwc io.ReadWriteCloser

func main() {
w = os.Stdout // OK os.File有write方法
w = new(bytes.Buffer) // OK /
bytes.Buffer有write方法
w = time.Second // 编译错误,time.Duration缺少write方法

rwc = os.Stdout //OK io.ReadWriteCloser有write方法
rwc = new(bytes.Buffer) // 编译错误,
bytes.Buffer缺少Close方法

w =rwc // OK io.ReadWriteCloser有write方法
rwc = w //编译错误,io.Writer没有Close方法
}` </pre>

对于每一个具体类型T而言,部分方法的接收者就是T,而其他方法的接收者就是*T指针。同时我们对类型T的变量直接调用*T的方法也可以是合法的,只要改变量是可变的,编译器隐式地帮助我们完成了取地址的操作。但这个仅仅是语法糖,类型T的方法没有对应的*T多,所以实现的接口也可能比对应的指针少。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">type IntSet struct{/*...*/} func (*IntSet) String() string var _ = IntSet{}.String() // 编译错误,String方法需要*IntSet接收者 </pre>

但是可以从一个IntSet变量上调用该方法:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var s IntSet var _ = s.String() // OK s是一个变量,&s有String() </pre>

因为只有*IntSETString(),所以也只有*IntSet实现了fmt.Stringer

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var _ fmt.Stringer = &s // OK var _ fmt.Stringer = s // 编译错误: IntSet缺少String方法 </pre>

就像信封封装了信件,接口也封装了对应的类型和数据,只有通过接口暴露的方法才可以调用,类型的其他方法则无法通过接口来调用:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`os.Stdout.Write([]byte("hello")) //OK: os.File有Write()
os.Stdout.Close() //
os.File有Close方法

var w io.Writer
w = os.Stdout
w.Write([]byte("hello")) // OK: io.Writer有Write方法
w.Close() // 编译错误: io.Writer 缺少close方法` </pre>

因为空接口类型对其实现类型没有任何要求,所以我们可以把任何值赋给空接口类型,就像下面这样:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var any interface{} any = 123 any = "mua~" any = map[string]int{"one"P:1} any = new(bytes.Buffer) </pre>

使用flag.Value()来解析参数

flag.Value是Go语言里面的标准接口。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`var peroid = flag.Duration("peroid",3*time.Second,"sleep.peroid")

func main(){
flag.Parse()
fmt.Printf("我睡着了~ Sleeping for %v...",peroid)
time.Sleep(
peroid)
fmt.Println("我睡醒了~")
}` </pre>

默认的睡眠时间是3s,但是可以通过 -peroid命令行来控制。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">./sleep -peroid 20ms Sleeeping for 50ms... </pre>

接口值

从概念上来讲,一个接口类型的值(简称接口值)其实有两部分:一个具体类型和该类型的一个值。二者称为接口的动态类型和动态值。对于像Go这样的静态类型语言,类型仅仅是一个编译时的概念,所以类型不是一个值。

如下四个语句中,变量w有三个不同的值(第一个和第三个是同一个值):

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var w io.writer w = os.Stdout w = new(bytes.Buffer) w = nil </pre>

接下来让我们详细查看一下在每个语句之后w的值和相关的动态行为。

  • 第一个语句声明了w:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var w io.Writer </pre>

在Go语言中,变量总是初始化为一个特定的值,接口也不例外。接口的零值就是把它的动态类型和值都设为nil

一个接口值是否是nil取决于它的动态类型,所以现在这是一个nil接口值。可以使用w == nil或者w != nil来检测一个接口值是否是nil。调用一个nil接口的任何方法都会导致崩溃:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w.Write([]byte("hello")) //崩溃: 对空指针取引用值 </pre>

  • 第二个语句把一个*os.File类型赋值给了w:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w = os.Stdout </pre>

这个赋值把一个具体类型隐式转换为一个接口类型,它与对应的显式转换io.Writer(os.Stdout)等价。不管这种类型的转换是隐式地还是显式的,它都可以转换操作数的类型和值。

  • 第三个语句把一个*bytes.Buffer类型的值赋值给了接口值:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w = new(bytes.Buffer) </pre>

动态类型现在是*buyes.Buffer,动态值现在则是一个指向新分配缓冲区的指针。

调用Write方法的机制也跟第二个语句一致:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">w.Write([]byte("hello")) //把"hello"写入bytes.Buffer`` </pre>

  • 第四个语句把nil赋给了接口值:

这个语句把动态类型和动态值都设置为nil,把w恢复到了它刚声明时的状态。

一个接口值可以指向多个任意大的动态值。比如,time.Time类型可以表示一个时刻,它是一个包含几个非导出字段的结构。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var x interface{} = time.Now() </pre>

值得注意的是,在比较两个接口值时,如果两个接口值的动态类型一致,但对应的动态值是不可比较的(比如:slice),那么这个比较就会以宕机而结束。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">var x interface{} = []int{1,2,3} fmt.Println(x == x) // 宕机:视图比较不可比较的类型 []int </pre>

含有空指针的非空接口

空的接口值(其中不包含任何信息)与仅仅动态值为nil的接口值是不一样的。

这种微妙的区别成为让每个Go程序员困惑过的陷阱。

示例代码如下:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`const debug = true

func mian(){
var buf *bytes.Buffer
if debug {
buf = new(bytes.Buffer)
}
f(buf) //注意:微妙的错误
if debug {
//....
}
}

// 如果out不是nil,那么会向其写入输出的数据
func f(out io.writer){
// ...
if out != nil{
out.Write([]byte("done!\n"))
}
}` </pre>

main函数调用f时,它把一个类型为*bytes.Buffer的空指针赋给了out参数,所以out的动态值为空。但它的动态类型是*bytes.Buffer,这表示out是一个包含非空指针的非空接口。所以防御性检查out != nil仍然是true

使用 sort.interface 来排序

Go语言的sort.Sort函数对序列和其中元素的布局无任何要求,它使用sort.Interface接口来指定通用排序算法和每个具体的序列类型之间的协议。

一个原地排序算法需要知道三个元素:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package sort

type Interface interface {
Len() int //序列长度
Less(i,j int) bool // 元素的含义 i,j 是元素的下标
Swap(i,j int) // 如何交换两个元素
}` </pre>

要对序列排序,需要先确定一个实现了如上三个方法的类型,接着把sort.Sort函数应用到上面这类方法的实例上。我们先实现一个最简单的例子:字符串slice。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`type StringSlice []string

func (p StringSlice) Len() int{
return Len(p)
}

func (p StringSlice) Less(i,j int) bool{
return p[i] <p[j]
}

func (p StringSlice) Swap(i,j int){
p[i],p[j] = p[j], p[i]
}` </pre>

现在就可以对一个字符串slice进行排序,只须简单地把一个slice转换为StringSlice类型即可。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">sort.Sort(StringSlice(names)) </pre>

类型转换生成了一个新的slice,与原始的names有同样的长度、容量、底层数组,不同的就是额外增加了三个用排序的方法。

由于字符串排序很常用,sort包也提供了直接排序的Strings函数。上面的代码可以直接写成:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">sort.String(names) </pre>

http.Handler接口

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`net/http

package http

type Handler interface{
ServeHTTP (w Response, r *Request)
}

func ListenAndServe(address string, h Handler) error` </pre>

ListenAndServe函数需要一个服务器地址,比如“localhost:8000”,以及一个Handler接口的实例(用来接受所有的请求)。这个函数会一直执行,直到服务出错。

假设我们有一个电子商务网站,使用一个数据库来存储商品和价格(以美元计价)的映射。如下:程序将展示一个最简单的实现,它用一个map类型(命名为database)来代表仓库,再加上一个ServeHTTP方法来满足http.Handler接口。该函数遍历整个map,并输出其中的元素:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main

import (
"fmt"
"log"
"net/http"
)

type doolars float32

func (d doolars) String() string {
return fmt.Sprintf("$%.2f", d)
}

type database map[string]doolars

func (db database) ServeHTTP(w http.ResponseWriter, rep *http.Request) {
for item, price := range db {
fmt.Fprintf(w, "%s: %s \n", item, price)
}
}

func main() {
db := database{"shoes": 15, "Shirts": 10, "hat": 12}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}` </pre>

启动程序后,在浏览器中输入localhost:8000就可以看到遍历输出后的内容。

image.png

到现在为止,这个服务器只能列出所有的商品,对于每个请求都是如此。一个更加真实的服务器会定义多个不同的URL,每个触发不同的行为。我们把现有的功能URL设为``/list,再加上另外一个/price,用来显示单个商品的价格,商品可以在请求参数中指定/price?item=socks`

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package main

import (
"fmt"
"log"
"net/http"
)

type doolars float32

func (d doolars) String() string {
return fmt.Sprintf("$%.2f", d)
}

type database map[string]doolars

func (db database) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.URL.Path {
case "/list":
for item, price := range db {
fmt.Fprintf(w, "%s: %s\n", item, price)
}
case "/price":
item := req.URL.Query().Get("item")
price, ok := db[item]
if !ok {
w.WriteHeader(http.StatusNotFound) //404
fmt.Fprintf(w, "no such item:%q\n", item)
return
}
fmt.Fprintf(w, "%s\n", price)
default:
w.WriteHeader(http.StatusNotFound) //404
fmt.Fprintf(w,"no such page: %s\n", req.URL)
}

}

func main() {
db := database{"shoes": 15, "Shirts": 10, "hat": 12}
log.Fatal(http.ListenAndServe("localhost:8000", db))
}` </pre>

启动程序后,请求示例如下图:

image.png
image.png
image.png

error接口

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">type error interface{ Error() string } </pre>

构造error最简单的方法就是调用error.New,它会返回一个包含指定的错误消息的心error实例。完整的error只包含四行代码:

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package error

func New(text string) error{return &errorString{text}}

type errorString struct{text string}

func (e *errorString) Error() string{return e.text}` </pre>

底层的errorString类型是一个结构,而没有直接用字符串,主要是为了避免将来无意间的(或者有预谋的)布局变更。满足error接口的是*errorString指针,而不是原始的errorString,主要是为了让每次New分配的error实例互不相等。

直接调用error.New比较罕见,因为有一个更易用的封装函数fmt.Errorf,它还额外提供了字符串格式化功能。

<pre class="custom" data-tool="mdnice编辑器" style="margin-top: 10px; margin-bottom: 10px; border-radius: 5px; box-shadow: rgba(0, 0, 0, 0.55) 0px 2px 10px;">`package fmt

import "error"

func Errorf(format string, args ...interface{}) error{
return error.New(Sprintf(format,args...))
}` </pre>

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

推荐阅读更多精彩内容