Go语言的数据结构

整理下Go语言提供的数据结构

一、顺序数据结构

1.1. 数组

语言自带。值得一提的是调函数的时候如果传数组是值传递(复制整个数组),这点和java差别很大

func set(arr [3]int){
    arr[0]=453532
}

func setPointer(arr *[3]int){
    arr[0]=453532
}

func Test(t *testing.T) {
    arr:=[...]int{1,2,3}
    set(arr)
    fmt.Println(arr)  // [1 2 3]
    setPointer(&arr)
    fmt.Println(arr)  //[453532 2 3]

}

1.2. 动态数组

可以把slice理解成java的ArrayList
但遗憾的是他没有ArrayList好用。

把slice当ArrayList用存在的坑:

  • slice是个view,调别的函数append修改后,原先slice的内容没变!(见下图)


    image.png

https://blog.csdn.net/weixin_40762352/article/details/79576616

解法:
a.自己封装一个ArrayList
b.调别的函数时,让函数返回slice,类似于a=append(a,1)
c.传slice的指针(自己改成引用传递)

构造slice

http://www.cheat-sheets.org/saved-copy/go-lang-cheat-sheet-master.20181212/golang_refcard.pdf

image.png

image.png

make([]T,len) 不传cap时,cap等于len

  • 零值是nil


    image.png

Q: ans:=make([]bool,n) 默认值?
A:

func Test(t *testing.T) {
    ages:=make([]bool,2)
    AssertTrue(!ages[0],t)
    AssertTrue(!ages[1],t)
}

实现原理:cap是啥? slice的肚子里有什么?

image.png
  • cap是干嘛的?
    扩展的时候,比如s2:=s1[3:5] 不能超过cap


    image.png

    append时候如果cap不够,能动态扩容(自动开个新array,拷贝过去)

cap vs len of slice in golang

image.png

实现原理: slice 怎么扩容

image.png

https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array-and-slice/#325-%E6%8B%B7%E8%B4%9D%E5%88%87%E7%89%87

https://segmentfault.com/a/1190000017341615

https://juejin.cn/post/6844903812331732999#heading-2

  • 有没有缩容设计
    java ArrayList提供了缩容方法,但没有自动缩容机制
    go没缩容机制,也没提供这样的api。如有必要,可以用copy

Go 本身没有切片缩容后,底层数组不会被释放掉。删除次数多了,会占用很多内存。
可以用copy的方法,创建新的切片和底层数组。并把原来的切片置nil。
https://cloud.tencent.com/developer/article/1720270

判断内容相等

可以用reflect.DeepEqual
https://golang.org/pkg/reflect/#DeepEqual

打印slice

fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)

https://www.runoob.com/go/go-slice.html

append

http://shouce.jb51.net/gopl-zh/ch4/ch4-02.html

concatenate two slices

https://stackoverflow.com/questions/16248241/concatenate-two-slices-in-go

append([]int{1,2}, []int{3,4}...)

  • 那要是 append nil... 呢?
    正常,不会panic, 相当于没append:
func main() {
    a := make([]int, 1)
    var b []int
    a = append(a, b...)
    a = append(a, nil...)
    fmt.Println(a)  // [0]
}

copy slice

使用copy函数
https://www.geeksforgeeks.org/how-to-copy-one-slice-into-another-slice-in-golang/
但是注意创建的新slice长度提前要设置好,The builtin copy(dst, src) copies min(len(dst), len(src)) elements.
https://stackoverflow.com/questions/30182538/why-cant-i-duplicate-a-slice-with-copy

arr := []int{1, 2, 3}
tmp := make([]int, len(arr))
copy(tmp, arr)
fmt.Println(tmp)
fmt.Println(arr)

Output (as expected):

[1 2 3]
[1 2 3]

1.3. Queue

1.3.1. 无界队列 (unbounded)

非并发安全

slice就能当queue用。
另外bytes.buffer可以当byte类型的queue,其实内部就是[]byte
https://www.kancloud.cn/digest/batu-go/153538

