ethan

ethan

新知,热爱生活,码农,读书
twitter
email
github

Go自帶庫的使用說明

Go 中的時間操作#

Golang 中與時間有關的操作,主要涉及到 time 包,核心數據結構是 time.Time,如下:

type Time struct {
    wall uint64
    ext  int64
    loc *Location
}

1、獲取時間相關函數#

1.1 獲取當前時間#

// 返回當前時間,注意此時返回的是 time.Time 類型
now := time.Now()
fmt.Println(now)
// 當前時間戳
fmt.Println(now.Unix())
// 納秒級時間戳
fmt.Println(now.UnixNano())
// 時間戳小數部分 單位:納秒
fmt.Println(now.Nanosecond())

輸出:

2021-01-10 14:56:15.930562 +0800 CST m=+0.000124449
1610261775
1610261775930562000
930562000

1.2 返回當前年月日時分秒、星期幾、一年中的第幾天等操作#

now := time.Now()
// 返回日期
year, month, day := now.Date()
fmt.Printf("year:%d, month:%d, day:%d\n", year, month, day)
// 年
fmt.Println(now.Year())
// 月
fmt.Println(now.Month())
// 日
fmt.Println(now.Day())
// 時分秒
hour, minute, second := now.Clock()
fmt.Printf("hour:%d, minute:%d, second:%d\n", hour, minute, second)
// 時
fmt.Println(now.Hour())
// 分
fmt.Println(now.Minute())
// 秒
fmt.Println(now.Second())
// 返回星期
fmt.Println(now.Weekday())
//返回一年中對應的第幾天
fmt.Println(now.YearDay())
//返回時區
fmt.Println(now.Location())
// 返回一年中第幾天
fmt.Println(now.YearDay())

1.3 格式化時間#

Go 語言提供了時間類型格式化函數 Format(),需要注意的是 Go 語言格式化時間模板不是常見的 Y-m-d H:i:s,而是 2006-01-02 15:04:05,也很好記憶 (2006 1 2 3 4 5)。

now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05"))
fmt.Println(now.Format("2006-01-02"))
fmt.Println(now.Format("15:04:05"))
fmt.Println(now.Format("2006/01/02 15:04"))
fmt.Println(now.Format("15:04 2006/01/02"))

2、時間戳與日期字符串相互轉化#

時間戳轉成日期格式,需要先轉成將時間戳轉成 time.Time 類型再格式化成日期格式。

2.1 根據秒數、納秒數返回 time.Time 類型#

now := time.Now()
layout := "2006-01-02 15:04:05"
t := time.Unix(now.Unix(),0)    // 參數分別是:秒數,納秒數
fmt.Println(t.Format(layout))

2.2 根據指定時間返回 time.Time 類型,使用函數 time.Date()#

now := time.Now()
layout := "2006-01-02 15:04:05"
//根據指定時間返回 time.Time 類型
//分別指定年,月,日,時,分,秒,納秒,時區
t := time.Date(2011, time.Month(3), 12, 15, 30, 20, 0, now.Location())
fmt.Println(t.Format(layout))

2.3 日期字符串解析成 time.Time 類型#

t, _ := time.ParseInLocation("2006-01-02 15:04:05", time.Now().Format("2006-01-02 15:04:05"), time.Local)
fmt.Println(t)  
// 輸出 2021-01-10 17:28:50 +0800 CST
// time.Local 指定本地時間

解析的時候需要特別注意時區的問題:

fmt.Println(time.Now())
fmt.Println(time.Now().Location())
t, _ := time.Parse("2006-01-02 15:04:05", "2021-01-10 15:01:02")
fmt.Println(t)

輸出:

2021-01-10 17:22:10.951904 +0800 CST m=+0.000094166
Local
2021-01-10 15:01:02 +0000 UTC

可以看到,time.Now() 使用的 CST (中國標準時間),而 time.Parse() 默認的是 UTC (零時區),它們相差 8 小時。所以解析時常用 time.ParseInLocation(),可以指定時區。img

3、計算、比較日期#

講到日期的計算就不得不提 time 包提供的一種新的類型 Duration,源碼是這樣定義的:

type Duration int64

底層類型是 int64,表示一段時間間隔,單位是 納秒。

3.1 24 小時之內的時間計算#

now := time.Now()
fmt.Println(now)
// 1小時1分1s之後
t1, _ := time.ParseDuration("1h1m1s")
fmt.Println(t1)
m1 := now.Add(t1)
fmt.Println(m1)
// 1小時1分1s之前
t2, _ := time.ParseDuration("-1h1m1s")
m2 := now.Add(t2)
fmt.Println(m2)
// 3小時之前
t3, _ := time.ParseDuration("-1h")
m3 := now.Add(t3 * 3)
fmt.Println(m3)
// 10 分鐘之後
t4, _ := time.ParseDuration("10m")
m4 := now.Add(t4)
fmt.Println(m4)
// Sub 計算兩個時間差
sub1 := now.Sub(m3)
fmt.Println(sub1.Hours())   // 相差小時數
fmt.Println(sub1.Minutes()) // 相差分鐘數

額外再介紹兩個函數 time.Since()time.Until()

// 返回當前時間與 t 的時間差,返回值是 Duration
time.Since(t Time) Duration
// 返回 t 與當前時間的時間差,返回值是 Duration
time.Until(t Time) Duration

