前言
在开发过程中,无论是前端还是后端,都经常需要对第三方服务发起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秒。