Golang time包

一、golang time 包的坑
1.定義

不同于 java 飽受詬病的各種亂七八糟的時(shí)間處理相關(guān)的包,golang 的 time 包的設(shè)計(jì)可謂相當(dāng)精巧。time 包用來(lái)描述“時(shí)刻”的類型為 Time,其定義如下:

type Time struct {
    // sec gives the number of seconds elapsed since
    // January 1, year 1 00:00:00 UTC.
    sec int64

    // nsec specifies a non-negative nanosecond
    // offset within the second named by Seconds.
    // It must be in the range [0, 999999999].
    nsec int32

    // loc specifies the Location that should be used to
    // determine the minute, hour, month, day, and year
    // that correspond to this Time.
    // The nil location means UTC.
    // All UTC times are represented with loc==nil, never loc==&utcLoc.
    loc *Location
}

如其注釋所述,sec 記錄其距離 UTC 時(shí)間0年1月1日0時(shí)的秒數(shù), nsec 記錄一個(gè) 0~999999999 的納秒數(shù),loc 記錄所在時(shí)區(qū)。事實(shí)上,僅需要 sec 和 nsec 就完全可以描述一個(gè)時(shí)間點(diǎn)了——“該時(shí)間點(diǎn)是距離 UTC 時(shí)間0年1月1日0時(shí) sec 秒、nsec 納秒的時(shí)間點(diǎn)”——非常準(zhǔn)確且非常不容易引起歧義,但這并不符合人們?nèi)粘I钪忻枋鰰r(shí)間點(diǎn)的方式,我們只會(huì)說(shuō)是某年某月某日,幾點(diǎn)幾分幾秒,然而,一旦要這樣說(shuō),事實(shí)上就涉及到時(shí)區(qū)了。

當(dāng)一個(gè) golang 的 Time 實(shí)例被程序員問:你記錄的是幾時(shí)幾分?這個(gè)實(shí)例可以說(shuō)是相當(dāng)無(wú)語(yǔ)的,因?yàn)橛譀]說(shuō)清是在哪個(gè)時(shí)區(qū)下的幾點(diǎn)幾分,我 TM 怎么知道是幾點(diǎn)幾分?然而程序員并不買賬,因?yàn)橐粋€(gè)時(shí)間點(diǎn)能準(zhǔn)確得說(shuō)出自己是幾點(diǎn)幾分似乎是天經(jīng)地義的事,于是 Time 類不得不自己記一個(gè)時(shí)區(qū),默認(rèn)就是這個(gè)無(wú)腦的程序員所在地的時(shí)區(qū)(這個(gè)值可以向操作系統(tǒng)索要),當(dāng)再次被問幾點(diǎn)幾分的時(shí)候,便可以作答了,而記的地方便是 loc 字段。

2.坑點(diǎn)

站在計(jì)算機(jī)冰冷的角度來(lái)看,“某時(shí)區(qū)某年某月某日幾點(diǎn)幾分幾秒”是對(duì)時(shí)間點(diǎn)的人性化描述,而“距離一個(gè)眾所周知的時(shí)間點(diǎn)多少秒、多少納秒”才是對(duì)時(shí)間點(diǎn)的準(zhǔn)確記錄。這一點(diǎn),在 Time 類型的實(shí)現(xiàn)中展現(xiàn)的淋漓盡致。所以,基于對(duì) Time 類型的了解,我們反觀一下對(duì)時(shí)間的一些操作,看看時(shí)區(qū)在影響著哪些。

時(shí)間的比較、求差操作,很明顯這類操作是與時(shí)區(qū)無(wú)關(guān)的,無(wú)論 loc 記錄的是什么,只要對(duì) sec 和 nsec 進(jìn)行比較、求差,就能得出正確的結(jié)果。時(shí)間的取時(shí)、取分操作,不用說(shuō)了,肯定是需要時(shí)區(qū)信息參與的。

時(shí)間的 format 操作,這里僅指 format 成年月日時(shí)分秒的形式,顯然也是需要時(shí)區(qū)參與的。時(shí)間的 parse 操作,即 format 的逆向操作,同樣需要時(shí)區(qū)參與。

