Go–发起HTTP请求

作者: adm 分类: go 发布时间: 2024-05-01

一、HTTP请求
根据 HTTP 标准,HTTP 请求可以使用多种请求方法。在日常开发中大多数会用到 5 种请求方法: GET、POST、PUT、PATCH 和 DELETE

方法 描述

GET	请求指定的页面信息,并返回实体主体
POST	向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST 请求可能会导致新的资源的建立和/或已有资源的修改
PUT	从客户端向服务器传送的数据取代指定的文档的内容
DELETE	请求服务器删除指定的页面
PATCH	是对 PUT 方法的补充,用来对已知资源进行局部更新

在一次http请求(request)中可携带参数的地方:

部分 描述

URL部分	即以?和&分割的url参数
header部分	在http头中的字段,比如常用的cookie
content-body部分	请求正文,如果是get请求则为空;如果是post请求,则请求头中的Content-Type字段规定了content-body部分应该怎么被解析

二、GET请求
2.1 不带参数的GET请求

package main

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

func HttpGet(url string) []byte {
    //发起请求
    res, err := http.Get(url)
    if err != nil {
        fmt.Println("get请求失败:", err)
    }
    //延迟关闭(返回response的body尽量关闭,避免内存泄露)
    defer res.Body.Close()
    // 请求头
    fmt.Println(res.Header)
    // 请求相应码
    fmt.Println(res.Status)

    //读取body数据
    body, err := ioutil.ReadAll(res.Body)
    if err != nil {
        fmt.Println("读取body失败:", err)
        return nil
    }

    return body
}

func main() {
    UrlPath := "https://xxx/instance"
    body := HttpGet(UrlPath)
    //打印body,以字符串形式
    fmt.Printf(string(body))
    
}

2.2 带参数的GET请求
2.2.1 静态参数
若参数是固定的,则将1.1中的UrlPath设置为完整的请求即可,例: UrlPath := “https://xxx/instance?name=xxx&age=18”

2.2.2 动态参数

package main

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

func HttpGetUrl(url string) []byte {
    //正常来说,GET请求所有的参数都应该是放在url上,body请求体中是没有数据的
    //请求头可能会携带token之类的参数

    //新建一个GET请求
    request, err := http.NewRequest("GET", url, nil)
    if err != nil {
        panic(err)
    }

    //请求头部信息
    //Set时候,如果原来这一项已存在,后面的就修改已有的
    //Add时候,如果原本不存在,则添加,如果已存在,就不做任何修改
    //最终服务端获取的应该是token2
    request.Header.Set("User-Agent", "自定义浏览器1...")
    request.Header.Set("User-Agent", "自定义浏览器2...")
    request.Header.Add("Host", "www.xxx.com")

    //header:  map[User-Agent:[自定义浏览器2...]]
    request.Header.Add("name", "alnk")
    request.Header.Add("name", "alnk2")

    //header:  map[Name:[alnk alnk2] User-Agent:[自定义浏览器2...]]
    request.Header.Add("Authorization", "token1...") //token

    fmt.Println("header: ", request.Header)

    //url参数
    query := request.URL.Query()
    query.Add("id", "1")
    query.Add("id", "2")
    query.Add("name", "wan")
    request.URL.RawQuery = query.Encode()
    fmt.Println("request.URL: ", request.URL) //request.URL:  https://xxx/instance?id=1&id=2&name=wan

    //发送请求给服务端,实例化一个客户端
    client := &http.Client{}
    res, err := client.Do(request)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()

    //服务端返回数据
    b, err := ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    return b
}

func main() {
    UrlPath := "https://xxx/instance"
    body := HttpGetUrl(UrlPath)
    //打印body,以字符串形式
    fmt.Printf(string(body))

}

三、POST请求
3.1 Content-Type类型

Content-Type	用途
application/x-www-form-urlencoded	默认的方式, 比如 title=test&id=1010
multipart/form-data	如果你的请求里包含多个文件等二进制信息,则form 的 enctyped 需要等于这个值,浏览器生成一个 boundary 用于分割不同的字段
application/json	以json格式传送
raw	纯二进制格式,这需要服务器拿到消息后自己定义解析方式,比如protobuf

