你不知道的 Go 之 string

簡(jiǎn)介

字符串(string)是 Go 語(yǔ)言提供的一種基礎(chǔ)數(shù)據(jù)類(lèi)型蜓陌。在編程開(kāi)發(fā)中幾乎隨時(shí)都會(huì)使用泰讽。本文介紹字符串相關(guān)的知識(shí)算凿,幫助你更好地理解和使用它囱井。

底層結(jié)構(gòu)

字符串底層結(jié)構(gòu)定義在源碼runtime包下的 string.go 文件中:

// src/runtime/string.go
type stringStruct struct {
  str unsafe.Pointer
  len int
}
  • str:一個(gè)指針,指向存儲(chǔ)實(shí)際字符串的內(nèi)存地址六剥。
  • len:字符串的長(zhǎng)度晚顷。與切片類(lèi)似,在代碼中我們可以使用len()函數(shù)獲取這個(gè)值疗疟。注意该默,len存儲(chǔ)實(shí)際的字節(jié)數(shù),而非字符數(shù)策彤。所以對(duì)于非單字節(jié)編碼的字符栓袖,結(jié)果可能讓人疑惑。后面會(huì)詳細(xì)介紹多字節(jié)字符店诗。

對(duì)于字符串Hello裹刮,實(shí)際底層結(jié)構(gòu)如下:

[圖片上傳失敗...(image-4adbce-1622475656721)]

str中存儲(chǔ)的是字符對(duì)應(yīng)的編碼,H對(duì)應(yīng)編碼72庞瘸,e對(duì)應(yīng)101等等捧弃。

我們可以使用下面的代碼輸出字符串的底層結(jié)構(gòu)和存儲(chǔ)的每個(gè)字節(jié):

package main

import (
  "fmt"
  "unsafe"
)

type stringStruct struct {
  str unsafe.Pointer
  len int
}

func main() {
  s := "Hello World!"
  fmt.Println(*(*stringStruct)(unsafe.Pointer(&s)))

  for _, b := range s {
    fmt.Println(b)
  }
}

運(yùn)行輸出:

{0x8edaff 5}

由于runtime.stringStruct結(jié)構(gòu)是非導(dǎo)出的,我們不能直接使用恕洲。所以我在代碼中手動(dòng)定義了一個(gè)stringStruct結(jié)構(gòu)體塔橡,字段與runtime.stringStruct完全相同梅割。

基本操作

創(chuàng)建

創(chuàng)建字符串有兩種基本方式霜第,使用var定義和字符串字面量:

var s1 string
s2 := "Hello World!"

注意var s string定義了一個(gè)字符串的空值,字符串的空值是空字符串户辞,即""泌类。字符串不可能為nil

字符串字面量可以使用雙引號(hào)反引號(hào)定義。在雙引號(hào)中出現(xiàn)的特殊字符需要進(jìn)行轉(zhuǎn)義刃榨,而在單引號(hào)中不需要:

s1 := "Hello \nWorld"
s2 := `Hello
World`

上面代碼中弹砚,s1中出現(xiàn)的換行符需要使用轉(zhuǎn)義字符\ns2中直接鍵入換行枢希。由于單引號(hào)定義的字面量與我們?cè)诖a中看到的完全相同桌吃,在包含大段文本(通常有換行)或比較多的特殊字符時(shí)經(jīng)常使用。另外使用單引號(hào)時(shí)苞轿,注意首行后面其他行的空格問(wèn)題:

package main

import "fmt"

func main() {
  s := `hello
  world`

  fmt.Println(s)
}

可能只是為了縮進(jìn)和美觀茅诱,在第二行的 "world" 前面加上了兩個(gè)空格。實(shí)際上這些空格也是字符串的一部分搬卒。如果這不是有意為之瑟俭,可能會(huì)造成一些困惑。上面代碼輸出:

hello
  world

索引和切片

可以使用索引獲取字符串對(duì)應(yīng)位置上存儲(chǔ)的字節(jié)值契邀,使用切片操作符獲取字符串的一個(gè)子串:

package main

import "fmt"

func main() {
  s := "Hello World!"
  fmt.Println(s[0])

  fmt.Println(s[:5])
}

輸出:

72
Hello

上篇文章你不知道的 Go 之 slice中也介紹過(guò)了摆寄,字符串的切片操作返回的不是切片,而是字符串坯门。