并发安全的无界队列

chan 是有界队列,go 团队不愿意在标准库里加入无界队列
可以自己封装,思路是在两个chan之间加个中转 slice,单协程操作该slice 避免并发问题。见 https://blog.zeromake.com/pages/ultimete-channel/

1.3.2. 有界 (bounded)

非并发安全

没有给单线程用的,需要自己拿slice封装

并发安全的有界阻塞队列:channel

channel是有界阻塞队列,倒是可以写入的时候判断,以实现有界非阻塞
https://www.coder.work/article/24623

默认是 unbuffered chan, 队列长度0。可以在 make 时设置队列长度


image.png
原理

Q: channel肚子里是啥,内部实现原理?
A:channel是个指针(chan是引用类型),指向的struct runtime.hchan,是个有锁的环形队列
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/

image.png

13 | Channel:另辟蹊径,解决并发问题

理论上可以实现 lock-free queue, 但是 golang 官方的 issue 一直没进展。作为替代,社区有实现 lock-free queue 的库
https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-channel/
https://github.com/golang/go/issues/8899#issuecomment-269321360

特殊用法

Q: channel的所有方法都是阻塞方法,想非阻塞咋办?
A: 用select+default自己封装非阻塞方法:
见effective go p102


image.png

Q: 会 panic 的情况?
总共有 3 种:

  • close 为 nil 的 chan;
  • send 已经 close 的 chan;
  • close 已经 close 的 chan。

Q: 如何用 chan 做全体广播?
a. 自己写个消费者去消费chan,然后广播通知所有订阅者
b. 利用close
https://www.cnblogs.com/faithfu/p/12068414.html

1.4. Deque

非并发安全

go 的slice 不好实现"从左侧 enque", 要找其他数据结构。
java有两种实现,链表和环形队列
Go的list包下有个链表,API很不deque,但是能凑合用
ring包下的环没法动态扩容,API也比较沙雕,没法拿来当deque
https://github.com/polaris1119/The-Golang-Standard-Library-by-Example/blob/master/chapter03/03.3.md

鉴于自带链表的API不好用,自己写了一个:
https://github.com/seeflood/Copy-Paste-Data-Structures/tree/master/src/main/go/io/github/seeflood/copy-paste-ds/deque

并发安全

chan 就是,内部是有锁环形队列

1.5. PriorityQueue

https://books.studygolang.com/The-Golang-Standard-Library-by-Example/chapter03/03.3.html
标准库有,API设计的很不好用,想用得实现一堆接口。想用IntHeap都得自己写(标准库里没有IntHeap);
heap包下面的unit test代码里有个IntHeap,因此一个偷懒的做法是:想用IntHeap的时候可以去heap包下面的unit test代码里把IntHeap复制粘贴到自己的项目

鉴于API过于难用,自己封装了个SliceHeap

1.6. 临时对象池 sync.Pool

sync.Pool 数据类型用来保存一组可独立访问的临时对象。请注意这里加粗的“临时”这两个字,它说明了 sync.Pool 这个数据类型的特点,也就是说,它池化的对象会在未来的某个时候被毫无预兆地移除掉。而且,如果没有别的对象引用这个被移除的对象的话,这个被移除的对象就会被垃圾回收掉。

实现的有问题,有需要还是找三方库:


image.png

详见 https://time.geekbang.org/column/article/301716

  • 连接池
    一般不用 sync.Pool

事实上,我们很少会使用 sync.Pool 去池化连接对象,原因就在于,sync.Pool 会无通知地在某个时候就把连接移除垃圾回收掉了,而我们的场景是需要长久保持这个连接,所以,我们一般会使用其它方法来池化连接

可以自己用队列实现(用 channel 或者 slice),或者用三方库

  • 协程池 worker pool
    标准库没有,要用三方库

二、Map

2.1. hashmap

自带的map就是
http://shouce.jb51.net/gopl-zh/ch4/ch4-03.html

2.1.1. 创建