3.2 使用 http.Post() + application/x-www-form-urlencoded

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "strings"
)

func HttpPost(urlP string) []byte {
    //url.Values用来存放body参数,无参数不加
    urlValues := url.Values{} //urlValues ---> map[string][]string
    urlValues.Add("user", "test")
    urlValues.Add("password", "test123")
    //编码
    reqBody := urlValues.Encode()

    //最终http报文里面的body是 user=test&password=test123
    resp, err := http.Post(urlP, "application/x-www-form-urlencoded", strings.NewReader(reqBody))
    if err != nil {
        fmt.Println(err)
        return nil
    }

    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)

    return body
}

func main() {
    var urlP = "https://www.xxx.com/json"
    body := HttpPost(urlP)
    fmt.Println(string(body))
}

3.3 使用 http.Post() + application/json

package main

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

func HttpPost(urlP string) []byte {
    // 参数
    data := make(map[string]interface{})
    data["user"] = "test"
    data["password"] = "test123"

    // 序列化
    bytesData, _ := json.Marshal(data)
    
    resp, err := http.Post(urlP, "application/json", bytes.NewReader(bytesData))
    if err != nil {
        fmt.Println(err)
        return nil
    }

    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)

    return body
}

func main() {
    var urlP = "https://www.xxx.com/json"
    body := HttpPost(urlP)
    fmt.Println(string(body))
}

3.4 http.PostForm()
提交post表单

package main

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

func HttpPost(urlP string) []byte {
    urlValues := url.Values{}
    urlValues.Add("user", "test")
    urlValues.Add("password", "test123")

    resp, err := http.PostForm(urlP, urlValues)
    //resp, err := http.PostForm(urlP, url.Values{
    //    "user": {"test"},
    //    "password": {"test123"},
    //})
    if err != nil {
        fmt.Println(err)
        return nil
    }

    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)

    return body
}

func main() {
    var urlP = "https://www.xxx.com/json"
    body := HttpPost(urlP)
    fmt.Println(string(body))
}

四、http.NewRequest()
如2.2.2,复杂的http请求,可以通过http.NewRequest 来创建请求,再用client.Do发送,http.NewRequest支持各种请求方法

get请求如2.2.2

4.1 POST

package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "strings"
)

func HttpPost(urlP string) []byte {
    //请求body
    urlMap := url.Values{}
    urlMap.Add("user", "test")
    urlMap.Add("password", "test123")

    //新建请求
    request, err := http.NewRequest("POST", urlP, strings.NewReader(urlMap.Encode()))
    if err != nil {
        fmt.Println(err)
        return nil
    }
    fmt.Println("request.url: ", request.URL)
    fmt.Println("request.method: ", request.Method)

    //请求头部信息
    request.Header.Add("Authorization", "token1...")
    //post formdata表单请求
    request.Header.Add("Content-Type", "application/x-www-form-urlencoded")

    ////发送json格式的参数
    //data := map[string]interface{}{
    //    "user":     "test",
    //    "password": 123,
    //}
    //// 序列化
    //bytesData, _ := json.Marshal(data)
    //request, _ := http.NewRequest("POST", urlP, strings.NewReader(string(bytesData)))
    //请求头设置
    //request.Header.Add("Authorization", "token1...")       //token
    //request.Header.Add("Content-Type", "application/json") //json请求

    //实例化一个客户端
    client := &http.Client{}
    //发送请求到服务端
    resp, err := client.Do(request)
    if err != nil {
        fmt.Println(err)
        return nil
    }

    defer resp.Body.Close()
    body, _ := ioutil.ReadAll(resp.Body)

    return body
}

func main() {
    var urlP = "https://www.xxx.com/json"
    body := HttpPost(urlP)
    fmt.Println(string(body))
}

4.2 DELETE

package main

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