字符串拼接

字符串拼接最簡(jiǎn)單直白的方式就是使用+符號(hào)微饥,+可以拼接任意多個(gè)字符串。但是+的缺點(diǎn)是待拼接的字符串必須是已知的田盈。另一種方式就是使用標(biāo)準(zhǔn)庫(kù)strings包中的Join()函數(shù)畜号,這個(gè)函數(shù)接受一個(gè)字符串切片和一個(gè)分隔符,將切片中的元素拼接成以分隔符分隔的單個(gè)字符串:

func main() {
  s1 := "Hello" + " " + "World"
  fmt.Println(s1)

  ss := []string{"Hello", "World"}
  fmt.Println(strings.Join(ss, " "))
}

上面代碼首先使用+拼接字符串允瞧,然后將各個(gè)字符串存放在一個(gè)切片中简软,使用strings.Join()函數(shù)拼接。結(jié)果是一樣的述暂。需要注意的是痹升,將待拼接的字符串放在一行中,使用+拼接畦韭,在 Go 語(yǔ)言?xún)?nèi)部會(huì)先計(jì)算需要的空間疼蛾,預(yù)先分配這個(gè)空間,最后將各個(gè)字符串拷貝過(guò)去艺配。這個(gè)行為與其他很多語(yǔ)言是不同的察郁,所以在 Go 語(yǔ)言中使用+拼接字符串不會(huì)有性能損失,甚至由于內(nèi)部?jī)?yōu)化比其他方式性能還要更好一些转唉。當(dāng)然前提拼接是一次完成的皮钠。下面代碼多次使用+拼接,會(huì)產(chǎn)生大量臨時(shí)字符串對(duì)象赠法,影響性能:

s := "hello"
var result string
for i := 1; i < 100; i++ {
  result += s
}

我們來(lái)測(cè)試一下各種方式的性能差異麦轰。首先定義 3 個(gè)函數(shù),分別用 1 次+拼接,多次+拼接和Join()拼接:

func ConcatWithMultiPlus() {
  var s string
  for i := 0; i < 10; i++ {
    s += "hello"
  }
}

func ConcatWithOnePlus() {
  s1 := "hello"
  s2 := "hello"
  s3 := "hello"
  s4 := "hello"
  s5 := "hello"
  s6 := "hello"
  s7 := "hello"
  s8 := "hello"
  s9 := "hello"
  s10 := "hello"
  s := s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10
  _ = s
}

func ConcatWithJoin() {
  s := []string{"hello", "hello", "hello", "hello", "hello", "hello", "hello", "hello", "hello", "hello"}
  _ = strings.Join(s, "")
}

然后在文件benchmark_test.go中定義基準(zhǔn)測(cè)試:

func BenchmarkConcatWithOnePlus(b *testing.B) {
  for i := 0; i < b.N; i++ {
    ConcatWithOnePlus()
  }
}

func BenchmarkConcatWithMultiPlus(b *testing.B) {
  for i := 0; i < b.N; i++ {
    ConcatWithMultiPlus()
  }
}

func BenchmarkConcatWithJoin(b *testing.B) {
  for i := 0; i < b.N; i++ {
    ConcatWithJoin()
  }
}

運(yùn)行測(cè)試:

$ go test -bench .
BenchmarkConcatWithOnePlus-8            11884388               170.5 ns/op
BenchmarkConcatWithMultiPlus-8           1227411              1006 ns/op
BenchmarkConcatWithJoin-8                6718507               157.5 ns/op

可以看到款侵,使用+一次拼接和Join()函數(shù)性能差不多末荐,而多次+拼接性能是其他兩種方式的近 1/9。另外需要注意我在ConcatWithOnePlus()函數(shù)中先定義 10 個(gè)字符串變量新锈,然后再使用+拼接甲脏。如果直接使用+拼接字符串字面量,編譯器會(huì)直接優(yōu)化為一個(gè)字符串字面量妹笆,結(jié)果就沒(méi)有可比較性了剃幌。

runtime包中,使用concatstrings()函數(shù)來(lái)處理使用+拼接字符串的操作:

