《Go語言入門經(jīng)典》19~21章讀書筆記

第19章創(chuàng)建HTTP客戶端

19.2 發(fā)出GET請(qǐng)求

Go語言在net/http包中提供了一個(gè)快捷方法,可用于發(fā)出簡(jiǎn)單的GET請(qǐng)求。使用這個(gè)方法意味著不需要考慮如何配置HTTP客戶端以及如何設(shè)置請(qǐng)求報(bào)頭。如果只是要從遠(yuǎn)程網(wǎng)站獲取一些數(shù)據(jù),那么默認(rèn)配置完全夠用。

package main

import (
    "net/http"
        "fmt"
        "io/ioutil"
        "log"
)

func main(){
    response, err := http.Get("https://ifconfig.io/")
    if (err != nil){
        log.Fatal(err)
    }

    defer response.Body.Close()
    body, err := ioutil.ReadAll(response.Body)
    if err != nil{
        log.Fatal(err)
    }

    fmt.Printf("%s", body)
}

19.3 發(fā)出POST請(qǐng)求

標(biāo)準(zhǔn)庫中的net/http包也提供了用于發(fā)出簡(jiǎn)單POST請(qǐng)求的快捷方法——Post,它支持設(shè)置內(nèi)容類型以及發(fā)送數(shù)據(jù)。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "strings"
)

func main() {
    postData := strings.NewReader(`{"some":"json"}`)
    response, err := http.Post("https://httpbin.org/post", "application/json", postData)
    if err != nil {
        log.Fatal(err)
    }

    defer response.Body.Close()
    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", body)
}

19.4 進(jìn)一步控制HTTP請(qǐng)求

要進(jìn)一步控制HTTP請(qǐng)求,應(yīng)使用自定義的HTTP客戶端。您可使用net/http包提供的默認(rèn)HTTP客戶端,但這將自動(dòng)使用默認(rèn)設(shè)置,除非您手工修改這些設(shè)置。下例使用的是設(shè)置為默認(rèn)的自定義HTTP客戶端。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
)

func main() {
    client := &http.Client{}
    request, err := http.NewRequest("GET", "https://ifconfig.co", nil)
    if err != nil {
        log.Fatal(err)
    }

    response, err := client.Do(request)
    defer response.Body.Close()

    body, err := ioutil.ReadAll(response.Body)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%s", body)
}

對(duì)為使用自定義HTTP客戶端所做的修改解讀如下。

  • 不使用net/http包的快捷方法Get,而創(chuàng)建一個(gè)HTTP客戶端。
  • 使用方法NewRequest向https://ifconfig.co發(fā)出GET請(qǐng)求。
  • 使用方法Do發(fā)送請(qǐng)求并處理響應(yīng)。

使用自定義HTTP客戶端意味著可對(duì)請(qǐng)求設(shè)置報(bào)頭、基本身份驗(yàn)證和cookies。鑒于使用快捷方法和自定義HTTP客戶端時(shí),發(fā)出請(qǐng)求所需代碼的差別很小,建議除非要完成的任務(wù)非常簡(jiǎn)單,否則都使用自定義HTTP客戶端。

19.5 調(diào)試HTTP請(qǐng)求

創(chuàng)建HTTP客戶端時(shí),了解收發(fā)請(qǐng)求和響應(yīng)的報(bào)頭和數(shù)據(jù)對(duì)整個(gè)流程很有用。為此,可使用標(biāo)準(zhǔn)庫中的fmt包來輸出各項(xiàng)數(shù)據(jù),但net/http/httputil也提供了能夠讓您輕松調(diào)試HTTP客戶端和服務(wù)器的方法。這個(gè)包中的方法DumpRequestOut和DumpResponse能夠讓您查看請(qǐng)求和響應(yīng)。

可在調(diào)試時(shí)添加這些方法,并在調(diào)試完畢后刪除它們,但還有一種選擇,那就是使用環(huán)境變量來開關(guān)調(diào)試。標(biāo)準(zhǔn)庫中的os包支持讀取環(huán)境變量,這能夠讓您輕松地開關(guān)調(diào)試。