now := time.Now()
fmt.Println(now)
t1, _ := time.ParseDuration("-1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(time.Since(m1))
fmt.Println(time.Until(m1))

輸出:

2021-01-10 20:41:48.668232 +0800 CST m=+0.000095594
2021-01-10 19:41:48.668232 +0800 CST m=-3599.999904406
1h0m0.000199007s
-1h0m0.000203035s

3.2 24 小時之外的時間計算#

涉及到一天以外的時間計算,就需要用到 time.AddDate(),函數原型:

func (t Time) AddDate(years int, months int, days int) Time

比如想知道 一年一個月零一天 之後的時間,就可以這樣:

now := time.Now()
fmt.Println(now)
m1 := now.AddDate(1,1,1)
fmt.Println(m1)

再比如,想獲得 2 天之前時間:

now := time.Now()
fmt.Println(now)
m1 := now.AddDate(0,0,-2)
fmt.Println(m1)

3.3 日期比較#

日期的比較總共有三種:之前、之後和相等。

// 如果 t 代表的時間點在 u 之前,返回真;否則返回假。
func (t Time) Before(u Time) bool
// 如果 t 代表的時間點在 u 之後,返回真;否則返回假。
func (t Time) After(u Time) bool
// 比較時間是否相等,相等返回真;否則返回假。
func (t Time) Equal(u Time) bool

now := time.Now()
fmt.Println(now)
// 1小時之後
t1, _ := time.ParseDuration("1h")
m1 := now.Add(t1)
fmt.Println(m1)
fmt.Println(m1.After(now))
fmt.Println(now.Before(m1))
fmt.Println(now.Equal(m1))

輸出:

2021-01-10 21:00:44.409785 +0800 CST m=+0.000186800
2021-01-10 22:00:44.409785 +0800 CST m=+3600.000186800
true
true
false

4、常見例子#

下面列舉一些常見的例子和函數封裝。

4.1 日期格式 轉 時間戳#

func TimeStr2Time(fmtStr,valueStr, locStr string) int64 {
    loc := time.Local
    if locStr != "" {
        loc, _ = time.LoadLocation(locStr) // 設置時區
    }
    if fmtStr == "" {
        fmtStr = "2006-01-02 15:04:05"
    }
    t, _ := time.ParseInLocation(fmtStr, valueStr, loc)
    return t.Unix()
}

4.2 獲取當前時間日期格式#

func GetCurrentFormatStr(fmtStr string) string {
    if fmtStr == "" {
        fmtStr = "2006-01-02 15:04:05"
    }
    return time.Now().Format(fmtStr)
}

4.3 時間戳 to 日期格式#

func Sec2TimeStr(sec int64, fmtStr string) string {
    if fmtStr == "" {
        fmtStr = "2006-01-02 15:04:05"
    }
    return time.Unix(sec, 0).Format(fmtStr)
}

Go-regexp 正則#

package main

import (
    "fmt"
    "regexp"
)

const text = "My email is [email protected]"

func main() {
    compile := regexp.MustCompile(`[a-zA-Z0-9]+@[a-zA-Z0-9]+\.[a-zA-Z0-9]+`)
    match := compile.FindString(text)
    fmt.Println(match)
}

Go 存儲基礎 — 文件 IO 操作#

img

兩大 IO 分類#

計算的體系架構,CPU,內存,網絡,IO。那么 IO 是什麼呢?一般理解成 Input、Output 的縮寫,通俗話就是輸入輸出的意思。

IO 分為網絡和存儲 IO 兩種類型(其實網絡 IO 和磁盤 IO 在 Go 裡面有著根本性區別)。網絡 IO 對應的是網絡數據傳輸過程,網絡是分布式系統的基石,通過網絡把離散的物理節點連接起來,形成一個有機的系統。

存儲 IO 對應的就是數據存儲到物理介質的過程,通常物理介質對應的是磁盤,磁盤上一般會分個區,然後在上面格式化個文件系統出來,所以普通程序員最常看見的是文件 IO 的形式。

在 Golang 裡可以歸類出兩種讀寫文件的方式:

  1. 標準庫封裝:操作對象 File;
  2. 系統調用 :操作對象 fd;

讀寫數據要素#

文件的讀寫最核心的要素是什麼?

通俗來講:讀文件,就是把磁盤上的文件的特定位置的數據讀到內存的 buffer 。寫文件,就是把內存 buffer 的數據寫到磁盤的文件的特定位置

這裡注意到兩個關鍵詞:

  1. 特定位置;
  2. 內存 buffer;

特定位置怎麼理解?怎麼指定所謂的特定位置

很簡單,用 [ offset, length ] 這兩個參數就能標識一段位置。

img

也就是 IO 偏移和長度,Offset 和 Length。

內存 buffer 怎麼理解?

歸根結底,文件的數據和誰直接打交道?內存,寫的時候是從內存寫到磁盤文件的,讀的時候是從磁盤文件讀到內存的。

本質上,下面的 IO 函數都離不開 Offset,Length,buffer 這三個要素。

標準庫封裝#

Go 對文件進行讀寫非常簡單,因為 Go 已經封裝了一個非常便捷的使用接口,位於標準庫 os 中。Go 標準庫對文件 IO 的封裝也就是 Go 推薦對文件進行 IO 時使用的操作方式。

打開文件(Open)#

func OpenFile(name string, flag int, perm FileMode) (*File, error)

Open 文件之後,獲取到一個句柄,也就是 File 結構,之後對文件的讀寫都是基於 File 結構之上進行的。

type File struct {
    *file // os specific
}

文件讀寫只需要針對這個句柄結構體做操作即可。

另外有一點隱藏起來的知識點必須要提一下:偏移。也就是最開始強調的讀寫 3 要素之一的 Offset 。打開(Open)文件的時候,文件當前偏移量默認設置為 0,也就是說 IO 的起始位置就是文件的最開頭。舉個例子,如果這個時候,寫 4K 的數據到文件,那麼就是寫 [0, 4K] 這個位置的數據,如果之前這上面已經有數據了,那麼就會是覆蓋寫。

除非 Open 文件的時候指定 O_APPEND 選項,偏移量會設置為文件末尾,那麼 IO 都是從文件末尾開始。

文件寫操作(Write)#

文件 File 句柄對象有兩個寫方法:

第一種:寫一個 buffer 到文件 ,使用文件當前偏移

func (f *File) Write(b []byte) (n int, err error)

注意:該寫操作會導致文件偏移量的增加。

第二種:從指定文件偏移,寫入 buffer 到文件

func (f *File) WriteAt(b []byte, off int64) (n int, err error)

注意:該寫操作不會更新文件偏移量

文件讀操作(Read)#

和寫對應,文件 File 句柄對象有兩個讀方法:

第一種:從文件當前偏移讀一個 buffer 的數據上來

func (f *File) Read(b []byte) (n int, err error)

注意:該讀操作會導致文件偏移量的增加。

第二種:從指定文件偏移,讀一個 buffer 大小的數據上來

func (f *File) ReadAt(b []byte, off int64) (n int, err error)

注意:該讀操作不會更新文件偏移量

指定偏移量(Seek)#

func (f *File) Seek(offset int64, whence int) (ret int64, err error)

這個句柄方法允許用戶指定文件的偏移位置。這個很容易理解,舉個例子,文件剛開始是 0 字節,寫 1M 的數據下去,大小變成 1M,Offset 往後挪 1M ,默認就是往後挪。

現在 Seek 方法允許把寫的偏移定位到任意位置,可以就可以從任意地方覆蓋寫入數據。

所以在 Go 裡面,文件 IO 非常簡單,先 Open 一個文件,拿到 File 句柄,然後就可以使用這個句柄 Write ,Read,Seek 就能進行 IO 了。

底層的原理#

Go 的標準庫 os 提供了極其方便的封裝,深入最原始的本質可以發現最核心的東西:系統調用

Go 標準庫的文件存儲 IO 就是基於系統調用之上的。可以稍微跟一下 os.OpenFile 的調用:

os 庫的 OpenFile 函數:

func OpenFile(name string, flag int, perm FileMode) (*File, error) {
    f, err := openFileNolog(name, flag, perm)
    if err != nil {
        return nil, err
    }
    f.appendMode = flag&O_APPEND != 0
    return f, nil
}

稍微看下 openFileNolog 函數:

func openFileNolog(name string, flag int, perm FileMode) (*File, error) {
    var r int
    for {
        var e error
        r, e = syscall.Open(name, flag|syscall.O_CLOEXEC, syscallMode(perm))
        if e == nil {
            break
        }
        if runtime.GOOS == "darwin" && e == syscall.EINTR {
            continue
        }
        return nil, &PathError{"open", name, e}
    }
    return newFile(uintptr(r), name, kindOpenFile), nil
}

可以看到 syscall.Open ,這個函數獲取到一個整數,也就是在 c 語言裡最常見的 fd 句柄,而 File 結構則僅僅是基於這個的一層封裝而已。

思考下,為什麼會有標準庫封裝這一層存在?

劃重點:為了屏蔽操作系統的區別,使用這個標準庫的所有操作都是跨平台的。換句話說,如果是特殊操作系統才有的特性,那麼在 os 庫裡就找不到對應封裝的 IO 操作。

img

那麼怎麼使用系統調用?

直接使用 syscall 庫,也就是系統調用。從名字也能看出來,系統調用是和操作系統強相關的,因為是操作系統提供的調用接口,所以系統調用會因為操作系統不同而導致不同的特性,不同的接口。

所以,如果直接使用 syscall 庫來使用系統調用,那麼需要自己來承擔系統帶來的兼容性問題。

系統調用#

系統調用在 syscall 裡有一層最基礎的封裝:

文件 Open#

func Open(path string, mode int, perm uint32) (fd int, err error) 

文件 Read#

func Read(fd int, p []byte) (n int, err error) func Pread(fd int, p []byte, offset int64) (n int, err error) 

文件讀有兩個接口,一個 Read 是從當前默認偏移讀一個 buffer 數據,Pread 接口則是從指定位置讀數據的接口。

思考一個問題:Pread 從效果上來講等於 SeekRead 組合起來使用,那麼是否可以認為 Pread 就可以被 Seek + Read 替代呢?

不行!根本原因在於 Seek + Read 是在用戶層就是兩步操作,而 Pread 雖然是 Seek + Read 的效果,但是操作系統給到用戶的語義是:Pread 是一個原子操作。還有一個重要區別,Pread 不會改變當前文件的偏移量(普通的 Read 調用會更新偏移量)。

所以,總結下,**Pread** 和順序調用 **Seek** 後調用 **Read** 有兩點重要區別:

  1. Pread 對用戶提供的語義是原子操作,在調用 Pread 時,無法中斷 SeekRead 操作;
  2. Pwrite 調用不會更新當前文件偏移量;

文件 Write#

func Write(fd int, p []byte) (n int, err error) func Pwrite(fd int, p []byte, offset int64) (n int, err error) 

文件寫對應也是有兩種接口,WrtiePwrite 分別是對應 ReadPread 。同樣的,Pwrite 作用上也是相當於先調用 Seek 再調用 Write ,但是同樣的也有兩點不同

  1. Pwrite 完成 SeekWrite 對外是原子操作的語義;
  2. Pwrite 調用不會更新當前文件偏移量;

文件 Seek#

func Seek(fd int, offset int64, whence int) (off int64, err error) 

這個函數調用允許用戶指定偏移(這個會影響到 ReadWrite 讀寫的位置)。一般來說,每個打開文件都有一個相關聯的 “當前文件偏移量”( current file offset )。讀(Read)、寫(Write)操作都是從當前文件偏移量處開始,並且 ReadWrite 會導致偏移量增加,增加量就是所讀寫的字節數。

小結一下:Go 核心的 Open,Read,Write,Seek 幾個系統調用,可以發現一個明顯不同與標準 IO 庫的區別:系統調用操作對象是個整數句柄Open 文件得到一個整數 fd,之後的所有 IO 都是針對這個 fd 來操作的。這個明顯和標準庫不同,os 標準庫 OpenFile 得到的是一個 File 結構體,所有的 IO 也是針對這個結構體的。

層次架構#

那麼究竟封裝的層次一般是什麼樣的呢, Unix 編程裡面開篇就有一張如下圖:

img

這張圖就非常形象的講明白了整個 Unix 體系結構。

  • 內核是最核心的實現,包括了和 IO 設備,硬件交互等功能。與內核緊密的一層是內核提供給外部調用的系統調用,系統調用提供了用戶態到內核態調用的一個通道;

  • 對於系統調用,各個語言的標準庫會有一些封裝,比如 C 語言的 libc 庫,Go 語言的 os ,syscall 庫都是類似的地位,這個就是所謂的公共庫。這層封裝的作用最主要是簡化普通程序員使用效率,並且屏蔽系統細節,為跨平台提供基礎(同樣的,為了跨平台的特性,可能會阉割很多不兼容的功能,所以才會有直接調用系統掉調用的需求);

  • 當然,右上角還看到一個缺口,應用程序除了可以使用公共函數庫,其實是可以直接調用系統調用的,但是由此帶來的複雜性又應用自己承擔。這種需求也是很常見的,標準庫封裝了通用的東西,同樣割舍了很多系統調用的功能,這種情況下,只能通過系統調用來獲取;

總結#

  1. IO 大類分為網絡 IO 和磁盤 IO,IO 對文件來說就是讀寫操作,寫的時候數據從內存到磁盤,讀的時候數據從磁盤到內存

  2. Go 文件 IO 最常用的是 os 庫,使用 Go 封裝的標準庫,os.OpenFile 打開,File.WriteFile.Read 進行讀寫,操作對象都是 File 結構體;

  3. Go 標準庫對 IO 的封裝是為了屏蔽複雜的系統調用,提供跨平台的使用姿勢。然後單獨提供 syscall 庫,讓程序員自我決策使用要使用更豐富的系統調用功能,當然後果自負;

  4. Go 標準庫 IO 操作對象是 File ,系統調用 IO 操作對象是 fd(非負整數)。

  5. Open 文件默認當前偏移量是 0 (文件最開始),加上 O_APPEND 參數之後偏移量會是文件末尾。通過 Seek 調用可以任意指定文件偏移,從而影響文件 IO 的位置;

  6. ReadWrite 函數只有 buffer (buffer 有長度),偏移則使用當前文件偏移量;

  7. PreadPwrite 的系統調用效果等同於 Seek 偏移量然後 ReadWrite,但是又大有不同。對外語義是原子操作,並且不更新當前文件偏移量;

Go - 文件讀寫操作#

讀寫文件#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*在已存在文件清空原有內容進行追加*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_RDWR|os.O_APPEND, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    /*關閉文件流*/
    defer file.Close()
    /*讀取*/
    reader := bufio.NewReader(file)
    for {
        str, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        fmt.Print(str)
    }
    /*寫入文件*/
    str := "hello FCC您好!!!\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*因為writer是帶緩存的,需要通過flush到磁盤*/
    writer.Flush()
}