Q: 用var a map[xxx]xxx这样声明时,会创建新的map吗?
实测不会,只生成nil;别的写法都会创建新map


func Test(t *testing.T) {
    var ages map[string]int

    if ages == nil {
        fmt.Println("var map[string]int==nil")
    }

    ages = make(map[string]int)
    if ages == nil {
        fmt.Println("make(map[string]int)==nil")
    }


    ages=map[string]int{}
    if ages == nil {
        fmt.Println("map[string]int{}==nil")
    }

}

打印结果:
var map[string]int==nil

https://books.studygolang.com/gopl-zh/ch4/ch4-03.html

  • nil的map(没初始化的map)能读能删不能写


    image.png

    slice用惯了可能会懒得写初始化,直接

    var s []int
    s = append(s, 1)

这样反正append能写nil slice。但是map必须先初始化才能写!

2.1.2. get与containsKey

age, ok := ages["bob"]
if !ok { /* "bob" is not a key in this map; age == 0. */ }

不加第二个变量ok也不会panic:


image.png

2.1.3. 遍历

for range

m := map[string]int{
    "hello": 100,
    "world": 200,
}
for key, value := range m {
    fmt.Println(key, value)
}
Q: 如果 map 是 nil, 会 panic 么

不会

package main

import "fmt"

func main() {
    var m map[string]string
    for k, _ := range m {
        fmt.Printf("k :%v\n",k)
    }
    fmt.Println("end")
}
Q: 能否在for range的过程中添加/删除元素?

A:
https://stackoverflow.com/questions/23229975/is-it-safe-to-remove-selected-keys-from-map-within-a-range-loop
删除可以,删掉的不会被遍历到;添加虽然可以,但是加进去的可能会在当次循环被遍历到、也可能不会,有不确定性。

2.1.4. 判断两个map相等

How to test the equivalence of maps in Golang?
https://www.geeksforgeeks.org/comparing-maps-in-golang/

reflect.DeepEqual(a, b)

2.1.5. golang中如何判断两个struct相等

https://www.geeksforgeeks.org/structure-equality-in-golang/

==和DeeplyEqual()都行,肚子里的值一样就算相等,不是java那种比较指向的内存对象相等

golang里的map用==比较两个key相等,因此实际上是比较struct肚子里的值。如果确实想比较两个key是“同一个”,可以用指针当key
https://stackoverflow.com/questions/29748003/map-with-function-pointer-as-key-in-go

2.1.6. 能否自定义map中元素判等的逻辑?

不能,map用==判断key相等的
https://juejin.cn/post/6844903923166232589#heading-8

2.1.7. 想用<key1,key2,value>式的双key map?

https://stackoverflow.com/questions/30529970/golang-map-with-multiple-keys-per-value
key放struct里就行

image.png

缺点是:如果修改 struct 肚子里的值,就没法再从 map 里取到原来对应的 value 了。


type mapKey struct {
    key int
}

func main() {
    var m = make(map[mapKey]string)
    var key = mapKey{10}


    m[key] = "hello"
    fmt.Printf("m[key]=%s\n", m[key])


    // 修改key的字段的值后再次查询map,无法获取刚才add进去的值
    key.key = 100
    fmt.Printf("再次查询m[key]=%s\n", m[key])
}

2.1.8. 内部实现

golang中的map是一个 指针,指向的struct是个哈希表
https://blog.csdn.net/K346K346/article/details/109559718
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
https://zjykzk.github.io/post/cs/golang/map/
https://guidao.github.io/go_map.html

Q: 如何处理哈希碰撞
用拉链法来解决哈希碰撞的问题。哈希表的每个桶都只能存储 8 个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中。
(个人理解,本质上是分块链表?)


image.png

Q: 如何扩容
随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍
具体来说,在以下两种情况发生时触发哈希的扩容:

  • 装载因子已经超过 6.5;
  • 哈希使用了太多溢出桶;

2.2. ordered map (例如 TreeMap/SkipListMap 这样的平衡搜索树类的数据结构)