獲取環(huán)境變量

os.Getevn(變量名)

輸出請(qǐng)求

debugRequest, err := httputil.DumpRequestOut(request, true)
fmt.Printf("%s", debugRequest)

得到類似如下的數(shù)據(jù)

GET / HTTP/1.1
Host: ifconfig.co
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip

輸出響應(yīng)

debugResponse, err := httputil.DumpResponse(response, true)
fmt.Printf("%s", debugResponse)

響應(yīng)中包含response header

19.6 處理超時(shí)

HTTP事務(wù)會(huì)為接收響應(yīng)等待一定的時(shí)間。客戶端向服務(wù)器發(fā)送請(qǐng)求后,完全無法知道響應(yīng)會(huì)在多長(zhǎng)時(shí)間內(nèi)返回。在底層,有大量影響響應(yīng)速度的變數(shù)。

  • DNS查找速度。
  • 打開到服務(wù)器IP地址的TCP套接字的速度。
  • 建立TCP連接的速度。
  • TLS握手的速度(如果連接是TLS的)。
  • 向服務(wù)器發(fā)送數(shù)據(jù)的速度。
  • 重定向的速度。
  • Web服務(wù)器返回響應(yīng)的速度。
  • 將數(shù)據(jù)傳輸?shù)娇蛻舳说乃俣取?/li>
import(
    "net/http"
    "time"
)

client := &http.Client{
    Timeout: 50 * time.Microsecond
}

上述配置要求客戶端在50ms內(nèi)完成請(qǐng)求。

通過創(chuàng)建一個(gè)傳輸(transport)并將其傳遞給客戶端,可更細(xì)致地控制超時(shí):控制HTTP連接的各個(gè)階段。在大多數(shù)情況下,使用Timeout就足以控制整個(gè)HTTP事務(wù),但在Go語言中,還可通過創(chuàng)建傳輸來控制HTTP事務(wù)的各個(gè)部分。

import (
    "net"
    "net/http"
    "time"
)
    tr := &http.Transport{
        DialContext: (&net.Dialer{
            Timeout:   30 * time.Second,
            KeepAlive: 30 * time.Second,
        }).DialContext,
        TLSHandshakeTimeout:   10 * time.Second,
        IdleConnTimeout:       90 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
    
    client := &http.Client{
        Transport: tr,
    }

19.8 問與答

問:能夠同時(shí)發(fā)出多個(gè)HTTP請(qǐng)求嗎?

答:可以。通過使用goroutine,客戶端可同時(shí)發(fā)出多個(gè)HTTP請(qǐng)求。

問:能夠根據(jù)返回HTTP狀態(tài)碼調(diào)整程序采取的措施嗎?

答:可以??赏ㄟ^Response.StatusCode來訪問響應(yīng)的狀態(tài)碼,因此可編寫基于服務(wù)器響應(yīng)的邏輯。

第20章處理JSON

20.4 解碼JSON

JSON解碼也是一種常見的網(wǎng)絡(luò)編程任務(wù)。收到的數(shù)據(jù)可能來自數(shù)據(jù)庫、API調(diào)用或配置文件。原始JSON就是文本格式的數(shù)據(jù),在Go語言中可表示為字符串。函數(shù)Unmarshal接受一個(gè)字節(jié)切片以及一個(gè)指定要將數(shù)據(jù)解碼為何種格式的接口。根據(jù)數(shù)據(jù)是如何收到的,它可能是字節(jié)切片,也可能不是。如果不是字節(jié)切片,就必須先進(jìn)行轉(zhuǎn)換,再將其傳遞給函數(shù)Unmarshal。

    jsonStringData := `{"name":"George", "age":40, "hobbies":["Cycling", "Cheese"]}`
    //轉(zhuǎn)為字節(jié)切片
    jsonByteData := []byte(jsonStringData)

與將數(shù)據(jù)編碼為JSON格式一樣,必須定義一個(gè)接口,以指定要將數(shù)據(jù)解碼為何種格式。與將數(shù)據(jù)編碼為JSON格式一樣,可使用結(jié)構(gòu)體標(biāo)簽來告訴解碼器如何將鍵映射到字段。

type Person struct {
    Name    string   `json:"name"`
    Age     int      `json:"Age"`
    Hobbies []string `json:"hobbies"`
}

下例演示了如何將JSON字符串?dāng)?shù)據(jù)轉(zhuǎn)換為字節(jié)切片,再使用json.Unmarshal進(jìn)行解碼。

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Person struct {
    Name    string   `json:"name"`
    Age     int      `json:"Age"`
    Hobbies []string `json:"hobbies"`
}

