在开发过程中,无论是前端还是后端,都经常需要对第三方服务发起HTTP请求获取数据,本文列出一些代码示例用于参考,主要是 GET 请求 和 POST 请求。
Go 1.20
Windows 11
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))
}
众所周知,GET请求携带参数的方式是:http://www.mysite.com?key1=value1&key2=value2
如果参数含有特殊字符,则需要对其进行URL编码。在URL编码里有两种规范,一种是RFC 1738,另一种是RFC 3986
两种规范的编码方式会有区别,例如:对于空格字符,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一样
}
这种 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一样
}
假设现在要上传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,上传文件越大,越消耗内存。
在 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一样
}
以 GET 请求作为示例,添加两个自定义请求头(X-Csrf-Token
和X-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()
的区别:
Header在底层是个map[string][]string
类型,可以看出header的值是个切片,Add方法就是往切片里添加一个元素,而Set则是把整个已有的切片替换掉。
但无论切片里有多少个元素,Golang的Header.Get()
方法获取到的都是第一个元素。而PHP则刚好相反,我用$_SERVER['HTTP_XXXXX']
获取到的是最后一个元素。
细心的小伙伴可能会注意到,在上面的代码示例中,都有一行client := &http.Client{Timeout: 5 * time.Second}
,这是创建一个http client,用于发起请求。
其实net/http
包自身就包含有一个默认的http client(http.DefaultClient
),但这个 client 默认是没有设置超时时间的,这是很危险的做法,如果一旦遇到接口响应慢的情况,我们的代码可能就会卡在这里很久,像http.Get()
、http.Post()
、http.PostForm()
、http.DefaultClient.Do()
都是使用默认的client。
所以在上面的代码示例里,都没有使用默认的client,而是新建一个包含 5 秒超时时间设置的 client 来发起请求。
如果有多个接口需要请求,而且请求之间没有依赖关系的话,我们可以使用协程并发请求,相比一个一个请求能节省很多时间。下面以GET方式并发请求2个接口为例:
package main
import (
"fmt"
"io"
"net/http"
"sync"
"time"
)
func getRequest(api string) (respBody string, err error) {
client := &http.Client{Timeout: 5 * time.Second} // 5秒超时
resp, err := client.Get(api)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 判断HTTP状态码是否等于200
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("HTTP状态码异常:%v", resp.StatusCode)
}
// 读取HTTP Body
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
return string(body), nil
}
func main() {
timeStart := time.Now()
defer func() {
fmt.Println("执行时间:", time.Since(timeStart))
}()
// 要请求的接口
apis := [...]string{
"http://localhost/test.php",
"http://localhost/test2.php",
}
// 使用协程并发请求
wg := sync.WaitGroup{}
respBucket := make(map[string]string)
for _, api := range apis {
wg.Add(1)
go func(api string) {
defer wg.Done()
resp, err := getRequest(api)
if err != nil {
fmt.Println("请求失败:", err)
return
}
respBucket[api] = resp
}(api)
}
// 等待结果
wg.Wait()
fmt.Println("请求结果:", respBucket)
}
代码示例中,请求了2个接口,其中一个接口需耗时2秒,另一个需耗时4秒。使用协程并发请求,只花费了4秒时间,而不是6秒。