文件內容拷貝至新文件#

package main

import (
    "fmt"
    "io/ioutil"
)

/*將文件1的內容拷貝到文件2*/
func main() {
    file1Path := "D:\\fcofficework\\DNS\\1.txt"
    file2Path := "D:\\fcofficework\\DNS\\2.txt"
    data, err := ioutil.ReadFile(file1Path)
    if err != nil {
        fmt.Printf("read file err=%v", err)
        return
    }
    err = ioutil.WriteFile(file2Path, data, 0666)
    if err != nil {
        fmt.Printf("write file err=%v\n", err)
    }
}

判斷文件或者目錄是否存在#

package main

import (
    "fmt"
    "os"
)

/*判斷文件以及目錄是否存在*/
func PathExists(path string) (bool, error) {
    _, err := os.Stat(path)
    if err == nil {
        fmt.Println("當前文件存在!")
        return true, nil
    }
    if os.IsNotExist(err) {
        fmt.Println("當前文件不存在!")
        return false, nil
    }
    return false, nil
}

func main() {
    path := "D:\\fcofficework\\2.txt"
    PathExists(path)
}

文件的拷貝#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*文件的拷貝*/

func CopyFile(dstFileName string, srcFileName string) (written int64, err error) {
    srcFile, err := os.Open(srcFileName)
    if err != nil {
        fmt.Printf("open file err=%v\n", err)
    }
    reader := bufio.NewReader(srcFile)

    dstFile, err := os.OpenFile(dstFileName, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Printf("open file err=%v\n", err)
        return
    }
    writer := bufio.NewWriter(dstFile)
    defer dstFile.Close()
    return io.Copy(writer, reader)
}

