Golang IO

io包中最重要的是兩個接口:Reader和Writer

Reader接口#####

type Reader interface {
    Read(p []byte) (n int ,err error)
}```
#####官方文檔中關于該接口方法的說明#####
>Read 將 len(p) 個字節讀取到 p 中。它返回讀取的字節數 n(0 <= n <= len(p)) 以及任何遇到的錯誤。即使 Read 返回的 n < len(p),它也會在調用過程中使用 p 的全部作為暫存空間。若一些數據可用但不到 len(p) 個字節,Read 會照例返回可用的數據,而不是等待更多數據。

> Read 在成功讀取 n > 0 個字節后遇到一個錯誤或 ```EOF (end-of-file)```,它就會返回讀取的字節數。它會從相同的調用中返回(非nil的)錯誤或從隨后的調用中返回錯誤(同時 n == 0)。 一般情況的一個例子就是 Reader 在輸入流結束時會返回一個非零的字節數,同時返回的 ```err``` 不是 ```EOF``` 就是```nil```。無論如何,下一個 Read 都應當返回 ```0, EOF```。

>調用者應當總在考慮到錯誤 err 前處理 n > 0 的字節。這樣做可以在讀取一些字節,以及允許的 EOF 行為后正確地處理 I/O 錯誤

*PS: 當```Read```方法返回錯誤時,不代表沒有讀取到任何數據,可能是數據被讀完了時返回的```io.EOF```。* 

 Reader 接口的方法集([Method_sets](http://golang.org/ref/spec#Method_sets))只包含一個 Read 方法,因此,所有實現了 ```Read``` 方法的類型都實現了```io.Reader ```接口,也就是說,在所有需要``` io.Reader``` 的地方,可以傳遞實現了 ````Read()``` 方法的類型的實例。

#####Writer 接口#####
---

type Writer interface {
Write(p []byte) (n int, err error)
}```

官方文檔中關于該接口方法的說明:#####

Write 將 len(p) 個字節從 p 中寫入到基本數據流中。它返回從 p 中被寫入的字節數 n(0 <= n <= len(p))以及任何遇到的引起寫入提前停止的錯誤。若 Write 返回的 n < len(p),它就必須返回一個 非nil 的錯誤。

io.File#####

os.File同時實現了io.Readerio.Writer接口。
在os包中有三個可倒出的特殊文件(os.File)實例:StdinStdoutStderr,自然也實現了 io.Readerio.Writer.

實現了 io.Reader 或 io.Writer 接口的類型#####
  • os.File 同時實現了 io.Readerio.Writer
  • strings.Reader 實現了 io.Reader
  • bufio.Reader/Writer 分別實現了 io.Readerio.Writer
  • bytes.Buffer 同時實現了 io.Readerio.Writer
  • bytes.Reader 實現了 io.Reader
  • compress/gzip.Reader/Writer 分別實現了 io.Readerio.Writer
  • crypto/cipher.StreamReader/StreamWriter 分別實現了 io.Readerio.Writer
  • crypto/tls.Conn 同時實現了 io.Readerio.Writer
  • encoding/csv.Reader/Writer 分別實現了 io.Readerio.Writer
  • mime/multipart.Part 實現了 io.Reader
  • io.LimitedReader、io.PipeReader、io.SectionReader實現了io.Reader
  • io.PipeWriter實現了io.Writer

PS: Go接口的命名約定:接口名以 er 結尾。注意,這里并非強行要求,你完全可以不以 er 結尾。標準庫中有些接口也不是以 er 結尾的。

ReaderAt 和 WriterAt 接口#####
ReaderAt 接口######

type ReaderAt interface {
  ReadAt(p []byte,off int64) (n int ,err error)
}```
官方文檔說明
>ReadAt 從幾本輸入源的偏移量off處開始,將len(p)個字節讀取到p 中,它返回讀取的字節數 n(0 <= n <= len(p))以及任何遇到的錯誤。

>當 ReadAt 返回的 n < len(p) 時,它就會返回一個 非nil 的錯誤來解釋 為什么沒有返回更多的字節。在這一點上,ReadAt 比 Read 更嚴格。

>即使 ReadAt 返回的 n < len(p),它也會在調用過程中使用 p 的全部作為暫存空間。若一些數據可用但不到 len(p) 字節,ReadAt 就會阻塞直到所有數據都可用或產生一個錯誤。 在這一點上 ReadAt 不同于 Read。

