第19章创建HTTP客户端

19.2 发出GET请求

Go语言在net/http包中提供了一个快捷方法,可用于发出简单的GET请求。使用这个方法意味着不需要考虑如何配置HTTP客户端以及如何设置请求报头。如果只是要从远程网站获取一些数据,那么默认配置完全够用。

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 发出POST请求

标准库中的net/http包也提供了用于发出简单POST请求的快捷方法——Post,它支持设置内容类型以及发送数据。

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 进一步控制HTTP请求

要进一步控制HTTP请求,应使用自定义的HTTP客户端。您可使用net/http包提供的默认HTTP客户端,但这将自动使用默认设置,除非您手工修改这些设置。下例使用的是设置为默认的自定义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)
}

对为使用自定义HTTP客户端所做的修改解读如下。

  • 不使用net/http包的快捷方法Get,而创建一个HTTP客户端。
  • 使用方法NewRequest向https://ifconfig.co发出GET请求。
  • 使用方法Do发送请求并处理响应。

使用自定义HTTP客户端意味着可对请求设置报头、基本身份验证和cookies。鉴于使用快捷方法和自定义HTTP客户端时,发出请求所需代码的差别很小,建议除非要完成的任务非常简单,否则都使用自定义HTTP客户端。

19.5 调试HTTP请求

创建HTTP客户端时,了解收发请求和响应的报头和数据对整个流程很有用。为此,可使用标准库中的fmt包来输出各项数据,但net/http/httputil也提供了能够让您轻松调试HTTP客户端和服务器的方法。这个包中的方法DumpRequestOut和DumpResponse能够让您查看请求和响应。

可在调试时添加这些方法,并在调试完毕后删除它们,但还有一种选择,那就是使用环境变量来开关调试。标准库中的os包支持读取环境变量,这能够让您轻松地开关调试。

获取环境变量

os.Getevn(变量名)

输出请求

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

得到类似如下的数据

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

输出响应

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

响应中包含response header

19.6 处理超时

HTTP事务会为接收响应等待一定的时间。客户端向服务器发送请求后,完全无法知道响应会在多长时间内返回。在底层,有大量影响响应速度的变数。

  • DNS查找速度。
  • 打开到服务器IP地址的TCP套接字的速度。
  • 建立TCP连接的速度。
  • TLS握手的速度(如果连接是TLS的)。
  • 向服务器发送数据的速度。
  • 重定向的速度。
  • Web服务器返回响应的速度。
  • 将数据传输到客户端的速度。
import(
    "net/http"
	"time"
)

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

上述配置要求客户端在50ms内完成请求。

通过创建一个传输(transport)并将其传递给客户端,可更细致地控制超时:控制HTTP连接的各个阶段。在大多数情况下,使用Timeout就足以控制整个HTTP事务,但在Go语言中,还可通过创建传输来控制HTTP事务的各个部分。

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 问与答

问:能够同时发出多个HTTP请求吗?

答:可以。通过使用goroutine,客户端可同时发出多个HTTP请求。

问:能够根据返回HTTP状态码调整程序采取的措施吗?

答:可以。可通过Response.StatusCode来访问响应的状态码,因此可编写基于服务器响应的逻辑。

第20章处理JSON

20.4 解码JSON

JSON解码也是一种常见的网络编程任务。收到的数据可能来自数据库、API调用或配置文件。原始JSON就是文本格式的数据,在Go语言中可表示为字符串。函数Unmarshal接受一个字节切片以及一个指定要将数据解码为何种格式的接口。根据数据是如何收到的,它可能是字节切片,也可能不是。如果不是字节切片,就必须先进行转换,再将其传递给函数Unmarshal。

	jsonStringData := `{"name":"George", "age":40, "hobbies":["Cycling", "Cheese"]}`
	//转为字节切片
	jsonByteData := []byte(jsonStringData)

与将数据编码为JSON格式一样,必须定义一个接口,以指定要将数据解码为何种格式。与将数据编码为JSON格式一样,可使用结构体标签来告诉解码器如何将键映射到字段。

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

下例演示了如何将JSON字符串数据转换为字节切片,再使用json.Unmarshal进行解码。

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)
}

结果

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

20.5 映射数据类型

JSON数据类型不会自动映射到Go语言中的数据类型,因此encoding/json包执行显式的数据类型转换。下表显示了JSON数据类型和Go数据类型之间的对应关系。

JSON

GO

Boolean

bool

Number

float64

String

string

Array

[]interface{}

Object

map[string]interface{}

Null

nil

创建用于编码和解码JSON的结构体时,必须对上述数据类型的对应关系做到心中有数,因为如果数据类型不匹配,encoding/ json包将引发错误。

下列一个将JSON字符串解码为结构体的示例,您认为结果将如何呢?

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)
}

如果您运行这个示例,将出现错误,因为在JSON中,值true实际上是一个字符串,因为它被放在引号内。Go解码器试图将这个值转换为Go布尔值,但由于这是一个字符串,这种转换是不可能的,因此进而引发致命错误。

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

