前言

在开发过程中,无论是前端还是后端,都经常需要对第三方服务发起HTTP请求获取数据,本文列出一些代码示例用于参考,主要是 GET 请求 和 POST 请求。

环境

Go 1.20
Windows 11

示例

1、GET请求,不带参数

package main

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

func main() {
	apiUrl := "http://localhost/test.php"
	client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
	resp, err := client.Get(apiUrl)
	if err != nil {
		log.Fatal("请求报错:", err)
	}
	defer resp.Body.Close()
	// 判断HTTP状态码是否等于200
	if resp.StatusCode != http.StatusOK {
		log.Fatal("HTTP状态码异常:", resp.StatusCode)
	}
	// 读取HTTP Body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal("读取响应体失败:", err)
	}

	fmt.Println("HTTP状态码:", resp.StatusCode)
	fmt.Println("HTTP响应头:", resp.Header) // 响应头是个map
	fmt.Println("HTTP响应体:", string(body))
}

2、GET请求,带参数

http://www.mysite.com?key1=value1&key2=value2

如果参数含有特殊字符,则需要对其进行URL编码。在URL编码里有两种规范,一种是RFC 1738,另一种是RFC 3986

+%20

RFC 1738编码代码示例:

package main

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

func main() {
	apiUrl := "http://localhost/test.php?%s"
	params := url.Values{}
	params.Add("key1", "Foo Bar")
	params.Add("key2", "中文参数")
	fullUrl := fmt.Sprintf(apiUrl, params.Encode())
	// fullUrl的值:
	// http://localhost/test.php?key1=Foo+Bar&key2=%E4%B8%AD%E6%96%87%E5%8F%82%E6%95%B0

	client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
	resp, err := client.Get(fullUrl)

	// 下面代码跟例子1一样
}

由于 net/url 包的 Encode 方法没有提供参数让我们选择用哪种规范进行编码,所以如果想使用RFC 3986方式编码的话需要另外想办法

RFC 3986编码代码示例:

package main

import (
	"fmt"
	"net/url"
	"strings"
)

func main() {
	params := map[string]string{
		"key1": "Foo Bar",
		"key2": "中文参数",
	}

	var kvPair []string
	for key, val := range params {
		kvPair = append(kvPair, key+"="+url.PathEscape(val))
	}
	queryString := strings.Join(kvPair, "&")

	apiUrl := "http://localhost/test.php?%s"
	fullUrl := fmt.Sprintf(apiUrl, queryString)
	// fullUrl的值:
	// http://localhost/test.php?key1=Foo%20Bar&key2=%E4%B8%AD%E6%96%87%E5%8F%82%E6%95%B0

    client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
	resp, err := client.Get(fullUrl)

	// 下面代码跟例子1一样
}

3、POST请求(application/x-www-form-urlencoded)

这种 POST 编码方式不支持上传文件,上传文件请看例子4

POST请求,传递 key1 和 key2 两个参数:

package main

import (
	"net/http"
	"net/url"
)

func main() {
	apiUrl := "http://localhost/test.php"

	params := url.Values{}
	params.Add("key1", "Hello World")
	params.Add("key2", "你好")
	client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
	resp, err := client.PostForm(apiUrl, params)

	// 下面代码跟例子1一样
}

4、POST请求(multipart/form-data)

假设现在要上传2个文件,和一个普通的字符串参数(key1),代码示例:

package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
)

func main() {
	buf := new(bytes.Buffer)
	mpWriter := multipart.NewWriter(buf)

	// 添加普通的字符串参数,非必须,如果只需上传文件可以注释掉
	// 普通字符串参数在PHP里是从 $_POST 获取
	err := mpWriter.WriteField("key1", "value1")
	if err != nil {
		log.Fatal("写入buffer报错:", err)
	}

	// 添加上传文件内容,filepath是文件的绝对路径
	// 上传文件内容在PHP里是从 $_FILES 获取
	fileBucket := [...]map[string]string{
		{"key": "file1", "filepath": "D:\\file1.txt"},
		{"key": "file2", "filepath": "D:\\file2.txt"},
	}
	for _, val := range fileBucket {
		file, err := os.Open(val["filepath"])
		if err != nil {
			log.Fatal("打开文件报错:", err)
		}
		defer file.Close()

		part, err := mpWriter.CreateFormFile(val["key"], filepath.Base(val["filepath"]))
		if err != nil {
			log.Fatal("创建上传文件字段失败:", err)
		}
		if _, err := io.Copy(part, file); err != nil {
			log.Fatal("复制文件内容失败:", err)
		}
	}
	mpWriter.Close()

	// 可以发送请求啦
	apiUrl := "http://localhost/test.php"
	client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
	resp, err := client.Post(apiUrl, mpWriter.FormDataContentType(), buf)

	// 下面代码跟例子1一样
}

注:此方法需要将待上传文件的数据全部读取到 bytes.Buffer,上传文件越大,越消耗内存。

5、POST请求(JSON)

在 POST 请求中,使用 JSON 来交互经常见,代码示例:

package main

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

type Teacher struct {
	ID        string `json:"id"`
	Firstname string `json:"firstname"`
	Lastname  string `json:"lastname"`
}

func main() {
	teacher := Teacher{
		ID:        "42",
		Firstname: "John",
		Lastname:  "Doe",
	}
	marshalled, err := json.Marshal(teacher)
	if err != nil {
		log.Fatal("转换JSON失败:", err)
	}

	// 发起请求
	apiUrl := "http://localhost/test.php"
	client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
	resp, err := client.Post(apiUrl, "application/json", bytes.NewReader(marshalled))

	// 下面代码跟例子1一样
}

进阶用法

1、自定义请求头

X-Csrf-TokenX-Request-ID
package main

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

func main() {
	apiUrl := "http://localhost/test.php"
	req, err := http.NewRequest(http.MethodGet, apiUrl, nil)
	if err != nil {
		log.Fatal("创建请求失败:", err)
	}
	req.Header.Add("X-Csrf-Token", "xxxxxx")
	req.Header.Add("X-Request-ID", "YYYYYY")

	// 可以发送请求啦
	client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
	resp, err := client.Do(req)

	// 下面代码跟例子1一样
}

Header.Add()Header.Set() map[string][]string
Header.Get()$_SERVER['HTTP_XXXXX']

2、超时设置

client := &http.Client{Timeout: 5 * time.Second}
net/httphttp.DefaultClienthttp.Get()http.Post()http.PostForm()http.DefaultClient.Do()

所以在上面的代码示例里,都没有使用默认的client,而是新建一个包含 5 秒超时时间设置的 client 来发起请求。

3、并发请求

如果有多个接口需要请求,而且请求之间没有依赖关系的话,我们可以使用协程并发请求,相比一个一个请求能节省很多时间。下面以GET方式并发请求2个接口为例:

代码示例中,请求了2个接口,其中一个接口需耗时2秒,另一个需耗时4秒。使用协程并发请求,只花费了4秒时间,而不是6秒。