>若 n = len(p) 個字節在輸入源的的結尾處由 ReadAt 返回,那么這時 err == EOF 或者 err == nil。

>若 ReadAt 按查找偏移量從輸入源讀取,ReadAt 應當既不影響
基本查找偏移量也不被它所影響。

>ReadAt 的客戶端可對相同的輸入源并行執行 ReadAt 調用。

######io.WriterAt 接口######
---

type Writer interface {
WriterAt(p []byte, off int64) (n int, err error) {
}```
官方文檔說明

WriteAt 從 p 中將 len(p) 個字節寫入到偏移量 off 處的基本數據流中。它返回從 p 中被寫入的字節數 n(0 <= n <= len(p))以及任何遇到的引起寫入提前停止的錯誤。若 WriteAt 返回的 n < len(p),它就必須返回一個 非nil 的錯誤。

若 WriteAt 按查找偏移量寫入到目標中,WriteAt 應當既不影響基本查找偏移量也不被它所影響。

若區域沒有重疊,WriteAt 的客戶端可對相同的目標并行執行 WriteAt 調用。

ReaderFrom 和 WriterTo 接口#####
ReaderFrom######

type ReaderFrom interface {
    ReaderFrom(r Reader) (n int64, err error)
}```
官方文檔說明:
>ReadFrom 從 r 中讀取數據,直到 EOF 或發生錯誤。其返回值 n 為讀取的字節數。除 io.EOF 之外,在讀取過程中遇到的任何錯誤也將被返回。

>如果 ReaderFrom 可用,Copy 函數就會使用它。

*PS: ```ReadFrom``` 方法不會返回 ```err == EOF```。*

下面的例子簡單的實現將文件中的數據全部讀取(顯示在標準輸出):

file, err := os.Open("writeAt.txt")
if err != nil {
panic(err)
}
defer file.Close()
writer := bufio.NewWriter(os.Stdout)
writer.ReadFrom(file)
writer.Flush()```
也可以通過ioutil.ReadFile 函數獲取文件全部內容, ioutil.ReadFile 內部通過ReadFrom方法實現。

如果不通過 ReadFrom 接口來做這件事,而是使用 io.Reader 接口,我們有兩種思路:######
  • 先獲取文件的大小(File 的 Stat 方法),之后定義一個該大小的 []byte,通過 Read 一次性讀取
  • 定義一個小的 []byte,不斷的調用 Read 方法直到遇到 EOF,將所有讀取到的 []byte 連接到一起
WriterTo#####

type WriterTo interface {
    WriterTo(w Writer) (n int64, err error)
}```
官方文檔說明
>WriteTo 將數據寫入 w 中,直到沒有數據可寫或發生錯誤。其返回值 n 為寫入的字節數。 在寫入過程中遇到的任何錯誤也將被返回。

>如果 WriterTo 可用,Copy 函數就會使用它

如果有“一次性從某個地方讀或寫到某個地方去。”這樣的需求,可以考慮使用這兩個接口:``` io.ReaderFrom``` 和 ```io.WriterTo```.
#####Seeker 接口#####
---

type Seeker interface {
Seek(offset int64, whence int) (ret int64, err error)
}```
官方文檔說明:

Seek 設置下一次 Read 或 Write 的偏移量為 offset,它的解釋取決于 whence: 0 表示相對于文件的起始處,1 表示相對于當前的偏移,而 2 表示相對于其結尾處。 Seek 返回新的偏移量和一個錯誤,如果有的話。

也就是說,Seek 方法用于設置偏移量的,這樣可以從某個特定位置開始操作數據流。聽起來和 ReaderAt/WriteAt 接口有些類似,不過 Seeker 接口更靈活,可以更好的控制讀寫數據流的位置。

簡單的示例代碼:獲取倒數第二個字符(需要考慮 UTF-8 編碼,這里的代碼只是一個示例)

reader := strings.NewReader("Go語言學習園地")
reader.Seek(-6, os.SEEK_END)
r, _, _ := reader.ReadRune()
fmt.Printf("%c\n", r)```

whence 的值,在 os 包中定義了相應的常量

const (
SEEK_SET int = 0 // seek relative to the origin of the file
SEEK_CUR int = 1 // seek relative to the current offset
SEEK_END int = 2 // seek relative to the end
)```

Closer接口#####

type Closer interface {
    Close() error
}```
該接口只有一個 Close() 方法,用于關閉數據流。

#####ByteReader 和 ByteWriter#####
這組接口的用途:讀或寫一個字節。接口定義如下:

---

type ByteReader interface {
ReadByte() (c byte, err error)
}

type ByteWriter interface {
WriteByte(c byte) error
}```

在標準庫中,有如下類型實現了 io.ByteReader 或 io.ByteWriter:

  • bufio.Reader/Writer 分別實現了io.ByteReader 和 io.ByteWriter
  • bytes.Buffer 同時實現了 io.ByteReader 和 io.ByteWriter
  • bytes.Reader 實現了 io.ByteReader
  • strings.Reader 實現了 io.ByteReader

eg:

var ch byte
fmt.Scanf("%c\n", &ch)

buffer := new(bytes.Buffer)
err := buffer.WriteByte(ch)
if err == nil {
    fmt.Println("寫入一個字節成功!準備讀取該字節……")
    newCh, _ := buffer.ReadByte()
    fmt.Printf("讀取的字節:%c\n", newCh)
} else {
    fmt.Println("寫入錯誤")
}```
接口的使用.... 在二進制數據或歸檔壓縮時用的比較多


#####ByteScanner、RuneReader 和 RuneScanner#####
######ByteScanner 接口的定義:######
------

type ByteScanner interface {
ByteReader // 內嵌了 ByteReader 接口
UnreadByte() error
}```
UnreadByte 方法的意思是:將上一次 ReadByte 的字節還原,使得再次調用 ReadByte 返回的結果和上一次調用相同,也就是說,UnreadByte 是重置上一次的 ReadByte。注意,UnreadByte 調用之前必須調用了 ReadByte,且不能連續調用 UnreadByte。

RuneReader 接口和 ByteReader 類似,只是 ReadRune 方法讀取單個 UTF-8 字符,返回其 rune 和該字符占用的字節數。該接口在 [regexp](http://golang.org/pkg/rege
xp) 包有用到。

RuneScanner 接口和 ByteScanner 類似

ReadCloser、ReadSeeker、ReadWriteCloser、ReadWriteSeeker、ReadWriter、WriteCloser 和 WriteSeeker 接口#####

這些接口是上面介紹的接口的兩個或三個組合而成的新接口。例如 ReadWriter 接口:

type ReadWriter interface {
    Reader
    Writer
}```
這是 Reader 接口和 Writer 接口的簡單組合(內嵌)。

這些接口的作用是:有些時候同時需要某兩個接口的所有功能,即必須同時實現了某兩個接口的類型才能夠被傳入使用。可見,io 包中有大量的“小接口”,這樣方便組合為“大接口”。

######SectionReader 類型#####
---
SectionReader 是一個 struct(沒有任何導出的字段),實現了 Read, Seek 和 ReadAt,同時,內嵌了 ReaderAt 接口。結構定義如下:

type SectionReader struct {
r ReaderAt // 該類型最終的 Read/ReadAt 最終都是通過 r 的 ReadAt 實現
base int64 // NewSectionReader 會將 base 設置為 off
off int64 // 從 r 中的 off 偏移處開始讀取數據
limit int64 // limit - off = SectionReader 流的長度
}```
從名稱我們可以猜到,該類型讀取數據流中部分數據。看一下

func NewSectionReader(r ReaderAt, off int64, n int64) *SectionReader```

的文檔說明就知道了:
>NewSectionReader 返回一個 SectionReader,它從 r 中的偏移量 off 處讀取 n 個字節后以 EOF 停止。

也就是說,SectionReader 只是內部(內嵌)ReaderAt 表示的數據流的一部分:從 off 開始后的 n 個字節。

這個類型的作用是:方便重復操作某一段 (section) 數據流;或者同時需要 ReadAt 和 Seek 的功能。


######LimitedReader 類型######
---

type LimitedReader struct {
R Reader // underlying reader,最終的讀取操作通過 R.Read 完成
N int64 // max bytes remaining
}```
文檔說明如下:

從 R 讀取但將返回的數據量限制為 N 字節。每調用一次 Read 都將更新 N 來反應新的剩余數量。
也就是說,最多只能返回 N 字節數據。

LimitedReader 只實現了 Read 方法(Reader 接口)。

使用示例如下:

content := "This Is LimitReader Example"
reader := strings.NewReader(content)
limitReader := &io.LimitedReader{R: reader, N: 8}
for limitReader.N > 0 {
    tmp := make([]byte, 2)
    limitReader.Read(tmp)
    fmt.Printf("%s", tmp)
}```
輸出:

This Is```
可見,通過該類型可以達到 只允許讀取一定長度數據 的目的。

在 io 包中,LimitReader 函數的實現其實就是調用 LimitedReader:

func LimitReader(r Reader, n int64) Reader { return &LimitedReader{r, n} }```

######PipeReader 和 PipeWriter 類型######
PipeReader(一個沒有任何導出字段的 struct)是管道的讀取端。它實現了 io.Reader 和 io.Closer 接口。

關于 Read 方法的說明:從管道中讀取數據。該方法會堵塞,直到管道寫入端開始寫入數據或寫入端關閉了。如果寫入端關閉時帶上了 error(即調用 CloseWithError 關閉),該方法返回的 err 就是寫入端傳遞的error;否則 err 為 EOF。

PipeWriter(一個沒有任何導出字段的 struct)是管道的寫入端。它實現了 io.Writer 和 io.Closer 接口。

關于 Write 方法的說明:寫數據到管道中。該方法會堵塞,直到管道讀取端讀完所有數據或讀取端關閉了。如果讀取端關閉時帶上了 error(即調用 CloseWithError 關閉),該方法返回的 err 就是讀取端傳遞的error;否則 err 為 ErrClosedPipe。

######io 包 管道 (pipe) 源碼分析######
PipeWriter 和 PipeReader 都只有i一個不可導出的成員```p *pipe```,這兩種類型的所有方法都是調用了 pipe 類型對應的方法實現的。

pipe類型的定義:

// A pipe is the shared pipe structure underlying PipeReader and PipeWriter.
type pipe struct {
rl sync.Mutex // gates readers one at a time
wl sync.Mutex // gates writers one at a time
l sync.Mutex // protects remaining fields
data []byte // data remaining in pending write
rwait sync.Cond // waiting reader
wwait sync.Cond // waiting writer
rerr error // if reader closed, error to give writes
werr error // if writer closed, error to give reads
}```

字段說明:

  • rl/wl 用于控制同一時刻只能有一個讀取器或寫入器
  • l 用于保護其他字段
  • data 在管道中的數據
  • rwait/wwait sync.Cond 類型(后續會講解),分別控制讀取器或寫入器等待
  • rerr/werr 讀取器(寫入器)關閉,該錯誤會被 Write (Read) 方法返回
    .
    .
    .
Copy 和 CopyN 函數######

Copy 函數的簽名:

func Copy(dst Writer, src Reader) (written int64, err error)
文檔說明:

Copy 將 src 復制到 dst,直到在 src 上到達 EOF 或發生錯誤。它返回復制的字節數,如果有的話,還會返回在復制時遇到的第一個錯誤。

成功的 Copy 返回 err == nil,而非 err == EOF。由于 Copy 被定義為從 src 讀取直到 EOF 為止,因此它不會將來自 Read 的 EOF 當做錯誤來報告。

若 dst 實現了 ReaderFrom 接口,其復制操作可通過調用 dst.ReadFrom(src) 實現。此外,若 src 實現了 WriterTo 接口,其復制操作可通過調用 src.WriteTo(dst) 實現。

eg1:

io.Copy(os.Stdout, strings.NewReader("Hello World!))```
**eg2:**

// 直接將內容輸出(寫入 Stdout 中)
func main() {
io.Copy(os.Stdout, os.Stdin)
fmt.Println("Got EOF -- bye")
}```

CopyN 函數的簽名:######

func CopyN(dst Writer, src Reader, n int64) (written int64, err error)```

函數文檔:
>CopyN 將 n 個字節從 src 復制到 dst。 它返回復制的字節數以及在復制時遇到的最早的錯誤。由于 Read 可以返回要求的全部數量及一個錯誤(包括 EOF),因此 CopyN 也能如此。
若 dst 實現了 ReaderFrom 接口,復制操作也就會使用它來實現。

#####ReadAtLeast 和 ReadFull 函數#####
######ReadAtLeast 函數簽名######
---

func ReadAtLeast(r Reader, buf []byte, min int) (n int, err error)```
函數文檔:

ReadAtLeast 將 r 讀取到 buf 中,直到讀了最少 min 個字節為止。它返回復制的字節數,如果讀取的字節較少,還會返回一個錯誤。若沒有讀取到字節,錯誤就只是 EOF。如果一個 EOF 發生在讀取了少于 min 個字節之后,ReadAtLeast 就會返回 ErrUnexpectedEOF。若 min 大于 buf 的長度,ReadAtLeast 就會返回 ErrShortBuffer。對于返回值,當且僅當 err == nil 時,才有 n >= min。

一般可能不太會用到這個函數。使用時需要注意返回的 error 判斷。

ReadFull函數簽名######

func ReadFull(r Reader,buf []byte) (n int,err error)```
函數文檔:
>ReadFull 精確地從 r 中將 len(buf) 個字節讀取到 buf 中。它返回復制的字節數,如果讀取的字節較少,還會返回一個錯誤。若沒有讀取到字節,錯誤就只是 EOF。如果一個 EOF 發生在讀取了一些但不是所有的字節后,ReadFull 就會返回 ErrUnexpectedEOF。對于返回值,當且僅當 err == nil 時,才有 n == len(buf)。

注意該函數和 ReadAtLeast 的區別:ReadFull 將 buf 讀滿;而 ReadAtLeast 是最少讀取 min 個字節。

#####WriteString 函數#####
---

func WriteString(w Writer, s string) (n int, err error)```
當 w 實現了 WriteString 方法時,直接調用該方法,否則執行 w.Write([]byte(s))。

MultiReader 和 MultiWriter 函數#####

在 io 包中定義了兩個非導出類型:mutilReader 和 multiWriter,它們分別實現了 io.Reader 和 io.Writer 接口。類型定義為:

type multiReader struct {
    readers []Reader
}

type multiWriter struct {
    writers []Writer
}```

MultiReader 和 MultiWriter 定義:

func MultiReader(readers ...Reader) Reader
func MultiWriter(writers ...Writer) Writer```
它們接收多個 Reader 或 Writer,返回一個 Reader 或 Writer。
MultiReader 只是邏輯上將多個 Reader 組合起來,并不能通過調用一次 Read 方法獲取所有 Reader 的內容。在所有的 Reader 內容都被讀完后,Reader 會返回 EOF。

TeeReader函數#####

函數簽名如下:

func TeeReader(r Reader, w Writer) Reader```

TeeReader 返回一個 Reader,它將從 r 中讀到的數據寫入 w 中。所有經由它處理的從 r 的讀取都匹配于對應的對 w 的寫入。它沒有內部緩存,即寫入必須在讀取完成前完成。任何在寫入時遇到的錯誤都將作為讀取錯誤返回。
也就是說,我們通過 Reader 讀取內容后,會自動寫入到 Writer 中去。例子代碼如下:

reader := io.TeeReader(strings.NewReader("Hello Go!"), os.Stdout)reader.Read(make([]byte, 9))```
輸出結果:

Hello Go!```
這種功能的實現其實挺簡單,無非是在 Read 完后執行 Write。
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

推薦閱讀更多精彩內容

  • golang標準庫對io的抽象非常精巧,各個組件可以隨意組合,可以作為接口設計的典范。這篇文章結合一個實際的例子來...
    icexin閱讀 10,440評論 1 16
  • 作者 丨icexin Golang 標準庫對 IO 的抽象非常精巧,各個組件可以隨意組合,可以作為接口設計的典范。...
    那個小碼哥閱讀 791評論 0 3
  • 摘要 Java I/O是Java技術體系中非常基礎的部分,它是學習Java NIO的基礎。而深入理解Java NI...
    biakia閱讀 7,633評論 7 81
  • golang的io包中,稍微有點兒晦澀的就是Pipe方法,今天我們就一起來看一看這個Pipe。 函數定義如下: f...
    suoga閱讀 15,507評論 9 10
  • 大學卒業后有過一段銘肌鏤骨的戀愛,之后和許多男同胞一樣,對戀愛有種莫明其妙的恐怖,很難承受另一段情感,出于心理需求...
    住你家隔壁偷窺閱讀 137評論 0 0