func main() {
    jsonStringData := `{"name":"George", "age":40, "hobbies":["Cycling", "Cheese"]}`
    jsonByteData := []byte(jsonStringData)

    p := Person{}
    err := json.Unmarshal(jsonByteData, &p)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", p)
}

結(jié)果

{Name:George Age:40 Hobbies:[Cycling Cheese]}

20.5 映射數(shù)據(jù)類型

JSON數(shù)據(jù)類型不會(huì)自動(dòng)映射到Go語言中的數(shù)據(jù)類型,因此encoding/json包執(zhí)行顯式的數(shù)據(jù)類型轉(zhuǎn)換。下表顯示了JSON數(shù)據(jù)類型和Go數(shù)據(jù)類型之間的對(duì)應(yīng)關(guān)系。

JSON GO
Boolean bool
Number float64
String string
Array []interface{}
Object map[string]interface{}
Null nil

創(chuàng)建用于編碼和解碼JSON的結(jié)構(gòu)體時(shí),必須對(duì)上述數(shù)據(jù)類型的對(duì)應(yīng)關(guān)系做到心中有數(shù),因?yàn)槿绻麛?shù)據(jù)類型不匹配,encoding/ json包將引發(fā)錯(cuò)誤。

下列一個(gè)將JSON字符串解碼為結(jié)構(gòu)體的示例,您認(rèn)為結(jié)果將如何呢?

package main

import (
    "encoding/json"
    "fmt"
    "log"
)

type Switch struct {
    On bool `json:"on"`
}

func main() {
    jsonStringData := `{"on":"true"}`
    jsonByteData := []byte(jsonStringData)

    s := Switch{}
    err := json.Unmarshal(jsonByteData, &s)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", s)
}

如果您運(yùn)行這個(gè)示例,將出現(xiàn)錯(cuò)誤,因?yàn)樵贘SON中,值true實(shí)際上是一個(gè)字符串,因?yàn)樗环旁谝?hào)內(nèi)。Go解碼器試圖將這個(gè)值轉(zhuǎn)換為Go布爾值,但由于這是一個(gè)字符串,這種轉(zhuǎn)換是不可能的,因此進(jìn)而引發(fā)致命錯(cuò)誤。

json: cannot unmarshal string into Go struct field Switch.on of type bool

20.6 處理通過HTTP收到的JSON

在Go語言中,通過HTTP請(qǐng)求獲取JSON時(shí),收到的數(shù)據(jù)為流而不是字符串或字節(jié)切片。

由于獲取的數(shù)據(jù)為流,因此可使用encoding/json包中的函數(shù)NewDecoder。這個(gè)函數(shù)接受一個(gè)io.Reader(這正是http.Get返回的類型),并返回一個(gè)Decoder。通過對(duì)返回的Decoder調(diào)用方法Decode,可將數(shù)據(jù)解碼為結(jié)構(gòu)體。與以前一樣,Decode也接受一個(gè)結(jié)構(gòu)體,因此必須創(chuàng)建一個(gè)結(jié)構(gòu)體實(shí)例,并將其作為參數(shù)傳遞給Decode。下面是一個(gè)完整的示例,將獲取的數(shù)據(jù)解碼為一個(gè)Go結(jié)構(gòu)體。與以前一樣,必要時(shí)可使用結(jié)構(gòu)體標(biāo)簽將JSON響應(yīng)中的字段映射到結(jié)構(gòu)體字段。

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
)