func main() {
    srcFile := "D:\\Photos\\Datapicture\\mmexport1530688562488.jpg"
    dstFile := "D:\\Photos\\1.jpg"
    _, err := CopyFile(dstFile, srcFile)
    if err == nil {
        fmt.Println("拷貝完成!")
    } else {
        fmt.Println("拷貝失敗,err=", err)
    }
}

讀取文件並統計文件中字符的個數#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*統計文件的字符個數*/

type CharCount struct {
    /*英文的個數*/
    ChCount int
    /*數字的個數*/
    NumCount int
    /*空格的個數*/
    SpaceCount int
    /*其他字符的個數*/
    OtherCount int
}

func main() {
    fileName := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.Open(fileName)
    if err != nil {
        fmt.Printf("open file err=%v\n", err)
        return
    }
    defer file.Close()
    var count CharCount
    reader := bufio.NewReader(file)
    for {
        str, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        for _, v := range str {
            switch {
            case v >= 'a' && v <= 'z':
                fallthrough
            case v >= 'A' && v <= 'Z':
                count.ChCount++
            case v == ' ' || v == '\t':
                count.SpaceCount++
            case v >= '0' && v <= '9':
                count.NumCount++
            default:
                count.OtherCount++
            }
        }
    }
    fmt.Printf("字符的個數為:%v 數字的個數為:%v 空格的個數為:%v 其他字符的個數為:%v",
        count.ChCount, count.NumCount, count.SpaceCount, count.OtherCount)
}

三種讀取文件的方式#

透過 os 讀取#

package main

import (
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("d:\\Photos\\Screenshots\\暗物質\\IMG_20180927_194619.jpg")
    if err != nil {
        fmt.Println("open file err", err)
    }
    fmt.Printf("file=%v", file)
    err1 := file.Close()
    if err1 != nil {
        fmt.Println("close file err = ", err1)
    }
}

緩衝式讀取文件#

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
)

/*緩衝式讀取文件*/
func main() {
    file, err := os.Open("d:\\Photos\\Screenshots\\暗物質\\IMG_20180927_194619.jpg")
    if err != nil {
        fmt.Println("open file err", err)
    }
    defer file.Close()
    reader := bufio.NewReader(file)
    for {
        str, err := reader.ReadString('\n')
        if err == io.EOF {
            break
        }
        fmt.Print(str)
    }
    fmt.Println("文件讀取結束!")
}

透過 ioutil 讀取#

package main

import (
    "fmt"
    "io/ioutil"
)

func main() {
    file := "D:\\fcofficework\\DNS\\authorized_keys"
    content, err := ioutil.ReadFile(file)
    if err != nil {
        fmt.Printf("read file err=%v", err)
    }
    fmt.Printf("%v", string(content))
}

文件寫入的案例#

在文件寫入內容,沒有則重新創建#

package main

import (
    "bufio"
    "fmt"
    "os"
)

/*在文件寫入內容,沒有文件則重新創建*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    defer file.Close()
    str := "hello world\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*因為writer是帶緩存的,需要通過flush到磁盤*/
    writer.Flush()
}

在已存在文件清空原有內容重新寫入#

package main

import (
    "bufio"
    "fmt"
    "os"
)

/*在已存在文件清空原有內容重新寫入*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_TRUNC, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    defer file.Close()
    str := "hello FCC\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*因為writer是帶緩存的,需要通過flush到磁盤*/
    writer.Flush()
}

在已存在文件清空原有內容進行追加#

package main

import (
    "bufio"
    "fmt"
    "os"
)

/*在已存在文件清空原有內容進行追加*/
func main() {
    filePath := "D:\\fcofficework\\DNS\\1.txt"
    file, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0666)
    if err != nil {
        fmt.Printf("open file err = %v\n", err)
        return
    }
    defer file.Close()
    str := "hello FCC您好!!!\r\n"
    writer := bufio.NewWriter(file)
    for i := 0; i < 5; i++ {
        writer.WriteString(str)
    }
    /*因為writer是帶緩存的,需要通過flush到磁盤*/
    writer.Flush()
}

解析命令行參數#

package main

import (
    "fmt"
    "os"
)