而坑點(diǎn)就在這里,一方面,format 操作使用 Time 實(shí)例記錄的時(shí)區(qū),大多數(shù)情況下是本地時(shí)區(qū);另一方面,parse 操作在并不會(huì)默認(rèn)使用本地時(shí)區(qū)。

time.Parse() 會(huì)嘗試從 value 里讀出時(shí)區(qū)信息,當(dāng)且僅當(dāng):有時(shí)區(qū)信息、時(shí)區(qū)信息以 zone offset 形式(如+0800)表示、表示結(jié)果與本地時(shí)區(qū)等價(jià)時(shí),才會(huì)使用本地時(shí)區(qū),否則使用讀出的時(shí)區(qū)。若 value 里沒有時(shí)區(qū)信息,則使用 UTC 時(shí)間。這便是第一個(gè)坑點(diǎn)。

相比之下,第二個(gè)坑點(diǎn)便算不上什么大事了——不要使用 == 去比較時(shí)間是否相等。golang 可沒有什么重載運(yùn)算符的說(shuō)法,使用 == 比較兩個(gè) Time 實(shí)例時(shí),事實(shí)上就是比較 sec、nsec、loc 三個(gè)字段是否都相等。然而如我所述,僅需要 sec 和 nsec 就完全可以描述一個(gè)時(shí)間點(diǎn)了,所以只要這兩個(gè)字段相等,兩個(gè) Time 實(shí)例就是指的同一個(gè)時(shí)間點(diǎn)。而僅因?yàn)?loc 值的不同,便判定兩個(gè) Time 實(shí)例不相等,這是非常荒謬的。這就是為什么應(yīng)該使用 Equal 比較時(shí)間點(diǎn)是否相等的原因。

func main() {
    // format 字符串為 年月日時(shí)分秒,沒有時(shí)區(qū)信息
    format := "20060102150405"

    // t1 沒有寫 time.Now() 是為了避免秒以下單位的時(shí)間的影響
    // 除此之外和寫 time.Now() 是一樣的
    t1 := time.Date(2017, time.November, 30, 0, 0, 0, 0, time.Local)

    // t1 使用本地時(shí)區(qū)進(jìn)行 format,結(jié)果是 "20171130000000"
    // 由進(jìn)行 parse,由于沒有指定時(shí)區(qū),結(jié)果是 UTC 時(shí)間 2017/11/30 00:00:00
    t2, _ := time.Parse(format, t1.Format(format))
    
    // t1 使用本地時(shí)區(qū)進(jìn)行 format,結(jié)果是 "20171130000000"
    // t2 使用 UTC 時(shí)間進(jìn)行 format,結(jié)果是 "20171130000000"
    // 所以輸出 true
    println("1-1 ", t1.Format(format) == t2.Format(format))
    
    // 很顯然不相等,既不是指同一個(gè)時(shí)間點(diǎn),時(shí)區(qū)信息也不一樣,所以輸出 false
    println("1-2 ", t1 == t2)
    
    // 顯然不相等,t1 和 t2 不是指同一個(gè)時(shí)間點(diǎn),所以輸出 false
    println("1-3 ", t1.Equal(t2))

    // t1 使用本地時(shí)區(qū)進(jìn)行 format,結(jié)果是 "20171130000000"
    // 由進(jìn)行 parse,指定了本地時(shí)區(qū),結(jié)果是本地時(shí)間 2017/11/30 00:00:00
    t2, _ = time.ParseInLocation(format, t1.Format(format), time.Local)
    
    // 顯然相等,輸出 true
    println("2-1 ", t1.Format(format) == t2.Format(format))
    // 既指同一個(gè)時(shí)間點(diǎn),時(shí)區(qū)信息也一樣,輸出 true
    println("2-2 ", t1 == t2)
    // 顯然相等,輸出 true
    println("2-3 ", t1.Equal(t2))

    // 原本 t2 與 t1 完全相等,現(xiàn)在將 t2 改為 UTC 時(shí)間 
    t2 = t2.UTC()
    
    // t1 使用本地時(shí)區(qū)進(jìn)行 format,結(jié)果是 "20171130000000"
    // t2 使用 UTC 時(shí)間進(jìn)行 format,結(jié)果是 "20171129160000"
    // 所以輸出 false
    println("3-1 ", t1.Format(format) == t2.Format(format))
    
    // t1 和 t2 表示了相同的時(shí)間點(diǎn),但各自時(shí)區(qū)信息不同,所以輸出 false
    println("3-2 ", t1 == t2)
    
    // 由于 t1 和 t2 表示了相同的時(shí)間點(diǎn),所以輸出 true
    println("3-3 ", t1.Equal(t2))
}
3.在docker中