// src/runtime/string.go
func concatstrings(buf *tmpBuf, a []string) string {
  idx := 0
  l := 0
  count := 0
  for i, x := range a {
    n := len(x)
    if n == 0 {
      continue
    }
    if l+n < l {
      throw("string concatenation too long")
    }
    l += n
    count++
    idx = i
  }
  if count == 0 {
    return ""
  }

  // If there is just one string and either it is not on the stack
  // or our result does not escape the calling frame (buf != nil),
  // then we can return that string directly.
  if count == 1 && (buf != nil || !stringDataOnStack(a[idx])) {
    return a[idx]
  }
  s, b := rawstringtmp(buf, l)
  for _, x := range a {
    copy(b, x)
    b = b[len(x):]
  }
  return s
}

類(lèi)型轉(zhuǎn)換

我們經(jīng)常需要將 string 轉(zhuǎn)為 []byte晾浴,或者從 []byte 轉(zhuǎn)換回 string负乡。這中間都會(huì)涉及一次內(nèi)存拷貝,所以要注意轉(zhuǎn)換頻次不宜過(guò)高脊凰。string 轉(zhuǎn)換為 []byte抖棘,轉(zhuǎn)換語(yǔ)法為[]byte(str)。首先創(chuàng)建一個(gè)[]byte并分配足夠的空間狸涌,然后將 string 內(nèi)容拷貝過(guò)去切省。

[圖片上傳失敗...(image-cda838-1622475656721)]

func main() {
  s := "Hello"

  b := []byte(s)
  fmt.Println(len(b), cap(b))
}

注意,輸出的cap可能與len不同帕胆,多出的容量處于對(duì)后續(xù)追加的性能考慮朝捆。

[]byte轉(zhuǎn)換為 string 轉(zhuǎn)換語(yǔ)法為string(bs),過(guò)程也是類(lèi)似懒豹。

你不知道的 string

1 編碼

在計(jì)算機(jī)發(fā)展早期芙盘,只有單字節(jié)編碼,最知名的是 ASCII(American Standard Code for Information Interchange脸秽,美國(guó)信息交換標(biāo)準(zhǔn)代碼)儒老。單字節(jié)編碼最多只能編碼 256 個(gè)字符,這對(duì)英語(yǔ)國(guó)家可能夠用了记餐。但是隨著計(jì)算機(jī)在全世界的普及驮樊,要編碼其他國(guó)家的語(yǔ)言(典型的就是漢字),單字節(jié)顯然是不夠的片酝。為此提出了 Unicode 編碼方案囚衔。Unicode 編碼為全世界所有國(guó)家的語(yǔ)言符號(hào)規(guī)定了統(tǒng)一的編碼方案。Unicode 相關(guān)的知識(shí)請(qǐng)查看參考鏈接每個(gè)程序員都必須知道的 Unicode 知識(shí)雕沿。

有很多人不知道 Unicode 與 UTF8练湿、UTF16、UTF32 這些有什么關(guān)系晦炊。實(shí)際上可以理解為 Unicode 只是規(guī)定了每個(gè)字符對(duì)應(yīng)的編碼值鞠鲜,實(shí)際很少直接存儲(chǔ)和傳輸這個(gè)值。UTF8/UTF16/UTF32 則定義這些編碼值如何在內(nèi)存或文件中存儲(chǔ)以及在網(wǎng)絡(luò)上傳輸?shù)母袷蕉瞎@缦湍罚瑵h字“中”,Unicode 編碼值為00004E2D稳衬,其他編碼如下:

UTF8編碼:E4B8AD
UTF16BE編碼:FEFF4E2D
UTF16LE編碼:FFFE2D4E
UTF32BE編碼:0000FEFF00004E2D
UTF32LE編碼:FFFE00002D4E0000

Go 語(yǔ)言中的字符串存儲(chǔ)是 UTF-8 編碼霞捡。UTF8 是可變長(zhǎng)編碼,優(yōu)點(diǎn)是兼容 ASCII薄疚。對(duì)非英語(yǔ)國(guó)家的字符采用多字節(jié)編碼方案碧信,而且對(duì)使用比較頻繁的字符采用較短的編碼,提升編碼效率街夭。缺點(diǎn)是 UTF8 的可變長(zhǎng)編碼讓我們不能直接砰碴、直觀地確定字符串的字符長(zhǎng)度。一般的中文字符使用 3 個(gè)字節(jié)來(lái)編碼板丽,例如上面的“中”呈枉。對(duì)于生僻字,可能采用更多的字節(jié)來(lái)編碼埃碱,例如“魋”的 UTF-8 編碼為E9AD8B20猖辫。

