第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")