很明顯,若要避免不必要的麻煩,就要正確地使用 time 包——而這句話的大前提是操作系統(tǒng)的時(shí)區(qū)設(shè)置是正確的,否則一切都是空談。

顯然絕大多數(shù)的 PC、服務(wù)器的時(shí)區(qū)設(shè)置肯定是正確(是吧?要不你檢查下?)。需要提高警惕的是 docker 用戶,docker 在編譯鏡像、啟動(dòng)容器時(shí)均不會(huì)繼承宿主機(jī)的時(shí)區(qū)設(shè)置。如果容器內(nèi)的服務(wù)對(duì)時(shí)間不敏感,可能僅是輸出日志的時(shí)間不是本地時(shí)間的問題,而如果服務(wù)對(duì)時(shí)間敏感,比如每天早上九點(diǎn)執(zhí)行某任務(wù),可能就要出錯(cuò)了。以設(shè)為上海時(shí)區(qū)為例,解決方法有兩個(gè),可視情況取舍。

要么在鏡像編譯時(shí)指定好時(shí)區(qū):

...
RUN rm /etc/localtime && ln -s /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
...

要么在容器啟動(dòng)時(shí)指定好時(shí)區(qū):

docker run -e TZ="Asia/Shanghai" -v /etc/localtime:/etc/localtime:ro ...
二、示例
1.golang時(shí)間戳和時(shí)間的轉(zhuǎn)化

Golang 輸出格式化的時(shí)間,以及時(shí)間相關(guān)的一些方法

   //獲取當(dāng)前時(shí)間
   //2018-07-11 15:07:51.8858085 +0800 CST m=+0.004000001
   t := time.Now() 
   fmt.Println(t)
 
   //獲取當(dāng)前時(shí)間戳
   fmt.Println(t.Unix()) //1531293019
 
   //獲得當(dāng)前的時(shí)間
    //2018-7-15 15:23:00
   fmt.PrintIn(t.Uninx().Format("2006-01-02 15:04:05"))  
 
   //時(shí)間 to 時(shí)間戳
   //設(shè)置時(shí)區(qū)
   loc, _ := time.LoadLocation("Asia/Shanghai")        
   //2006-01-02 15:04:05是轉(zhuǎn)換的格式如php的"Y-m-d H:i:s"
   tt, _ := time.ParseInLocation("2006-01-02 15:04:05", "2018-07-11 15:07:51", loc) 
   fmt.Println(tt.Unix())                             //1531292871
 
   //時(shí)間戳 to 時(shí)間
   tm := time.Unix(1531293019, 0)
   //2018-07-11 15:10:19
   fmt.Println(tm.Format("2006-01-02 15:04:05")) 
 
   //獲取當(dāng)前年月日,時(shí)分秒
   y := t.Year()                 //年
   m := t.Month()                //月
   d := t.Day()                  //日
   h := t.Hour()                 //小時(shí)
   i := t.Minute()               //分鐘
   s := t.Second()               //秒
   //2018 July 11 15 24 59
   fmt.Println(y, m, d, h, i, s) 
}

//長(zhǎng)度為10的時(shí)間戳是以“秒”為單位; 
//長(zhǎng)度為13位數(shù)的時(shí)間戳是以“毫秒”為單位; 
//長(zhǎng)度為19位數(shù)的時(shí)間戳是以“納秒”為單位;