func HttpDelete(urlD string) []byte {
    //携带tonken
    //通过ID删除
    request, err := http.NewRequest("DELETE", urlD, nil)
    if err != nil {
        panic(err)
    }

    //请求头部信息
    request.Header.Add("Authorization", "token1...") //token

    //url参数
    query := request.URL.Query()
    query.Add("id", "1")
    query.Add("id", "2")
    query.Add("id", "3")
    request.URL.RawQuery = query.Encode()

    //发送请求给服务端
    client := &http.Client{}
    res, err := client.Do(request)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()

    //服务端返回数据
    b, err := ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }

    return b
}

func main() {
    var urlD = "https://www.xxx.com/json"
    body := HttpDelete(urlD)
    fmt.Println(string(body))
}

4.3 PUT

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"
)

func HttpPut(urlP string) []byte {
    //通过ID修改用户数据
    //数据格式化
    data := map[string]interface{}{
        "name": "wang",
        "age":  18,
    }
    dataStr, err := json.Marshal(data)
    if err != nil {
        panic(err)
    }

    //创建一个新的请求
    request, err := http.NewRequest("PUT", urlP, strings.NewReader(string(dataStr)))
    if err != nil {
        panic(err)
    }

    //请求头设置
    request.Header.Add("Authorization", "token1...")       //token
    request.Header.Add("Content-Type", "application/json") //json请求

    //url参数
    query := request.URL.Query()
    query.Add("id", "1")
    request.URL.RawQuery = query.Encode()

    //发送请求给服务端
    client := &http.Client{}
    res, err := client.Do(request)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()

    //服务端返回数据
    b, err := ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    return b
}

func main() {
    var urlP = "https://www.xxx.com/json"
    body := HttpPut(urlP)
    fmt.Println(string(body))
}

4.4 PATCH

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "strings"
)

func HttpPatch(urlP string) []byte {

    data := map[string]interface{}{
        "user":     "test",
        "password": 123,
    }
    dataStr, err := json.Marshal(data)
    if err != nil {
        panic(err)
    }

    //创建一个新的请求
    request, err := http.NewRequest("PATCH", urlP, strings.NewReader(string(dataStr)))
    if err != nil {
        panic(err)
    }

    //请求头设置
    request.Header.Add("Authorization", "token1...")       //token
    request.Header.Add("Content-Type", "application/json") //json请求
    

    //发送请求给服务端
    client := &http.Client{}
    res, err := client.Do(request)
    if err != nil {
        panic(err)
    }
    defer res.Body.Close()

    //服务端返回数据
    b, err := ioutil.ReadAll(res.Body)
    if err != nil {
        panic(err)
    }
    return b
}

func main() {
    var urlP = "https://www.xxx.com/json"
    body := HttpPatch(urlP)
    fmt.Println(string(body))
}

五、处理响应
不管用何种方式请求,最后都会得到 response,err ,不需要返回数据直接用_忽略,需要可将其直接打印出来,或者做进一步操作

5.1 直接打印

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

func main() {
    //发起一个get请求
    res, _ := http.Get("https://www.baidu.com")
    fmt.Println(res.StatusCode)     //获取状态码
    fmt.Println(res.Status)         //获取状态码及英文名称
    fmt.Println(res.Header)         //获取响应头
    body, _ := io.ReadAll(res.Body) //读取响应body,返回为[]byte,数据基本都存储在body里
    fmt.Printf(string(body))        //转换成json字符串进行打印

    // 将响应数据解析为JSON格式
    var jsonData map[string]interface{}
    err := json.Unmarshal(body, &jsonData)
    if err != nil {
        fmt.Println("解析JSON数据失败:", err)
        return
    }

    // 输出JSON数据
    fmt.Println(jsonData)
}

5.2 对返回数据做进一步操作(解析json类型的返回结果)
很多时候,请求一个接口,是需要返回数据(返回数据类型多为json)中的某些字段的数据,引用或变更,直接转换为字符串打印的话,就变成了一个整体,不便于数据拆解,故此对于返回数据还需另外处理

5.2.1 定义结构体
对于返回数据,可定义一个结构体(需与接口返回body的数据结构一致,不然会报错),然后使用 json.Unmarshal 函数将返回的json字符串解析为结构体类型的值,就可引用该结构体的字段来访问解析后的数据,如下:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"
)

// RequestData 定义请求接收的数据的结构体
type RequestData struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
    Age  int    `json:"age"`
    City string `json:"city"`
}