type User struct {
    Name string `json:"name"`
    Blog string `json:"blog"`
}

func main() {
    var u User
    res, err := http.Get("https://api.github.com/users/shapeshed")
    if err != nil {
        log.Fatal(err)
    }

    defer res.Body.Close()

    err = json.NewDecoder(res.Body).Decode(&u)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("%+v\n", u)
}

20.9 作業(yè)

必須將JSON對(duì)象中的所有字段都解碼到結(jié)構(gòu)體中嗎?

不是這樣的,可定義只包含您感興趣的字段的結(jié)構(gòu)體。您可使用結(jié)構(gòu)體標(biāo)簽來將JSON字段映射到Go結(jié)構(gòu)體字段。

如果一個(gè)結(jié)構(gòu)體字段可能為空,那么該使用哪個(gè)結(jié)構(gòu)體標(biāo)簽?在這種情況下,如果該字段確實(shí)為空,結(jié)果將如何呢?

如果一個(gè)字段可能為空,應(yīng)給它添加結(jié)構(gòu)體標(biāo)簽omitempty。這樣解碼時(shí),如果該字段確實(shí)為空,將忽略它。

第21章處理文件

21.2 使用ioutil包讀寫文件

21.2.1 讀取文件

讀取文件是最常見的操作之一。ioutil包提供了函數(shù)Readfile,您可使用它來完成這項(xiàng)任務(wù),這個(gè)函數(shù)將一個(gè)文件名作為參數(shù),并以字節(jié)切片的方式返回文件的內(nèi)容。這意味著如果要將文件內(nèi)容作為字符串使用,則必須將返回的字節(jié)切片轉(zhuǎn)換為字符串。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
)

func main() {
    fileBytes, err := ioutil.ReadFile("demo.txt")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(fileBytes)

    fileString := string(fileBytes)
    fmt.Println(fileString)
}

對(duì)程序清單21.1解讀如下。

  • 使用ioutil包中的函數(shù)Readfile讀取文件。
  • 這個(gè)函數(shù)返回一個(gè)字節(jié)切片。
  • 將返回的字節(jié)切片轉(zhuǎn)換為字符串。
  • 將字符串打印到終端,以顯示文件的內(nèi)容。

21.2.2 創(chuàng)建文件

ioutil包還提供了用于創(chuàng)建文件的便利函數(shù)WriteFile。這個(gè)函數(shù)設(shè)計(jì)用于將數(shù)據(jù)寫入文件,但也可使用它來創(chuàng)建文件。函數(shù)WriteFile接受一個(gè)文件名、要寫入文件的數(shù)據(jù)以及應(yīng)用于文件的權(quán)限。

符號(hào)表示法是數(shù)字表示法的視覺表示。符號(hào)表示法總共包含10個(gè)字符。最左邊的字符指出了文件是普通文件、目錄還是其他東西,如果這個(gè)字符為-,就表示文件為普通文件;接下來的3個(gè)字符指定了文件所有者的權(quán)限;再接下來的3個(gè)字符表示所有者所在用戶組的權(quán)限;而最后3個(gè)字符表示其他人的權(quán)限。

在UNIX型系統(tǒng)中,文件的默認(rèn)權(quán)限為0644,即所有者能夠讀取和寫入,而其他人只能讀取。

package main

import (
    "io/ioutil"
    "log"
)

func main() {
    b := make([]byte, 0)
    err := ioutil.WriteFile("demo.txt", b, 0644)
    if err != nil {
        log.Fatal(err)
    }
}

解讀如下。

  • 函數(shù)WriteFile接受一個(gè)字節(jié)切片,因此創(chuàng)建一個(gè)空字節(jié)切片,并將其賦給變量b。
  • 調(diào)用函數(shù)WriteFile,并向它傳遞文件名、空字節(jié)切片以及要給文件設(shè)置的權(quán)限。
  • 如果沒有錯(cuò)誤,將創(chuàng)建指定的文件。

