【golang】请求HTTP接口代码示例

前言

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

众所周知,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一样
}

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、自定义请求头

以 GET 请求作为示例,添加两个自定义请求头(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() 的区别:
Header在底层是个map[string][]string类型,可以看出header的值是个切片,Add方法就是往切片里添加一个元素,而Set则是把整个已有的切片替换掉。

但无论切片里有多少个元素,Golang的Header.Get()方法获取到的都是第一个元素。而PHP则刚好相反,我用$_SERVER['HTTP_XXXXX']获取到的是最后一个元素。

2、超时设置

细心的小伙伴可能会注意到,在上面的代码示例中,都有一行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 来发起请求。

3、并发请求

如果有多个接口需要请求,而且请求之间没有依赖关系的话,我们可以使用协程并发请求,相比一个一个请求能节省很多时间。下面以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秒。

你可能感兴趣的:(Golang,golang,http)