Go–发起HTTP请求
一、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) }