搜了下好像标准库没有
社区有

2.3. LRUCache

没有
自己写了一个:
https://github.com/seeflood/Copy-Paste-Data-Structures/tree/master/src/main/go/io/github/seeflood/copy-paste-ds/map

2.4. ConcurrentHashMap

a. 通过改变线程模型,让单线程操作map

b. 用读写锁自己封装一个

c. 用第三方的实现,分片加锁

比如 https://github.com/orcaman/concurrent-map

d. 应对特殊场景的 sync.Map

不常用,特殊场景会比 map+RWMutex 的方式性能好;缺点是会丢失类型信息,方法传参、返回结果都是 interface{}
https://www.jianshu.com/p/a2831dfa0a91
https://studygolang.com/articles/18070
https://colobu.com/2017/07/11/dive-into-sync-Map/

image.png
  • 内部原理


    image.png

俩 map,有一个仅用于读的 read map,相当于读缓存
read map 有性能优势是因为 value 是个 holder,holder 里能基于 cas 修改value
新增 kv 会写进 dirty map (会加互斥锁,不是读写锁),达到一定条件( 缓存 miss 率太高、达到一定的值)后,会将dirty数据提升为read

https://tonybai.com/2020/11/10/understand-sync-map-inside-through-examples/

https://time.geekbang.org/column/article/301174

2.5. 其他并发安全的数据结构

除了sync.Map和sync.Pool,GO就没啥并发安全数据结构了。见讨论:
https://groups.google.com/g/golang-china/c/JdbR_CGo3ao/m/Uct3hj2wKCsJ

三、Set

3.1. hashset

没有,只能拿map模拟
https://stackoverflow.com/questions/34018908/golang-why-dont-we-have-a-set-datastructure

实现时注意,map[interface{}]struct{}比map[interface{}]bool好在空结构体不占用空间:
https://geektutu.com/post/hpg-empty-struct.html

自己轮了一个
https://github.com/seeflood/Copy-Paste-Data-Structures/tree/master/src/main/go/io/github/seeflood/copy-paste-ds/set

3.2. bitset/bitmap

标准库没有。开源的有个github.com/willf/bitset
https://blog.cyeam.com/golang/2017/01/18/go-optimize-bitset

我也实现了个
https://github.com/seeflood/Copy-Paste-Data-Structures/blob/master/src/main/go/io/github/seeflood/copy-paste-ds/set/BitSet.go
实现方法参考https://books.studygolang.com/gopl-zh/ch6/ch6-05.html

另外,分块bitmap(Roaring bitmap)有个开源的go实现
https://github.com/RoaringBitmap/roaring

四、字符串相关

4.1. string

http://shouce.jb51.net/gopl-zh/ch3/ch3-05.html
https://blog.golang.org/strings

4.1.1. 默认是按字节访问

In Go, a string is in effect a read-only slice of bytes
也就是说string是只读的[]byte(也就是说string不可变),虽然你的字符串可能是UTF-8编码,但是语言提供的很多API(比如len(string)、随机访问字符s[1]等)不管你啥编码,统一按字节访问字符串……
那想按字符访问字符串怎么办?

求长度

关于求字符串长度,替代len(string)可以用utf8.RuneCountInString(s),但是这API每次调用得遍历解析一遍计算长度,时间(N)。java的String.length()是返回char[].length,时间O(1)

按字符访问

关于按字符访问,for range是按字符访问字符串,但他是顺序访问,不支持随机访问


image.png

想随机访问?得把字符串转[]rune:

r := []rune(s)

但是你会发现,转成[]rune后,strings包下面的API都用不了了!
这设计,真想骂娘!!!

注1:strings包下的API只处理UTF-8编码的string。
注2:Go语言源文件总是用UTF8编码,并且Go语言的文本字符串也以UTF8编码的方式处理
注3:UTF-8编码中,没有任何字符的编码是其它字符编码的子串,或是其它编码序列的子串,因此搜索一个字符时只要搜索它的字节编码序列即可,不用担心前后的上下文会对搜索结果产生干扰(说人话就是:做字符串匹配的时候,无脑用字节匹配就行,见下图)