fmt.Printf("Now is %v\n", time.Now().Unix())    //秒
fmt.Printf("Now is %v\n", time.Now().UnixNano())   //納秒
fmt.Printf("Now is %v\n", time.Now().UnixNano()/1e6)  //納秒轉(zhuǎn)毫秒
fmt.Printf("Now is %v\n", time.Now().UnixNano()/1e9)   //納秒轉(zhuǎn)秒
//輸出:

//Now is 1536631685
//Now is 1536631685040620400
//Now is 1536631685040
//Now is 1536631685

更多參考 Golang的時(shí)間生成,格式化,以及獲取函數(shù)執(zhí)行時(shí)間的方法

2.Golang時(shí)區(qū)設(shè)置

在Go語(yǔ)言上,go語(yǔ)言的time.Now()返回的是當(dāng)?shù)貢r(shí)區(qū)時(shí)間,直接用:time.Now().Format("2006-01-02 15:04:05")輸出的是當(dāng)?shù)貢r(shí)區(qū)時(shí)間。

go語(yǔ)言并沒有全局設(shè)置時(shí)區(qū)這么一個(gè)東西,每次輸出時(shí)間都需要調(diào)用一個(gè)In()函數(shù)改變時(shí)區(qū):

var cstSh, _ = time.LoadLocation("Asia/Shanghai") //上海
fmt.Println("SH : ", time.Now().In(cstSh).Format("2006-01-02 15:04:05"))

在windows系統(tǒng)上,沒有安裝go語(yǔ)言環(huán)境的情況下,time.LoadLocation會(huì)加載失敗。

var cstZone = time.FixedZone("CST", 8*3600)       // 東八
fmt.Println("SH : ", time.Now().In(cstZone).Format("2006-01-02 15:04:05"))

最好的辦法是用time.FixedZone

三、定時(shí)器

1.GO-time.after 用法

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
    return NewTimer(d).C
}

直譯就是:  
等待參數(shù)duration時(shí)間后,向返回的chan里面寫入當(dāng)前時(shí)間。和NewTimer(d).C效果一樣,直到計(jì)時(shí)器觸發(fā),垃圾回收器才會(huì)恢復(fù)基礎(chǔ)計(jì)時(shí)器。如果擔(dān)心效率問題, 請(qǐng)改用 NewTimer, 然后調(diào)用計(jì)時(shí)器. 不用了就停止計(jì)時(shí)器。

解釋一下,是什么意思呢?
就是調(diào)用time.After(duration),此函數(shù)馬上返回,返回一個(gè)time.Time類型的Chan,不阻塞。后面你該做什么做什么,不影響。到了duration時(shí)間后,自動(dòng)塞一個(gè)當(dāng)前時(shí)間進(jìn)去。你可以阻塞的等待,或者晚點(diǎn)再取。因?yàn)榈讓邮怯肗ewTimer實(shí)現(xiàn)的,所以如果考慮到效率低,可以直接自己調(diào)用NewTimer。

package main

import (
    "time"
    "fmt"
)

func main()  {
    tchan := time.After(time.Second*3)
    fmt.Printf("tchan type=%T\n",tchan)
    fmt.Println("mark 1")
    fmt.Println("tchan=",<-tchan)
    fmt.Println("mark 2")
}

上面的例子運(yùn)行結(jié)果如下

tchan type=<-chan time.Time
mark 1
tchan= 2018-03-15 09:38:51.023106 +0800 CST m=+3.015805601
mark 2

首先瞬間打印出前兩行,然后等待3S,打印后后兩行。

2.Golang 定時(shí)器timer和ticker

func timer1() {
    fmt.Println("loop begin",time.Now())
    //timer1 := time.NewTimer(3 * time.Second)
    timer1 := time.NewTicker(3 * time.Second)
    for {
        select {
        case <-timer1.C:
            fmt.Println("loop loop",time.Now())
            //timer1.Reset(2*time.Second)
        }
    }
}
func main() {

        d := time.Duration(time.Second*2)

        t := time.NewTimer(d)
        defer t.Stop()

        for {
                <- t.C

                fmt.Println("timeout...")
        // need reset
        t.Reset(time.Second*2)
        }
}