20.6 处理通过HTTP收到的JSON

在Go语言中,通过HTTP请求获取JSON时,收到的数据为流而不是字符串或字节切片。

由于获取的数据为流,因此可使用encoding/json包中的函数NewDecoder。这个函数接受一个io.Reader(这正是http.Get返回的类型),并返回一个Decoder。通过对返回的Decoder调用方法Decode,可将数据解码为结构体。与以前一样,Decode也接受一个结构体,因此必须创建一个结构体实例,并将其作为参数传递给Decode。下面是一个完整的示例,将获取的数据解码为一个Go结构体。与以前一样,必要时可使用结构体标签将JSON响应中的字段映射到结构体字段。

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 作业

必须将JSON对象中的所有字段都解码到结构体中吗?

不是这样的,可定义只包含您感兴趣的字段的结构体。您可使用结构体标签来将JSON字段映射到Go结构体字段。

如果一个结构体字段可能为空,那么该使用哪个结构体标签?在这种情况下,如果该字段确实为空,结果将如何呢?

如果一个字段可能为空,应给它添加结构体标签omitempty。这样解码时,如果该字段确实为空,将忽略它。

第21章处理文件

21.2 使用ioutil包读写文件

21.2.1 读取文件

读取文件是最常见的操作之一。ioutil包提供了函数Readfile,您可使用它来完成这项任务,这个函数将一个文件名作为参数,并以字节切片的方式返回文件的内容。这意味着如果要将文件内容作为字符串使用,则必须将返回的字节切片转换为字符串。

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)
}

对程序清单21.1解读如下。

  • 使用ioutil包中的函数Readfile读取文件。
  • 这个函数返回一个字节切片。
  • 将返回的字节切片转换为字符串。
  • 将字符串打印到终端,以显示文件的内容。

21.2.2 创建文件

ioutil包还提供了用于创建文件的便利函数WriteFile。这个函数设计用于将数据写入文件,但也可使用它来创建文件。函数WriteFile接受一个文件名、要写入文件的数据以及应用于文件的权限。

符号表示法是数字表示法的视觉表示。符号表示法总共包含10个字符。最左边的字符指出了文件是普通文件、目录还是其他东西,如果这个字符为-,就表示文件为普通文件;接下来的3个字符指定了文件所有者的权限;再接下来的3个字符表示所有者所在用户组的权限;而最后3个字符表示其他人的权限。

在UNIX型系统中,文件的默认权限为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)
	}
}

解读如下。

  • 函数WriteFile接受一个字节切片,因此创建一个空字节切片,并将其赋给变量b。
  • 调用函数WriteFile,并向它传递文件名、空字节切片以及要给文件设置的权限。
  • 如果没有错误,将创建指定的文件。

这里给函数WriteFile传递了空字节切片,这是一种使用ioutil包中便利函数的技巧。函数WriteFile在指定的文件不存在时创建它,因此也可使用这个函数来创建空文件。

21.3 写入文件

正如您预期的,函数WriteFile也可用来写入文件。要写入文件,只需传入一些值,而不是传入空字节切片。要将字符串写入文件,必须先将其转换为字节切片。

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

21.4 列出目录的内容

要处理文件系统中的文件,必须知道目录结构。为此,ioutil包提供了便利函数ReadDir,它接受以字符串方式指定的目录名,并返回一个列表,其中包含按文件名排序的文件。文件名的类型为FileInfo,包含如下信息。

  • Name:文件的名称。
  • Size:文件的长度,单位为字节。
  • Mode:用二进制位表示的权限。
  • ModTime:文件最后一个被修改的时间。
  • IsDir:文件是否是目录。
  • Sys:底层数据源。

下面的代码列出了目录中文件的权限,文件名及大小。

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 复制文件

ioutil包可用于执行一些常见的文件处理操作,但要执行更复杂的操作,应使用os包。os包运行在稍低的层级,因此使用它时,必须手工关闭打开的文件。

要复制文件,只需结合使用os包中的几个函数。以编程方式复制文件的步骤如下。 1.打开要复制的文件。 2.读取其内容。 3.创建并打开要将这些内容复制到其中的文件。 4.将内容写入这个文件。 5.关闭所有已打开的文件。

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包中的函数Open来读取磁盘文件。
  • 使用defer语句在程序完成其他所有操作后关闭文件。
  • 使用函数OpenFile打开文件。第一个参数是要打开(如果不存在,就创建)的文件的名称;第二个参数是用于文件的标志,在这里指定的是读写文件,并在文件不存在时创建它;最后一个参数设置文件的权限。
  • 再次使用defer语句在执行完其他操作后关闭文件。
  • 使用io包中的函数Copy复制源文件的内容,并将其写入目标文件。

21.6 删除文件

os包提供了函数Remove,它能够将文件或文件夹删除。需要指出的是,使用这个函数时,不会发出警告,您也无法将删除的文件恢复,因此务必要谨慎。

os.Remove("filename")