image.png

4.1.2. 判断

判空

string的零值是"",也没法赋值nil给string变量
https://tour.golang.org/basics/12

所以len(s)==0 或者s==""都可以拿来判空
https://stackoverflow.com/questions/18594330/what-is-the-best-way-to-test-for-an-empty-string-in-go

判断相等

就用==就行。不区分大小写用EqualFold
https://blog.csdn.net/oqqYuan1234567890/article/details/59110219

4.1.3. 类型转换

[]rune []byte和string互相转换

https://blog.csdn.net/dengming0922/article/details/80883574

https://stackoverflow.com/questions/29255746/how-encode-rune-into-byte-using-utf8

string和数字转换

一种方法是用fmt.Sprintf返回一个格式化的字符串;
另一个方法是用strconv包的 strconv.Itoa. lintcode不让用这个,leetcode 可以。
http://shouce.jb51.net/gopl-zh/ch3/ch3-05.html

x := 123
y := fmt.Sprintf("%d", x)
fmt.Println(y, strconv.Itoa(x)) // "123 123"

https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.7.md#4712-%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%8E%E5%85%B6%E5%AE%83%E7%B1%BB%E5%9E%8B%E7%9A%84%E8%BD%AC%E6%8D%A2

image.png
字符和数字互相转换?

如果是 0-9 这样的“单字符数字”(我自己起的名字),可以这样转:

    b := 1 + '0' // int32
    fmt.Printf("Size: %d\nType: %s\nCharacter: %c\n", unsafe.Sizeof(b), reflect.TypeOf(b), b)

    rawNum := b - '0' // int32
    fmt.Println(rawNum)

如果是“多字符数字”,就得当做字符串和数字做转换

Convert interface/any data structure to string

https://yourbasic.org/golang/interface-to-string/
Use fmt.Sprintf

var x interface{} = "abc"
str := fmt.Sprintf("%v", x)

另见《fmt.Sprintf vs string(data) vs String()》

4.1.4. 改

字符串的值是不可变的:一个字符串包含的字节序列永远不会被改变,当然我们也可以给一个字符串变量分配一个新字符串值。可以像下面这样将一个字符串追加到另一个字符串:

s := "left foot"
t := s
s += ", right foot"
这并不会导致原始的字符串值被改变,但是变量s将因为+=语句持有一个新的字符串值,但是t依然是包含原先的字符串值。

fmt.Println(s) // "left foot, right foot"
fmt.Println(t) // "left foot"
因为字符串是不可修改的,因此尝试修改字符串内部数据的操作也是被禁止的:

s[0] = 'L' // compile error: cannot assign to s[0]

拼接

java有StringBuilder这样的字符串专用stack,相应的go里可以用的动态数组有:

  • []byte,通过append拼接
  • bytes.Buffer,通过buffer.WriteString()往里面写字符串
    其实bytes.Buffer内部就是[]byte
  • strings.Builder

Examples &选型benchmark:
https://mojotv.cn/go/golang-most-efficient-string-join
https://geektutu.com/post/hpg-string-concat.html#1-2-benchmark-%E6%80%A7%E8%83%BD%E6%AF%94%E6%8B%BC
从benchmark看来,有preallocate的性能好,没有的话这三个差别不大

ref:
https://stackoverflow.com/questions/1760757/how-to-efficiently-concatenate-strings-in-go/47798475#47798475
https://www.jianshu.com/p/df92c0ee6cc8

随机访问、修改
只改其中一两个字符

https://stackoverflow.com/questions/37688457/how-to-replace-nth-char-from-a-string-in-go

方案1. reslice

chars = chars[:3] + "z" + chars[4:]

方案2. 从一开始就用[]byte,而不是string