func main() {
    // 发起GET请求
    resp, err := http.Get("https://www.xxx.com/data")
    if err != nil {
        fmt.Println("发起请求时发生错误:", err)
        return
    }
    defer resp.Body.Close()

    // 读取响应体的内容
    body, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("读取响应体时发生错误:", err)
        return
    }

    // 解析响应体中的JSON数据到结构体
    var data RequestData
    err = json.Unmarshal(body, &data)
    if err != nil {
        fmt.Println("解析JSON数据时发生错误:", err)
        return
    }

    // 输出解析后的数据,或后续引用
    fmt.Println("ID:", data.ID)
    fmt.Println("Name:", data.Name)
    fmt.Println("Age:", data.Age)
    fmt.Println("City:", data.City)
    
    
    //循环
    //data1 := make(map[string]RequestData)
    //err = json.Unmarshal(body, &data)
    //if err != nil {
    //    fmt.Println("解析JSON数据时发生错误:", err)
    //    return
    //}
    //for _, v := range data1 {
    //    fmt.Println(v.ID)
    //    fmt.Println(v.Age)
    //}
}

5.2.2 gjson
对于一般场景,如5.1.1,定义结构体即可,但有时返回body结构太复杂,定义容易出错;或者返回数据太多,而真正需要的数据,可能只有几个字段,这时候定义个结构体去拆解所有的返回数据,就有点费时费力,且得不偿失了

故针对以上情景,可以使用gjson来处理返回body数据,方便快捷,不易出错

下载gjson: go get -u github.com/tidwall/gjson

使用:

传入 JSON 串和要读取的键路径,路径使用点号语法,如 “name.last “或 “age”。一旦找到值,就会立即返回。

路径是一系列用点分隔的键。键可以包含特殊的通配符 “*”和”? 要访问数组值,使用索引作为键。要获取数组中的元素个数或访问子路径,请使用 “#”字符。点和通配符可以用’\’转义。

func main() {
    //发送请求
    request, _ := http.NewRequest("POST", urlP, bytes.NewBuffer(bytesData))
    //实例化一个客户端
    client := &http.Client{}
    //发送请求到服务端
    resp, _ := client.Do(request)
    defer resp.Body.Close()
    //读取响应数据
    body, _ := io.ReadAll(resp.Body)
    //处理数据
    results := gjson.Get(string(body), "friends")       //string(body)将返回数据转换成json字符串,初始数据为friends数组
    results.ForEach(func(_, result gjson.Result) bool { //遍历friends数组,一般不需要键k,故用_忽略
        first1 := gjson.Get(result.Raw, "first")          //默认返回的是gjson.Result类型,如果是直接打印,可直接使用,若是需进一步操作,建立转换成对应类型
        first2 := gjson.Get(result.Raw, "first").String() //从 jsonStr 中获取 "name" 字段的值,然后通过调用 .String() 方法,将获取到的值转换为字符串类型,如果 "name" 字段的值不是字符串类型,它会被转换为字符串
        first3 := gjson.Get(result.Raw, "first").Str      //也是从 jsonStr 中获取 "name" 字段的值,但是是直接获取该字段的值,没有进行类型转换,如果"name"字段的值不是字符串类型,则将保持原来的类型
                                                         //所以如果确定 "name" 字段的值一定是字符串类型,那么使用 .Str 是更简洁的方式。如果不确定 "name" 字段的值是什么类型,或者想要确保它是一个字符串类型,那么使用 .String() 进行显式转换是更安全的选择。

        fmt.Println(first1, first2, first3)
        return true
    })
    //假设返回数据为:
    //{
    //    "name": {"first": "Tom", "last": "Anderson"},
    //    "age":37,
    //    "children": ["Sara","Alex","Jack"],
    //"friends": [
    //  {"first": "James", "last": "Murphy"},
    //  {"first": "Roger", "last": "Craig"}
    //  ]
    //}

    //对应键路径的值
    //"name.last"          >> "Anderson"
    //"age"                >> 37
    //"children"           >> ["Sara","Alex","Jack"]
    //"children.#"         >> 3
    //"children.1"         >> "Alex"
    //"child*.2"           >> "Jack"
    //"c?ildren.0"         >> "Sara"
    //"friends.#.first"    >> ["James","Roger"]
}