/*解析命令行參數*/
func main() {
    fmt.Println("命令行參數有:", len(os.Args))
    for i, v := range os.Args {
        fmt.Printf("args[%v]=%v\n", i, v)
    }
}
package main

import (
    "flag"
    "fmt"
)

/*解析命令行參數*/
func main() {
    var user string
    var pwd string
    var host string
    var port int
    flag.StringVar(&user, "u", "", "用戶名,默認為空")
    flag.StringVar(&pwd, "pwd", "", "密碼,默認為空")
    flag.StringVar(&host, "h", "localhost", "主機名,默認為空")
    flag.IntVar(&port, "port", 3306, "端口號,默認為空")
    /*轉換*/
    flag.Parse()
    fmt.Printf("user=%v pwd=%v host=%v port=%v", user, pwd, host, port)
}

Go-json 序列化#

序列化#

package main

import (
    "encoding/json"
    "fmt"
)

type Monster struct {
    Name     string  `json:"name"`
    Age      int     `json:"age"`
    Birthday string  `json:"birthday"`
    Sal      float64 `json:"sal"`
    Skill    string  `json:"skill"`
}

/*結構體序列化*/
func NewMinsterStruct() {
    monster := Monster{
        Name:     "孫悟空",
        Age:      500,
        Birthday: "2011-11-11",
        Sal:      8000.0,
        Skill:    "如意七十二變",
    }
    data, err := json.Marshal(&monster)
    if err != nil {
        fmt.Printf("序列化錯誤err:%v\n", err)
    }
    fmt.Printf("Map序列化後=%v\n", string(data))
}

/*Map序列化*/
func MapSerlizer() {
    var a map[string]interface{}
    a = make(map[string]interface{})
    a["name"] = "牛魔王"
    a["age"] = 10
    a["address"] = "火雲洞"
    data, err := json.Marshal(a)
    if err != nil {
        fmt.Printf("序列化錯誤err:%v\n", err)
    }
    fmt.Printf("monster序列化後=%v\n", string(data))
}

/*切片序列化*/
func SliceSerlizer() {
    var slice []map[string]interface{}
    var m1 map[string]interface{}
    m1 = make(map[string]interface{})
    m1["name"] = "TGH"
    m1["age"] = "19"
    m1["address"] = "北京"
    slice = append(slice, m1)

    var m2 map[string]interface{}
    m2 = make(map[string]interface{})
    m2["name"] = "FCC"
    m2["age"] = "18"
    m2["address"] = [2]string{"華府", "影視帝國"}
    slice = append(slice, m2)

    data, err := json.Marshal(slice)
    if err != nil {
        fmt.Printf("序列化錯誤err:%v\n", err)
    }
    fmt.Printf("切片序列化後=%v\n", string(data))
}

/*基本數據類型序列化*/
func FloatSerlize() {
    var num1 float64 = 245.56
    data, err := json.Marshal(num1)
    if err != nil {
        fmt.Printf("序列化錯誤err:%v\n", err)
    }
    fmt.Printf("基本數據類型序列化後=%v\n", string(data))
}

func main() {
    NewMinsterStruct()
    MapSerlizer()
    SliceSerlizer()
    FloatSerlize()
}
Map序列化後={"name":"孫悟空","age":500,"birthday":"2011-11-11","sal":8000,"skill":"如意七十二變"}
monster序列化後={"address":"火雲洞","age":10,"name":"牛魔王"}
切片序列化後=[{"address":"北京","age":"19","name":"TGH"},{"address":["華府","影視帝國"],"age":"18","name":"FCC"}]
基本數據類型序列化後=245.56

反序列化#

package main

import (
    "encoding/json"
    "fmt"
)

type Monster struct {
    Name     string  `json:"name"`
    Age      int     `json:"age"`
    Birthday string  `json:"birthday"`
    Sal      float64 `json:"sal"`
    Skill    string  `json:"skill"`
}

func unmarshalStruct() {
    str := "{\"name\":\"孫悟空\",\"age\":500,\"birthday\":\"2011-11-11\",\"sal\":8000,\"skill\":\"如意七十二變\"}"
    var monster Monster
    err := json.Unmarshal([]byte(str), &monster)
    if err != nil {
        fmt.Printf("反序列化失敗err:%v\n", err)
    }
    fmt.Printf("反序列化後monster:%v\n", monster)
}

func unmarshallMap() {
    str := "{\"address\":\"火雲洞\",\"age\":10,\"name\":\"牛魔王\"}"
    var a map[string]interface{}
    err := json.Unmarshal([]byte(str), &a)
    if err != nil {
        fmt.Printf("反序列化失敗err:%v\n", err)
    }
    fmt.Printf("反序列化Map後:%v\n", a)
}

func unmarshalSlice() {
    str := "[{\"address\":\"北京\",\"age\":\"19\",\"name\":\"TGH\"}," +
        "{\"address\":[\"華府\",\"影視帝國\"],\"age\":\"18\",\"name\":\"FCC\"}]"
    var slice []map[string]interface{}
    err := json.Unmarshal([]byte(str), &slice)
    if err != nil {
        fmt.Printf("反序列化失敗err:%v\n", err)
    }
    fmt.Printf("反序列化Slice後:%v\n", slice)
}

func main() {
    unmarshalStruct()
    unmarshallMap()
    unmarshalSlice()
}

輸出結果:

反序列化後monster:{孫悟空 500 2011-11-11 8000 如意七十二變} 
反序列化Map後:map[address:火雲洞 age:10 name:牛魔王] 
反序列化Slice後:[map[address:北京 age:19 name:TGH] map[address:[華府 影視帝國] age:18 name:FCC]]

Go-HTTP 包的使用#

Web 是基於 http 協議的一個服務,Go 語言裡面提供了一個完善的 net/http 包,通過 http 包可以很方便的搭建起來一個可以運行的 Web 服務。同時使用這個包能很簡單地對 Web 的路由,靜態文件,模版,cookie 等數據進行設置和操作。

http 包建立 Web 服務器#