func main() {
  var chars = []byte{'a', 'b', 'c', 'd', 'e', 'f'}
  fmt.Println(string(chars[3]))
  fmt.Printf("%T\n", chars)
  chars[3] = 'z'
  fmt.Println(string(chars))
}

方案3. 拷贝成别的数据结构,然后修改

func replaceAt(s string, i int, c rune) string {
    r := []rune(s)
    r[i] = c
    return string(r)
}
替换

strings包下面的替换不支持正则
https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.7.md#474-%E5%AD%97%E7%AC%A6%E4%B8%B2%E6%9B%BF%E6%8D%A2

想用正则替换得用regexp包
https://www.geeksforgeeks.org/golang-replacing-all-string-which-matches-with-regular-expression/

写正则表达式的时候用...比较方便(字符串面值),使用反引号代替双引号。在原生的字符串面值中,没有转义操作

其他操作

就用strings 和 strconv 包
https://github.com/unknwon/the-way-to-go_ZH_CN/blob/master/eBook/04.7.md

4.1.5.字符

java用char通吃,但go设计的比较烂,需要理解byte,uint8,int32以及rune.

https://golangbyexample.com/character-in-go/
http://c.biancheng.net/view/18.html
Go语言的字符有以下两种:

  • 一种是 uint8 类型,byte 类型是 uint8 的别名,代表了 ASCII 码的一个字符。
  • 另一种是 rune 类型,代表一个 UTF-8 字符,当需要处理中文、日文或者其他复合字符时,则需要用到 rune 类型。rune 类型是int32的别名。
声明字符变量

To declare either a byte or a rune we use single quotes. While declaring byte we have to specify the type, If we don’t specify the type, then the default type is meant as a rune.

To declare a string, we use double quotes or backquotes. Double quotes string honors escape character while back quotes string is a raw literal string and doesn’t honor any kind of escaping.

这些字符都是啥类型?

s := "hello, world"
c:=s[0] // c is uint8
cnum := c - '0' //uint8
bytes := []byte(s)
x:=bytes[0] //x is uint8

test:='0' //int32.'0'是untyped constant
cnum32:=test-'0' //int32.'0'是untyped constant

直接访问的话,得到的字符是uint8.byte是uint8的别名

r, size := utf8.DecodeRuneInString(s[1:]) // r is int32
for i, w := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, w, w)
}
// w is int32

utf8解码后是int32.for range自动解码。

As mentioned above, integers come in a couple of forms and each form has its own default type: int for simple constants like 123 or 0xFF or -14 and rune for quoted characters like 'a', '世' or '\r'.
https://blog.golang.org/constants#TOC_10.

国际化字符rune

rune是int32的别名,但是使用起来注意有区别,比如for range 字符串时,每个中文字符算3个长度,而for range []rune(字符串) 每个中文字符算一个长度:

for i, r := range "Hello, 世界" {
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
//...
//7 '世' 19990
//10 '界' 30028
for i, r := range []rune("Hello, 世界" ){
fmt.Printf("%d\t%q\t%d\n", i, r, r)
}
//...
//7 '世' 19990
//8 '界' 30028

4.2. Suffix Array

第一次看到有语言把后缀数组放进了标准库,关键放就放了,这个API设计的就像个玩具项目:
构造方法居然只支持New([]byte) 不支持string不支持 []rune等;
后缀数组理论上能做的事情很多,但是API只提供了个Lookup,同样只支持[]byte
玩具项目都没这么懒……
https://www.kancloud.cn/yetship/golang_standard_library_samples/527457

五、树相关,图相关

啥都没有

其他数据结构?用三方库吧!

其他的基本都没有,我写了一些放到自己的Copy-Paste Data Structures 项目里了
https://github.com/seeflood/Copy-Paste-Data-Structures

比较有名的三方数据结构库:
https://github.com/Workiva/go-datastructures

Q:比较有名的concurrent data structure库?
A:没有。
orcaman/concurrent-map这个连release都没有,pr也很久没合并过了,看着比较练手。

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

推荐阅读更多精彩内容