使用timer定時(shí)器,超時(shí)后需要重置,才能繼續(xù)觸發(fā)。參考golang的一些坑---timer篇[Golang] timer可能造成的內(nèi)存泄漏

go func(){ <-timer.C //讀取timer的channel,當(dāng)timer到期時(shí)channel會(huì)讀取到數(shù)據(jù),向下執(zhí)行邏輯,但當(dāng)timer.stop()被調(diào)用時(shí),這個(gè)channel并不會(huì)讀取到數(shù)據(jù),導(dǎo)致一直hung在這里,goroutine不會(huì)退出 do something....}

4.golang中使用timer的三種方式

// (A)
time.AfterFunc(5 * time.Minute, func() {
    fmt.Printf("expired")
}

// (B) create a Timer object
timer := time.NewTimer(5 * time.Minute)
<-timer.C
fmt.Printf("expired")

// (C) time.After() returns timer.C internally
<-time.After(5 * time.Minute)
fmt.Printf("expired")

注意AfterFunc是在另外一個(gè)協(xié)程里,參考Go語(yǔ)言中的定時(shí)器

3.go里面select-case和time.Ticker的使用注意事項(xiàng)
問題出在這個(gè)select里面:

select {
case ch <- i:
case <-tick.C:
fmt.Printf("%d: case <-tick.C\n", i)
}

當(dāng)兩個(gè)case條件都滿足的時(shí)候,運(yùn)行時(shí)系統(tǒng)會(huì)通過一個(gè)偽隨機(jī)的算法決定哪個(gè)case將會(huì)被執(zhí)行
所以當(dāng)tick.C條件滿足的那個(gè)循環(huán),有某種概率造成ch<-i沒有發(fā)送(雖然通道兩端沒有阻塞,滿足發(fā)送條件)

4.從99.9%CPU淺談Golang的定時(shí)器實(shí)現(xiàn)原理

//場(chǎng)景1:
for {
    select {
    case <- time.After(10 * time.Microsecond):
        fmt.Println("hello timer")
    }
}

//場(chǎng)景2:
for {
    select {
    case <- time.Tick(10 * time.Microsecond):
        fmt.Println("hello, tick")
    }
}

從第3節(jié)的源碼中我們可以看到After和Tick其實(shí)是一個(gè)創(chuàng)建了一個(gè)單次的timer一個(gè)是創(chuàng)建了一個(gè)永久性的timer。因此場(chǎng)景2中Tick的用法會(huì)導(dǎo)致進(jìn)程中創(chuàng)建無(wú)數(shù)個(gè)Tick,這最終導(dǎo)致了timer處理線程忙死。因此,使用Tick進(jìn)行定時(shí)任務(wù)的話我們可以將Tick對(duì)象建在循環(huán)外面:

    tick := time.Tick(10 * time.Microsecond)
    for {
        select {
        case <- tick:
            fmt.Printf("hello, tick 2")
        }
    }

其次golang的處理方式中也可以看出,go的timer的處理和用戶端程序定義的間隔時(shí)間不一定完全精準(zhǔn),用戶的回調(diào)函數(shù)執(zhí)行時(shí)間越長(zhǎng)單個(gè)timer對(duì)堆中其他鄰近timer的影響越大。因此timer的回調(diào)函數(shù)一定是執(zhí)行時(shí)間越短越好。

5.golang中timer定時(shí)器實(shí)現(xiàn)原理

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,732評(píng)論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,214評(píng)論 3 426
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,781評(píng)論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,588評(píng)論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,315評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,699評(píng)論 1 327
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,698評(píng)論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,882評(píng)論 0 289
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,441評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,189評(píng)論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,388評(píng)論 1 372
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,933評(píng)論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,613評(píng)論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,023評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,310評(píng)論 1 293
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,112評(píng)論 3 398
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,334評(píng)論 2 377

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