package main
import (
    "fmt"
    "net/http"
    "strings"
    "log"
)
func sayhelloName(w http.ResponseWriter, r *http.Request) {
    r.ParseForm()  //解析參數,默認是不會解析的
    fmt.Println(r.Form)  //這些信息是輸出到服務器端的打印信息
    fmt.Println("path", r.URL.Path)
    fmt.Println("scheme", r.URL.Scheme)
    fmt.Println(r.Form["url_long"])
    for k, v := range r.Form {
        fmt.Println("key:", k)
        fmt.Println("val:", strings.Join(v, ""))
    }
    fmt.Fprintf(w, "Hello golang!") //這個寫入到w的是輸出到客戶端的
}
func main() {
    http.HandleFunc("/", sayhelloName) //設置訪問的路由
    err := http.ListenAndServe(":8080", nil) //設置監聽的端口
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

上面這個代碼,build 之後,然後執行 web.exe, 這個時候其實已經在 8080 端口監聽 http 鏈接請求了。

在瀏覽器輸入http://localhost:8080

可以看到瀏覽器頁面輸出了Hello golang!

瀏覽器輸入地址:

http://localhost:8080/?url_long=var1&url_long=var2

可以看看瀏覽器輸出的什麼

看到上面的代碼,要編寫一個 Web 服務器很簡單,只要調用 http 包的兩個函數就可以了。

使用 http 包請求頁面#

package main

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

func main() {
    request, err := http.NewRequest(http.MethodGet, "http://www.imooc.com", nil)
    if err != nil {
        panic(err)
    }
    request.Header.Add("User-Agent",
        "Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A372 Safari/604.1")

    client := http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            fmt.Println("Redirect:", req)
            return nil
        },
    }

    resp, err := client.Do(request)
    //resp, err := http.DefaultClient.Do(request)
    //resp, err := http.Get("http://www.imooc.com")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    s, err := httputil.DumpResponse(resp, true)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(s))
}

程序運行打印出 HTML 內容

img

net/http 包的坑 ——i/o timeout#

問題#

來看一段日常代碼。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net"
    "net/http"
    "time"
)

var tr *http.Transport

func init() {
    tr = &http.Transport{
        MaxIdleConns: 100,
        Dial: func(netw, addr string) (net.Conn, error) {
            conn, err := net.DialTimeout(netw, addr, time.Second*2) //設置建立連接超時
            if err != nil {
                return nil, err
            }
            err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設置發送接受數據超時
            if err != nil {
                return nil, err
            }
            return conn, nil
        },
    }
}

func main() {
    for {
        _, err := Get("http://www.baidu.com/")
        if err != nil {
            fmt.Println(err)
            break
        }
    }
}


func Get(url string) ([]byte, error) {
    m := make(map[string]interface{})
    data, err := json.Marshal(m)
    if err != nil {
        return nil, err
    }
    body := bytes.NewReader(data)
    req, _ := http.NewRequest("Get", url, body)
    req.Header.Add("content-type", "application/json")

    client := &http.Client{
        Transport: tr,
    }
    res, err := client.Do(req)
    if res != nil {
        defer res.Body.Close()
    }
    if err != nil {
        return nil, err
    }
    resBody, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    return resBody, nil
}

做的事情,比較簡單,就是循環去請求http://www.baidu.com/, 然後等待響應。

看上去貌似沒啥問題吧。

代碼跑起來,也確實能正常收發消息。

但是這段代碼跑一段時間,就會出現 i/o timeout 的報錯。

這其實是最近排查了的一個問題,發現這個坑可能比較容易踩上,這邊對代碼做了簡化。

實際生產中發生的現象是,golang 服務在發起 http 調用時,雖然http.Transport設置了 3s 超時,會偶發出現 i/o timeout 的報錯。

但是查看下游服務的時候,發現下游服務其實 100ms 就已經返回了。

排查#

img

五層網絡協議對應的消息體變化分析

就很奇怪了,明明服務端顯示處理耗時才 100ms,且客戶端超時設的是 3s, 怎麼就出現超时报錯 i/o timeout 呢?

這裡推測有兩個可能。

  • 因為服務端打印的日誌其實只是服務端應用層打印的日誌。但客戶端應用層發出數據後,中間還經過客戶端的傳輸層,網絡層,數據鏈路層和物理層,再經過服務端的物理層,數據鏈路層,網絡層,傳輸層到服務端的應用層。服務端應用層處耗時 100ms,再原路返回。那剩下的 3s-100ms 可能是耗在了整個流程裡的各個層上。比如網絡不好的情況下,傳輸層 TCP 使勁丟包重傳之類的原因。
  • 網絡沒問題,客戶端到服務端鏈路整個收發流程大概耗時就是 100ms 左右。客戶端處理邏輯問題導致超時。

一般遇到問題,大部分情況下都不會是底層網絡的問題,大膽懷疑是自己的問題就對了,不死心就抓個包看下。

img

抓包結果

分析下,從剛開始三次握手(畫了紅框的地方)。

到最後出現超时报錯 i/o timeout(畫了藍框的地方)。

從 time 那一列從 7 到 10,確實間隔 3s。而且看右下角的藍框,是 51169 端口發到 80 端口的一次 Reset 連接。

80 端口是服務端的端口。換句話說就是客戶端 3s 超時主動斷開鏈接的。

但是再仔細看下第一行三次握手到最後客戶端超時主動斷開連接的中間,其實有非常多次 HTTP 請求。

回去看代碼設置超時的方式。

tr = &http.Transport{
    MaxIdleConns: 100,
    Dial: func(netw, addr string) (net.Conn, error) {
        conn, err := net.DialTimeout(netw, addr, time.Second*2) //設置建立連接超時
        if err != nil {
            return nil, err
        }
        err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設置發送接受數據超時
        if err != nil {
            return nil, err
        }
        return conn, nil
    },
}

也就是說,這裡的 3s 超時,其實是在建立連接之後開始算的,而不是單次調用開始算的超時。

看注釋裡寫的是

SetDeadline sets the read and write deadlines associated with theconnection.

超時原因#

大家知道 HTTP 是應用層協議,傳輸層用的是 TCP 協議。

HTTP 協議從 1.0 以前,默認用的是短連接,每次發起請求都會建立 TCP 連接。收發數據。然後斷開連接。

TCP 連接每次都是三次握手。每次斷開都要四次揮手。

其實沒必要每次都建立新連接,建立的連接不斷開就好了,每次發送數據都復用就好了。

於是乎,HTTP 協議從 1.1 之後就默認使用長連接。具體相關信息可以看之前的這篇文章

那麼 golang 標準庫裡也兼容這種實現。

通過建立一個連接池,針對每個域名建立一個 TCP 長連接,比如http://baidu.com 和 http://golang.com 就是兩個不同的域名。

第一次訪問http://baidu.com 域名的時候會建立一個連接,用完之後放到空閒連接池裡,下次再要訪問 http://baidu.com 的時候會重新從連接池裡把這個連接捞出來復用。

img

復用長連接