六、补充
6.1 ioutil.ReadAll 已弃用
从go 1.16及之后的版本,io/ioutil就已经被正式弃用。为了兼容性考虑,依旧可以正常调用这个包,只不过当调用ioutil里面的方法,最后都是跳转到了io以及os包。

因为ioutil.ReadAll 通过 slice 将数据流读到内存中,slice 会动态扩容,对于大文件的读取会导致内存不足,被 OOM kill,并且频繁的动态扩容也会导致时间消耗增加。

故:返回的数据量小时,可继续使用或用io.ReadAll,返回的数据量大或者不清楚数据大小时,使用io.Copy():直接从 src 读取数据,并写入到 dst,不会一次性读取全部数据,也不会频繁进行切片扩容。

详细情况请自行百度

package main

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

func main() {
    res, _ := http.Get("https://www.baidu.com")
    //body, _ := ioutil.ReadAll(res.Body)
    //body, _ := io.ReadAll(res.Body)
    body, _ := io.Copy(os.Stdout, res.Body)
    fmt.Printf(string(body))
}

6.2 较复杂的json参数
要传递的参数形式:

{
    "content": "test",
    "notifyParams": [
        {
            "type": "jobNos",
            "values": [
                "80xxxxxx",
                "W9xxxxxx",
                "O00xxxxxx"
            ]
        }
    ]
}

请求样例:

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

type ttNotice struct {
    Content      string         `json:"content"`
    NotifyParams []notifyParams `json:"notifyParams"`
}
type notifyParams struct {
    Type   string   `json:"type"`
    Values []string `json:"values"`
}

func main() {
    url := "http://xxx.com"

    notifyList := make([]notifyParams, 0) //实例化一个list:[{type: "",Values: ["",""]},{...}]
    var arrs notifyParams
    arrs.Type = "jobNos"
    for _, v := range arr { //arr为带有多个id的结构体
        arrs.Values = append(arrs.Values, v.SolverId)
    }
    //去除重复的id
    var arrLen int = len(arrs.Values) - 1
    for ; arrLen > 0; arrLen-- {
        //拿最后项与前面项的各项逐个(自后向前)进行比较
        for j := arrLen - 1; j >= 0; j-- {
            if arrs.Values[arrLen] == arrs.Values[j] { //例:len(arrs.Values)=5,则下标为0~4,arrs.Values[4] == arrs.Values[3]
                arrs.Values = append(arrs.Values[:arrLen], arrs.Values[arrLen+1:]...) //切片,arrs.Values[:4],arrs.Values[5:]
                break
            }
        }
    }

    notifyList = append(notifyList, arrs)
    notice := ttNotice{
        Content:      "test",
        NotifyParams: notifyList,
    }
    //序列化
    s1, err := json.Marshal(notice)
    if err != nil {
        fmt.Println(err)
    }
    //发送请求
    _, err = http.Post(url, "application/json", bytes.NewReader(s1))
    if err != nil {
        fmt.Println(err)
    }
}

6.2.1 切片

package main

import "encoding/json"

//传参格式
type str struct {
    Name string   `json:"name"`
    Id   []string `json:"id"`
}

func main() {
    s1 := []string{"2x101"}
    s2 := []string{"w41e2e", "52s5sf"}

    //传参
    data1 := str{
        Name: "wang",
        Id:   s1,
    }
    data2 := str{
        Name: "wang",
        Id:   s2[:1],
    }
    bytesData, _ := json.Marshal(datax)

    var s4 []string
    s4 = append(s4, "1024565V")
    s4 = append(s4, "1554200V")
    var str1 str
    str1.Name = "wang"
    str1.Id = s4
    bytesData, _ = json.Marshal(str1)

    //s3 := map[string]interface{}{
    //    "name": "wang",
    //    "id":   []string{"sf451x"},
    //}
    //bytesData, _ = json.Marshal(s3)
}

如果觉得我的文章对您有用,请随意赞赏。您的支持将鼓励我继续创作!