我們使用len()函數(shù)獲取到的都是編碼后的字節(jié)長(zhǎng)度,而非字符長(zhǎng)度砚殿,這一點(diǎn)在使用非 ASCII 字符時(shí)很重要:

func main() {
  s1 := "Hello World!"
  s2 := "你好啃憎,中國(guó)"

  fmt.Println(len(s1))
  fmt.Println(len(s2))
}

輸出:

12
15

Hello World!有 12 個(gè)字符很好理解,你好似炎,中國(guó)有 5 個(gè)中文字符辛萍,每個(gè)中文字符占 3 個(gè)字節(jié),所以輸出 15羡藐。

對(duì)于使用非 ASCII 字符的字符串叹阔,我們可以使用標(biāo)準(zhǔn)庫(kù)的 unicode/utf8 包中的RuneCountInString()方法獲取實(shí)際字符數(shù):

func main() {
  s1 := "Hello World!"
  s2 := "你好,中國(guó)"

  fmt.Println(utf8.RuneCountInString(s1)) // 12
  fmt.Println(utf8.RuneCountInString(s2)) // 5
}

為了便于理解传睹,下面給出字符串“中國(guó)”的底層結(jié)構(gòu)圖:

[圖片上傳失敗...(image-f3fff8-1622475656721)]

2 索引和遍歷

使用索引操作字符串耳幢,獲取的是對(duì)應(yīng)位置上的字節(jié)值,如果該位置是某個(gè)多字節(jié)編碼的中間位置欧啤,可能返回的字節(jié)值不是一個(gè)合法的編碼值:

s := "中國(guó)"
fmt.Println(s[0])

前面介紹過(guò)“中”的 UTF8 編碼為E4B8AD睛藻,故s[0]取第一個(gè)字節(jié)值,結(jié)果為 228(十六進(jìn)制 E4 的值)邢隧。

為了方便地遍歷字符串店印,Go 語(yǔ)言中for-range循環(huán)對(duì)多字符編碼有特殊的支持。每次遍歷返回的索引是每個(gè)字符開(kāi)始的字節(jié)位置倒慧,值為該字符的編碼值:

func main() {
  s := "Go 語(yǔ)言"

  for index, c := range s {
    fmt.Println(index, c)
  }
}

所以遇到多字節(jié)字符按摘,索引就不是連續(xù)的包券。上面“語(yǔ)”占用 3 個(gè)字節(jié),所以“言”的索引就是“中”的索引 3 加上它的字節(jié)數(shù) 3炫贤,結(jié)果就是 6溅固。上面的代碼輸出如下:

0 71
1 111
2 32
3 35821
6 35328

我們也可以以字符形式輸出:

func main() {
  s := "Go 語(yǔ)言"

  for index, c := range s {
    fmt.Printf("%d %c\n", index, c)
  }
}

輸出:

0 G
1 o
2 
3 語(yǔ)
6 言

按照這個(gè)方法,我們可以編寫(xiě)一個(gè)簡(jiǎn)單的RuneCountInString()函數(shù)兰珍,就叫做Utf8Count吧:

func Utf8Count(s string) int {
  var count int
  for range s {
    count++
  }
  return count
}

fmt.Println(Utf8Count("中國(guó)")) // 2

3 亂碼和不可打印字符

如果 string 中出現(xiàn)不合法的 utf8 編碼侍郭,打印時(shí)對(duì)于每個(gè)不合法的編碼字節(jié)都會(huì)輸出一個(gè)特定的符號(hào)?

func main() {
  s := "中國(guó)"
  fmt.Println(s[:5])

  b := []byte{129, 130, 131}
  fmt.Println(string(b))
}

上面輸出:

中??
???

因?yàn)椤皣?guó)”編碼有 3 個(gè)字節(jié),s[:5]只取了前兩個(gè)掠河,這兩個(gè)字節(jié)無(wú)法組成一個(gè)合法的 UTF8 字符亮元,故輸出兩個(gè)?

另外需要警惕不可打印字符唠摹,之前有個(gè)同事請(qǐng)教我一個(gè)問(wèn)題爆捞,兩個(gè)字符串輸出的內(nèi)容相同,但是它們就是不相等:

func main() {
  b1 := []byte{0xEF, 0xBB, 0xBF, 72, 101, 108, 108, 111}
  b2 := []byte{72, 101, 108, 108, 111}

  s1 := string(b1)
  s2 := string(b2)

  fmt.Println(s1)
  fmt.Println(s2)
  fmt.Println(s1 == s2)
}

輸出:

hello
hello
false

我直接把字符串內(nèi)部字節(jié)寫(xiě)出來(lái)了勾拉,可能一眼就看出來(lái)了嵌削。但是我們當(dāng)時(shí)遇到這個(gè)問(wèn)題還是稍微費(fèi)了一番功夫來(lái)調(diào)試的。因?yàn)楫?dāng)時(shí)字符串是從文件中讀取的望艺,而文件采用的是帶 BOM 的 UTF8 編碼格式苛秕。我們都知道 BOM 格式會(huì)自動(dòng)在文件頭部加上 3 個(gè)字節(jié)0xEFBBBF。而字符串比較是會(huì)比較長(zhǎng)度和每個(gè)字節(jié)的找默。讓問(wèn)題更難調(diào)試的是艇劫,在文件中 BOM 頭也是不顯示的。

4 編譯優(yōu)化

[]byte轉(zhuǎn)換為 string 的場(chǎng)景很多惩激,處于性能上的考慮店煞。如果轉(zhuǎn)換后的 string 只是臨時(shí)使用,這時(shí)轉(zhuǎn)換并不會(huì)進(jìn)行內(nèi)存拷貝风钻。返回的 string會(huì)指向切片的內(nèi)存顷蟀。編譯器會(huì)識(shí)別如下場(chǎng)景:

  • map 查找:m[string(b)]
  • 字符串拼接:"<" + string(b) + ">"骡技;
  • 字符串比較:string(b) == "foo"鸣个。

因?yàn)?string 只是臨時(shí)使用,期間切片不會(huì)發(fā)生變化布朦。故這樣使用沒(méi)有問(wèn)題囤萤。

總結(jié)

字符串是使用頻率最高的基本類(lèi)型之一,熟悉掌握它可以幫助我們更好地編碼和解決問(wèn)題是趴。

參考

  1. 《Go 專(zhuān)家編程》
  2. 每個(gè)程序員都必須知道的 Unicode 知識(shí)涛舍,https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/
  3. 你不知道的Go GitHub:https://github.com/darjun/you-dont-know-go

我的博客:https://darjun.github.io

歡迎關(guān)注我的微信公眾號(hào)【GoUpUp】,共同學(xué)習(xí)唆途,一起進(jìn)步~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末富雅,一起剝皮案震驚了整個(gè)濱河市掸驱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌没佑,老刑警劉巖毕贼,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異图筹,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)让腹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)远剩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人骇窍,你說(shuō)我怎么就攤上這事瓜晤。” “怎么了腹纳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵痢掠,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我嘲恍,道長(zhǎng)足画,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任佃牛,我火速辦了婚禮淹辞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘俘侠。我一直安慰自己象缀,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布爷速。 她就那樣靜靜地躺著央星,像睡著了一般。 火紅的嫁衣襯著肌膚如雪惫东。 梳的紋絲不亂的頭發(fā)上莉给,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音廉沮,去河邊找鬼禁谦。 笑死,一個(gè)胖子當(dāng)著我的面吹牛废封,可吹牛的內(nèi)容都是我干的州泊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼漂洋,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼遥皂!你這毒婦竟也來(lái)了力喷?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤演训,失蹤者是張志新(化名)和其女友劉穎弟孟,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體样悟,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡拂募,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窟她。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片陈症。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖震糖,靈堂內(nèi)的尸體忽然破棺而出录肯,到底是詐尸還是另有隱情,我是刑警寧澤吊说,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布论咏,位于F島的核電站,受9級(jí)特大地震影響颁井,放射性物質(zhì)發(fā)生泄漏厅贪。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一雅宾、第九天 我趴在偏房一處隱蔽的房頂上張望卦溢。 院中可真熱鬧,春花似錦秀又、人聲如沸单寂。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)宣决。三九已至,卻和暖如春昏苏,著一層夾襖步出監(jiān)牢的瞬間尊沸,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工贤惯, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留洼专,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓孵构,卻偏偏與公主長(zhǎng)得像屁商,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子颈墅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

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