為什麼要強調是同一個域名:一個域名會建立一個連接,一個連接對應一個讀 goroutine 和一個寫 goroutine。正因為是同一個域名,所以最後才會泄漏 3 個 goroutine,如果不同域名的話,那就會泄漏 1+2*N 個協程,N 就是域名數。

假設第一次請求要 100ms,每次請求完http://baidu.com 後都放入連接池中,下次繼續復用,重複 29 次,耗時 2900ms。

第 30 次請求的時候,連接從建立開始到服務返回前就已經用了 3000ms,剛好到設置的 3s 超時閾值,那麼此時客戶端就會報超時錯誤。

雖然這時候服務端其實才花了 100ms,但耐不住前面 29 次加起來的耗時已經很長。

也就是說只要通過http.Transport設置了err = conn.SetDeadline(time.Now().Add(time.Second * 3)),並且用了長連接,哪怕服務端處理再快,客戶端設置的超時再長,總有一刻,程序會報超時錯誤。

正確姿勢#

原本預期是給每次調用設置一個超時,而不是給整個連接設置超時。

另外,上面出現問題的原因是給長連接設置了超時,且長連接會復用。

基於這兩點,改一下代碼。

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "time"
)

var tr *http.Transport

func init() {
    tr = &http.Transport{
        MaxIdleConns: 100,
        // 下面的代碼被幹掉了
        //Dial: func(netw, addr string) (net.Conn, error) {
        //  conn, err := net.DialTimeout(netw, addr, time.Second*2) //設置建立連接超時
        //  if err != nil {
        //      return nil, err
        //  }
        //  err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設置發送接受數據超時
        //  if err != nil {
        //      return nil, err
        //  }
        //  return conn, nil
        //},
    }
}


func Get(url string) ([]byte, error) {
    m := make(map[string]interface{})
    data, err := json.Marshal(m)
    if err != nil {
        return nil, err
    }
    body := bytes.NewReader(data)
    req, _ := http.NewRequest("Get", url, body)
    req.Header.Add("content-type", "application/json")

    client := &http.Client{
        Transport: tr,
        Timeout: 3*time.Second,  // 超時加在這裡,是每次調用的超時
    }
    res, err := client.Do(req) 
    if res != nil {
        defer res.Body.Close()
    }
    if err != nil {
        return nil, err
    }
    resBody, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    return resBody, nil
}

func main() {
    for {
        _, err := Get("http://www.baidu.com/")
        if err != nil {
            fmt.Println(err)
            break
        }
    }
}

看注釋會發現,改動的點有兩個

  • http.Transport裡的建立連接時的一些超時設置幹掉了。
  • 在發起 http 請求的時候會場景http.Client,此時加入超時設置,這裡的超時就可以理解為單次請求的超時了。同樣可以看下注釋
Timeout specifies a time limit forrequestsmade by this Client.

到這裡,代碼就改好了,實際生產中問題也就解決了。

實例代碼裡,如果拿去跑的話,其實還會下面的錯

Get http://www.baidu.com/: EOF

這個是因為調用得太猛了,http://www.baidu.com 那邊主動斷開的連接,可以理解為一個限流措施,目的是為了保護服務器,畢竟每個人都像這麼搞,服務器是會炸的。。。

解決方案很簡單,每次 HTTP 調用中間加個 sleep 間隔時間就好。

到這裡,其實問題已經解決了,下面會在源碼層面分析出現問題的原因。

源碼分析#

用的 go 版本是 1.12.7。

從發起一個網絡請求開始跟。

res, err := client.Do(req)
func (c *Client) Do(req *Request) (*Response, error) {
    return c.do(req)
}

func (c *Client) do(req *Request) {
    // ...
    if resp, didTimeout, err = c.send(req, deadline); err != nil {
        // ...
    }
    // ...  
}  
func send(ireq *Request, rt RoundTripper, deadline time.Time) {
    // ...    
    resp, err = rt.RoundTrip(req)
    // ...  
} 

// 從這裡進入 RoundTrip 邏輯
/src/net/http/roundtrip.go: 16
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    return t.roundTrip(req)
}

func (t *Transport) roundTrip(req *Request) (*Response, error) {
    // 嘗試去獲取一個空閒連接,用於發起 http 連接
    pconn, err := t.getConn(treq, cm)
    // ...
}

// 重点关注这个函数,返回是一个长连接
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (*persistConn, error) {
    // 省略了大量逻輯,只關注下面兩點
    // 有空閒連接就返回
    pc := <-t.getIdleConnCh(cm)

    // 沒有創建連接
    pc, err := t.dialConn(ctx, cm)

}

當第一次發起一個 http 請求時,這時候肯定沒有空閒連接,會建立一個新連接。同時會創建一個讀 goroutine 和一個寫 goroutine。

img

讀寫協程

注意上面代碼裡的t.dial(ctx, "tcp", cm.addr()),如果像文章開頭那樣設置了http.Transport

Dial: func(netw, addr string) (net.Conn, error) {
    conn, err := net.DialTimeout(netw, addr, time.Second*2) //設置建立連接超時
    if err != nil {
        return nil, err
    }
    err = conn.SetDeadline(time.Now().Add(time.Second * 3)) //設置發送接受數據超時
    if err != nil {
        return nil, err
    }
    return conn, nil
},

那麼這裡就會在下面的 dial 裡被執行到

func (t *Transport) dial(ctx context.Context, network, addr string) (net.Conn, error) {
    // ...
    c, err := t.Dial(network, addr)
    // ...
}

這裡面調用的設置超時,會執行到

// /src/net/net.go
func (c *conn) SetDeadline(t time.Time) error {
    //...
    c.fd.SetDeadline(t)
    //...
}

//...

func setDeadlineImpl(fd *FD, t time.Time, mode int) error {
    // ...
    runtime_pollSetDeadline(fd.pd.runtimeCtx, d, mode)
    return nil
}

//go:linkname poll_runtime_pollSetDeadline internal/poll.runtime_pollSetDeadline
func poll_runtime_pollSetDeadline(pd *pollDesc, d int64, mode int) {
    // ...
    // 設置一個定時器事件
    rtf = netpollDeadline
    // 並將事件註冊到定時器裡
    modtimer(&pd.rt, pd.rd, 0, rtf, pd, pd.rseq)
}  

上面的源碼,簡單來說就是,當第一次調用請求的,會建立個連接,這時候還會註冊一個定時器事件,假設時間設了 3s,那麼這個事件會在 3s 後發生,然後執行註冊事件的邏輯。而這個註冊事件就是netpollDeadline。注意這個netpollDeadline,待會會提到。

img

讀寫協程定時器事件

