HTTP 编程(一):客户端如何发起 HTTP 请求

通过 net.Dialnet.DialTimeout 函数来访问基于 HTTP 协议的网络服务是完全没有问题的,因为 HTTP 协议是基于 TCP/IP 协议栈的。不过没问题不代表很方便,如果通过 net.Dial 函数进行 HTTP 编程,HTTP 状态码、报文头部和实体部分处理起来是相当繁琐的(关于 HTTP 协议的更多细节可以阅读网络协议里的应用层协议来详细了解),因此 Go 语言标准库内置了 net/http 包来涵盖 HTTP 客户端和服务端的具体实现,通过 net/http 包我们可以更方便快捷地编写 HTTP 客户端和服务端程序。

学院君注:这里的 HTTP 客户端编程类似 PHP 里面使用 curl 或者 Guzzle 扩展包发起 HTTP 请求,HTTP 服务端编程类似实现 PHP 里面的 PHP-FPM 或者 Swoole HTTP 服务器对客户端请求进行响应。

首先我们来看 HTTP 客户端编程。

http.Client

net/http 包提供了最简洁的 HTTP 客户端实现,无需借助第三方网络通信库(比如 libcurl)就可以直接使用最常见的 GETPOST 方式发起 HTTP 请求。

具体来说,我们可以通过 net/http 包里面的 Client 类提供的如下方法发起 HTTP 请求:

func (c *Client) Do(req *Request) (*Response, error)
func (c *Client) Get(url string) (resp *Response, err error)
func (c *Client) Head(url string) (resp *Response, err error)
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error)
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)

下面学院君简单介绍下这几个方法的使用。

http.Get

示例代码

要发起一个 GET 请求,只需调用 http.Get() 方法并传入请求 URL 即可,示例代码如下:

resp, err := http.Get("https://xueyuanjun.com") 
if err != nil {
    fmt.Printf("发起请求失败:%v", err)
    return 
}

defer resp.Body.Close() 
io.Copy(os.Stdout, resp.Body)

上面这段代码用于对学院君首页发起请求,并将其网页内容打印到标准输出流中。

底层调用

其实通过 http.Get 发起请求时,默认调用的是上述 http.Client 缺省对象上的 Get 方法:

func Get(url string) (resp *Response, err error) {
    return DefaultClient.Get(url)
}

DefaultClient 默认指向的正是 http.Client 对象实例:

var DefaultClient = &Client{}

它是 net/http 包的一个公开属性,当我们在 http 上调用 GetPostPostFormHead 方法时,最终调用的都是该对象上的对应方法。

返回值

回到 http.Get() 方法本身,该方法返回值有两个,第一个是响应对象,第二个是 error 对象,如果请求过程中出现错误,则 error 对象不为空,否则,可以通过响应对象获取状态码、响应头、响应实体等信息,响应对象所属的类是 http.Response,你可以查看 API 文档或者源码了解该类型的具体信息,一般我们可以通过 resp.Body 获取响应实体,通过 resp.Header 获取响应头,通过 resp.StatusCode 获取响应状态码。

获取响应成功后记得调用 resp.Body 上的 Close 方法结束网络请求释放资源。

http.Post

要以 POST 的方式发送数据,也很简单,只需调用 http.Post() 方法并依次传递下面这 3 个参数即可:

  • 请求目标的 URL
  • POST 请求数据的资源类型(MIMEType)
  • 数据的比特流([]byte 形式)

下面的示例代码演示了如何上传用户头像:

resp, err := http.Post("https://xueyuanjun.com/avatar", "image/jpeg", &imageDataBuf) 
if err != nil {
    // 处理错误
    return 
}

if resp.StatusCode != http.StatusOK { 
    // 处理错误 
    return 
} 

// ...

底层实现及返回值和 http.Get 一样。

http.PostForm

http.PostForm() 方法实现了标准编码格式为 application/x-www-form-urlencoded 的 POST 表单提交。

下面的示例代码模拟 HTML 登录表单提交:

resp, err := http.PostForm("https://xueyuanjun.com/login", url.Values{"name":{"学院君"}, "password": {"test-passwd"}}) 

if err != nil {
    // 处理错误
    return 
} 

if resp.StatusCode != http.StatusOK { 
    // 处理错误 
    return 
} 

// ...

注意,POST 请求参数需要通过 url.Values 方法进行编码和封装。

底层实现及返回值和 http.Get 一样。

http.Head

HTTP 的 Head 请求表示只请求目标 URL 的响应头信息,不返回响应实体。我们可以通过 http.Head() 方法发起 Head 请求,该方法和 http.Get() 方法一样,只需传入目标 URL 参数即可。

下面的示例代码用于请求学院君首页的 HTTP 响应头信息:

resp, err := http.Head("https://xueyuanjun.com")
if err != nil {
    fmt.Println("Request Failed: ", err.Error())
    return
}

defer resp.Body.Close()
// 打印头信息
for  key, value := range resp.Header  {
    fmt.Println(key, ":", value)
}

通过 http.Head() 方法返回的响应实体 resp.Body 值为空。

底层实现及返回值和 http.Get 一样。

(*http.Client).Do

最后,我们来看一下 http.Client 类的 Do 方法。

在多数情况下,http.Gethttp.Posthttp.PostForm 就可以满足需求,但是如果我们发起的 HTTP 请求需要设置更多的自定义请求头信息,比如:

  • 设置自定义的 User-Agent,而不是默认的 Go http package
  • 传递 Cookie 信息;
  • 发起其它方式的 HTTP 请求,比如 PUTPATCHDELETE 等。

此时可以通过 http.Client 类提供的 Do() 方法来实现,使用该方法时,就不再是通过缺省的 DefaultClient 对象调用 http.Client 类中的方法了,而是需要我们手动实例化 Client 对象并传入添加了自定义请求头信息的请求对象来发起 HTTP 请求:

// 初始化客户端请求对象
req, err := http.NewRequest("GET", "https://xueyuanjun.com", nil)
if err != nil {
    fmt.Printf("请求初始化失败:%v", err)
    return
}
// 添加自定义请求头
req.Header.Add("Custom-Header", "Custom-Value")
// ... 其它请求头配置
client := &http.Client{
    // ... 设置客户端属性
}
resp, err := client.Do(req)
if err != nil {
    fmt.Printf("客户端发起请求失败:%v", err)
    return
}

defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)

用于初始化请求对象的 http.NewRequest 方法需要传入三个参数,第一个是请求方法,第二个是目标 URL,第三个是请求实体,只有 POST、PUT、DELETE 之类的请求才需要设置请求实体,对于 HEAD、GET 而言,传入 nil 即可。

http.NewRequest 方法返回的第一个值就是请求对象实例 req,该实例所属的类是 http.Request,你可以调用该类上的公开方法和属性对请求对象进行自定义配置,比如请求方法、URL、请求头等。

设置完成后,就可以将请求对象传入 client.Do() 方法发起 HTTP 请求,之后的操作和前面四个基本方法一样。

更多使用细节我们会在后续教程单独介绍,比如 Cookie 如何设置、文件如何上传和下载、请求/响应超时如何处理等,这里只是简单介绍这几个基本 HTTP 请求方法的使用。

上一篇: Socket 编程(二):Dial 函数的底层实现及超时处理

下一篇: HTTP 编程(二):http.Client 底层实现剖析