這里給函數(shù)WriteFile傳遞了空字節(jié)切片,這是一種使用ioutil包中便利函數(shù)的技巧。函數(shù)WriteFile在指定的文件不存在時(shí)創(chuàng)建它,因此也可使用這個(gè)函數(shù)來創(chuàng)建空文件。

21.3 寫入文件

正如您預(yù)期的,函數(shù)WriteFile也可用來寫入文件。要寫入文件,只需傳入一些值,而不是傳入空字節(jié)切片。要將字符串寫入文件,必須先將其轉(zhuǎn)換為字節(jié)切片。

s := "Hello World"
err := ioutil.WriteFile("demo.txt", []byte(s), 0644)

21.4 列出目錄的內(nèi)容

要處理文件系統(tǒng)中的文件,必須知道目錄結(jié)構(gòu)。為此,ioutil包提供了便利函數(shù)ReadDir,它接受以字符串方式指定的目錄名,并返回一個(gè)列表,其中包含按文件名排序的文件。文件名的類型為FileInfo,包含如下信息。

  • Name:文件的名稱。
  • Size:文件的長(zhǎng)度,單位為字節(jié)。
  • Mode:用二進(jìn)制位表示的權(quán)限。
  • ModTime:文件最后一個(gè)被修改的時(shí)間。
  • IsDir:文件是否是目錄。
  • Sys:底層數(shù)據(jù)源。

下面的代碼列出了目錄中文件的權(quán)限,文件名及大小。

package main

import (
    "fmt"
    "io/ioutil"
    "log"
)

func main() {
    files, err := ioutil.ReadDir(".")
    if err != nil {
        log.Fatal(err)
    }

    for _, file := range files {
        fmt.Println(file.Mode(), file.Name(), file.Size())
    }
}

21.5 復(fù)制文件

ioutil包可用于執(zhí)行一些常見的文件處理操作,但要執(zhí)行更復(fù)雜的操作,應(yīng)使用os包。os包運(yùn)行在稍低的層級(jí),因此使用它時(shí),必須手工關(guān)閉打開的文件。

要復(fù)制文件,只需結(jié)合使用os包中的幾個(gè)函數(shù)。以編程方式復(fù)制文件的步驟如下。
1.打開要復(fù)制的文件。
2.讀取其內(nèi)容。
3.創(chuàng)建并打開要將這些內(nèi)容復(fù)制到其中的文件。
4.將內(nèi)容寫入這個(gè)文件。
5.關(guān)閉所有已打開的文件。

package main

import (
    "io"
    "log"
    "os"
)

func main() {
    from, err := os.Open("demo.txt")
    if err != nil {
        log.Fatal(err)
    }
    defer from.Close()

    to, err := os.OpenFile("demo.copy.txt", os.O_RDWR|os.O_CREATE, 0666)
    if err != nil {
        log.Fatal(err)
    }

    _, err = io.Copy(to, from)
    if err != nil {
        log.Fatal(err)
    }

}

解讀如下。

  • 使用os包中的函數(shù)Open來讀取磁盤文件。
  • 使用defer語句在程序完成其他所有操作后關(guān)閉文件。
  • 使用函數(shù)OpenFile打開文件。第一個(gè)參數(shù)是要打開(如果不存在,就創(chuàng)建)的文件的名稱;第二個(gè)參數(shù)是用于文件的標(biāo)志,在這里指定的是讀寫文件,并在文件不存在時(shí)創(chuàng)建它;最后一個(gè)參數(shù)設(shè)置文件的權(quán)限。
  • 再次使用defer語句在執(zhí)行完其他操作后關(guān)閉文件。
  • 使用io包中的函數(shù)Copy復(fù)制源文件的內(nèi)容,并將其寫入目標(biāo)文件。

21.6 刪除文件

os包提供了函數(shù)Remove,它能夠?qū)⑽募蛭募A刪除。需要指出的是,使用這個(gè)函數(shù)時(shí),不會(huì)發(fā)出警告,您也無法將刪除的文件恢復(fù),因此務(wù)必要謹(jǐn)慎。

os.Remove("filename")
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容