設置了超時事件,且超時事件是 3s 後之後,發生。再次期間正常收發數據。一切如常。

直到 3s 過後,這時候看讀 goroutine,會等待網絡數據返回。

// /src/net/http/transport.go:1642
func (pc *persistConn) readLoop() {
    //...
    for alive {
        _, err := pc.br.Peek(1)  // 阻塞讀取服務端返回的數據
    //...
}

然後就是一直跟代碼。

src/bufio/bufio.go: 129
func (b *Reader) Peek(n int) ([]byte, error) {
    // ...
    b.fill() 
    // ...   
}

func (b *Reader) fill() {
    // ...
    n, err := b.rd.Read(b.buf[b.w:])
    // ...
}

/src/net/http/transport.go: 1517
func (pc *persistConn) Read(p []byte) (n int, err error) {
    // ...
    n, err = pc.conn.Read(p)
    // ...
}

func (fd *netFD) Read(p []byte) (n int, error) {
    n, err = fd.pfd.Read(p)
    // ...
}

func (fd *FD) Read(p []byte) (int, error) {
    n, err := fd.pd.Read(p)
    // ...
}

func (pd *pollDesc) waitRead(isFile bool) error {
    return pd.wait('r', isFile)
}

func (pd *pollDesc) wait(mode int, isFile bool) error {
    // ...
    res := runtime_pollWait(pd.runtimeCtx, mode)
    return convertErr(res, isFile)
}

直到跟到runtime_pollWait,這個可以簡單認為是等待服務端數據返回。

//go:linkname poll_runtime_pollWait internal/poll.runtime_pollWait
func poll_runtime_pollWait(pd *pollDesc, mode int) int {

    // 1.如果網絡正常返回數據就跳出
  for !netpollblock(pd, int32(mode), false) {
    // 2.如果有出錯情況也跳出
        err = netpollcheckerr(pd, int32(mode))
        if err != 0 {
            return err
        }
    }
    return 0
}

整條鏈路跟下來,就是會一直等待數據,等待的結果只有兩個

  • 有可以讀的數據
  • 出現報錯

這裡面的報錯,又有那麼兩種

  • 連接關閉
  • 超時
func netpollcheckerr(pd *pollDesc, mode int32) int {
    if pd.closing {
        return 1 // errClosing
    }
    if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
        return 2 // errTimeout
    }
    return 0
}

其中提到的超時,就是指這裡面返回的數字2,會通過下面的函數,轉化為ErrTimeout, 而ErrTimeout.Error()其實就是 i/o timeout。

func convertErr(res int, isFile bool) error {
    switch res {
    case 0:
        return nil
    case 1:
        return errClosing(isFile)
    case 2:
        return ErrTimeout // ErrTimeout.Error() 就是 "i/o timeout"
    }
    println("unreachable: ", res)
    panic("unreachable")
}

那麼問題來了。上面返回的超時錯誤,也就是返回 2 的時候的條件是怎麼滿足的?

if (mode == 'r' && pd.rd < 0) || (mode == 'w' && pd.wd < 0) {
    return 2 // errTimeout
}

還記得剛剛提到的 netpollDeadline 嗎?

這裡面放了定時器 3s 到點時執行的邏輯。

func timerproc(tb *timersBucket) {
    // 計時器到設定時間點了,觸發之前註冊函數
    f(arg, seq) // 之前註冊的是 netpollDeadline
}

func netpollDeadline(arg interface{}, seq uintptr) {
    netpolldeadlineimpl(arg.(*pollDesc), seq, true, true)
}

func netpolldeadlineimpl(pd *pollDesc, seq uintptr, read, write bool) {
    //...
    if read {
        pd.rd = -1
        rg = netpollunblock(pd, 'r', false)
    }
    //...
}

這裡會設置pd.rd=-1,是指poller descriptor.read deadline,含義網絡輪詢器文件描述符的讀超時, 在 linux 裡萬物皆文件,這裡的文件其實是指這次網絡通訊中使用到的 socket。

這時候再回去看發生超時的條件就是if (mode == 'r' && pd.rd < 0)

至此。代碼裡就收到了 io timeout 的報錯。

總結#

  • 不要在http.Transport中設置超時,那是連接的超時,不是請求的超時。否則可能會出現莫名 io timeout 報錯。
  • 請求的超時在創建client裡設置。

Go 中 Unicode 相關包#

Go 中 Unicode 相關包#

Go 語言讓複雜的編碼問題變得簡單很多,極大的減輕了程序員的心智負擔。為了方便對 unicode 字符串進行處理,Go 語言標準庫提供三個包:unicode、unicode/utf8 和 unicode/utf16。

這裡簡單介紹下三個包的功能:

  • unicode:unicode 提供數據和函數來測試 Unicode 代碼點(Code Point,用 rune 存儲)的某些屬性。

  • unicode/utf8:用於處理 UTF-8 編碼的文本,提供一些常量和函數,包括在 rune(碼點) 和 UTF-8 字節序列之間的轉換。

  • unicode/utf16:函數比較少,主要是 UTF-16 序列的編碼和解碼。

Go 中字符串的寫法。

在 Go 語言中,字符串字面值有 4 種寫法,比如「徐新华」可以這麼寫:

s1 := "徐新华"
s2 := "\u5F90\u65B0\u534E"
s3 := "\U00005F90\U000065B0\U0000534E"
s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"

簡單來生活就是 \u 緊跟四個十六進制數,\U 緊跟八個十六進制數。其中 \u 或 \U 代表後面是 Unicode 碼點。而 \x 緊跟兩個十六進制數,這些十六進制不是 Unicode 碼點,而是 UTF-8 編碼。

下面的代碼有利於理解:

package main
import (
    "fmt"
    "unicode/utf8"
)
func main() {
    s := `徐新华`
    var (
        buf = make([]byte, 4)
        n   int
    )
    fmt.Println("字符\tUnicode碼點\tUTF-8編碼十六進制\tUTF-8編碼二進制")
    for _, r := range s {
        n = utf8.EncodeRune(buf, r)
        fmt.Printf("%q\t%U\t\t%X\t\t%b\n", r, r, buf[:n], buf[:n])
    }

    s2 := "\u5F90\u65B0\u534E"
    s3 := "\U00005F90\U000065B0\U0000534E"
    s4 := "\xe5\xbe\x90\xe6\x96\xb0\xe5\x8d\x8e"

    fmt.Println(s2)
    fmt.Println(s3)
    fmt.Println(s4)
}

運行結果:

字符 Unicode碼點 UTF-8編碼十六進制 UTF-8編碼二進制
'' U+5F90  E5BE
載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。