├── .gitignore ├── LICENSE ├── README.md ├── doc.go ├── example_test.go ├── go.mod ├── http_context.go ├── httpc.go ├── req_body_reader.go └── resp_body_reader.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 RecallSong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httpc简介 2 | httpc这是一个发起http请求的客户端库。 3 | 4 | 它具有的特色包括:简单易用、易于扩展、支持链式调用、支持多种请求和响应格式的处理等。 5 | 6 | 特别适合用来调用RESTful风格的接口。 7 | 8 | # 下载 9 | go get github.com/recallsong/httpc 10 | 11 | # Api文档 12 | 查看 [在线Api文档](https://godoc.org/github.com/recallsong/httpc) 13 | 14 | 我们也可以利用godoc工具在本地查看api文档: 15 | ``` 16 | godoc -http=:9090 17 | ``` 18 | 在浏览器中查看地址: 19 | 20 | http://localhost:9090/pkg/github.com/recallsong/httpc 21 | 22 | # 快速入门 23 | ## 最简单的使用方式 24 | ```go 25 | var resp string 26 | // GET http://localhost/hello?name=RecallSong 27 | err := httpc.New("http://localhost").Path("hello").Query("name", "RecallSong").Get(&resp) 28 | if err != nil { 29 | fmt.Println(resp) // 以字符串方式获取响应的数据 30 | } else { 31 | fmt.Println(err) 32 | } 33 | ``` 34 | ## 设置请求头和Cookie等 35 | ```go 36 | var resp string 37 | err := httpc.New("http://localhost").Path("/hello").Query("param", "value"). 38 | Header("MyHeader", "HeaderValue"). 39 | AddCookie(&http.Cookie{Name: "cookieName", Value: "cookieValue"}). 40 | Body("body data").Post(&resp) 41 | if err != nil { 42 | fmt.Println(resp) // 以字符串方式获取响应的数据 43 | } else { 44 | fmt.Println(err) 45 | } 46 | ``` 47 | ## 发送和接收json格式的数据 48 | ### 使用map传递数据 49 | ```go 50 | body := map[string]interface{}{ 51 | "name": "RecallSong", 52 | "age": 18, 53 | } 54 | var resp map[string]interface{} 55 | // 根据请求的Content-Type自动对数据进行转换 56 | err := httpc.New("http://localhost").Path("json"). 57 | ContentType(httpc.TypeApplicationJson). 58 | Body(body). // body转变为 {"name":"RecallSong","age":18} 59 | Post(&resp) // 根据响应中的Content-Type,将返回的数据解析到resp中 60 | fmt.Println(err, resp) 61 | 62 | // 如果请求或响应没有指定Content-Type,或是不正确,也可以强制指定转换格式类型 63 | err = httpc.New("http://localhost").Path("json"). 64 | Body(body, httpc.TypeApplicationJson). // body转变为 {"name":"RecallSong","age":18} 65 | Post(&resp, httpc.TypeApplicationJson) // 将返回的数据按json格式解析到map中 66 | fmt.Println(err, resp) 67 | ``` 68 | ### 使用struct传递数据 69 | ```go 70 | type Person struct { 71 | Name string `json:"name"` 72 | Age int `json:"age"` 73 | } 74 | body := Person{Name: "RecallSong", Age: 18} 75 | var resp Person 76 | err := httpc.New("http://localhost").Path("json"). 77 | Body(body, httpc.TypeApplicationJson). 78 | Post(&resp, httpc.TypeApplicationJson) 79 | fmt.Println(err, resp) 80 | ``` 81 | ## 发送和接收xml格式的数据 82 | ```go 83 | type Person struct { 84 | Name string `xml:"name"` 85 | Age int `xml:"age"` 86 | } 87 | body := Person{Name: "RecallSong", Age: 18} 88 | var resp Person 89 | err := httpc.New("http://localhost").Path("xml"). 90 | Body(body, httpc.TypeApplicationXml). // 数据转变为xml格式 91 | Post(&resp, httpc.TypeApplicationXml) 92 | fmt.Println(err, resp) 93 | ``` 94 | ## 发送表单参数 95 | ### 使用结构体发送 96 | ```go 97 | sbody := struct { 98 | Name string `form:"name"` 99 | Age int `form:"age"` 100 | }{ 101 | Name: "RecallSong", 102 | Age: 18, 103 | } 104 | var resp string 105 | err := httpc.New("http://localhost").Path("echo"). 106 | Body(sbody, httpc.TypeApplicationForm). // 将结构体转变为form格式的数据体 107 | Post(&resp) 108 | fmt.Println(err, resp) 109 | ``` 110 | ### 使用map发送 111 | ```go 112 | mbody := map[string]interface{}{ 113 | "name": "RecallSong", 114 | "age": 19, 115 | } 116 | var resp string 117 | err := httpc.New("http://localhost").Path("echo"). 118 | Body(mbody, httpc.TypeApplicationForm). // 将map变为form格式的数据体 119 | Post(&resp) 120 | fmt.Println(err, resp) 121 | ``` 122 | ### 使用url.Values发送 123 | ```go 124 | ubody := url.Values{} 125 | ubody.Set("name", "RecallSong") 126 | ubody.Set("age", "20") 127 | var resp string 128 | err := httpc.New("http://localhost").Path("echo"). 129 | Body(ubody). // 将url.Values类型转变form格式的数据体 130 | Post(&resp) 131 | fmt.Println(err, resp) 132 | ``` 133 | ## 自动编码url路径参数 134 | ```go 135 | var resp string 136 | // 可以自动编码url路径参数 137 | err := httpc.New("http://localhost").EscapedPath("recall/Song").EscapedPath(18).Get(&resp) 138 | // 请求地址为 http://localhost/recall%2FSong/18 139 | fmt.Println(err, resp) 140 | ``` 141 | ## 上传文件 142 | ### 方式1 143 | ```go 144 | file, err := os.Open("doc.go") 145 | if err != nil { 146 | fmt.Println(err) 147 | return 148 | } 149 | defer file.Close() 150 | body := map[string]interface{}{ 151 | "file": file, 152 | "name": "RecallSong", 153 | "age": 18, 154 | "file2": httpc.FilePath("doc.go:hello.go"), //上传doc.go文件,参数名为file2,文件名为hello.go 155 | } 156 | var resp string 157 | err = httpc.New("http://localhost").Path("echo"). 158 | Body(body, httpc.TypeMultipartFormData).Post(&resp) 159 | fmt.Println(err) 160 | ``` 161 | ### 方式2 162 | ```go 163 | file, err := os.Open("doc.go") 164 | if err != nil { 165 | fmt.Println(err) 166 | return 167 | } 168 | defer file.Close() 169 | body := struct { 170 | Name string `form:"name"` 171 | Address []string `form:"address"` 172 | Age int `form:"age"` 173 | File *os.File `form:"file" file:"hello.go"` 174 | File2 httpc.FilePath `form:"file2"` 175 | }{ 176 | Name: "RecallSong", 177 | Address: []string{"HangZhou", "WenZhou"}, 178 | Age: 18, 179 | File: file, 180 | File2: httpc.FilePath("doc.go:hello2.go"), //上传doc.go文件,参数名为file2,文件名为hello2.go 181 | } 182 | var resp string 183 | err = httpc.New("http://localhost").Path("echo"). 184 | Body(body, httpc.TypeMultipartFormData).Post(&resp) 185 | fmt.Println(err) 186 | ``` 187 | ## 接收响应数据 188 | ```go 189 | // 前面的例子我们知道了可以接收json和xml格式的数据,也可以接收数据到一个string变量中 190 | // 除此之外,我们还可以有一下几种方式接收数据 191 | 192 | // []byte 方式接收 193 | var bytesResp []byte 194 | err := httpc.New("http://localhost").Path("hello").Get(&bytesResp) 195 | fmt.Println(err, bytesResp) 196 | 197 | // *http.Response 方式接收 198 | var resp *http.Response 199 | err := httpc.New("http://localhost").Path("hello").Get(&resp) 200 | if err != nil { 201 | fmt.Println(err) 202 | } else { 203 | // 注意这种方式要关闭Body 204 | defer resp.Body.Close() 205 | body, err := ioutil.ReadAll(resp.Body) 206 | fmt.Println(err, string(body)) 207 | } 208 | ``` 209 | ## 下载文件 210 | ### 方式1 211 | ```go 212 | // 默认方式保存文件 213 | err := httpc.New("http://localhost").Path("echo").Body("content").Post(httpc.FilePath("download1.txt")) 214 | fmt.Println(err) 215 | ``` 216 | ### 方式2 217 | ```go 218 | err := httpc.New("http://localhost").Path("echo").Body("content").Post(&httpc.SaveInfo{ 219 | Path: "download2.txt", 220 | Override: true, 221 | Mode: 0777}) 222 | fmt.Println(err) 223 | ``` 224 | ## 指定成功的http状态码 225 | ```go 226 | // 如果返回的状态码与指定的状态码不匹配,则返回一个error 227 | err := httpc.New("http://localhost").Path("not_exist"). 228 | SuccessStatus(200).Get(nil) 229 | fmt.Println(err) 230 | // Output: 231 | // error http status 404 , expect 200 232 | ``` 233 | ## 请求上下文 234 | ```go 235 | // 请求上下文中包含了每次请求的设置、连接设置等,所有请求应该尽量共享Context 236 | // 我们可以设置回调通知的函数 237 | ctx := httpc.NewContext(). 238 | AddCbBeforeSend(func(client *httpc.HttpC, args ...interface{}) error { 239 | fmt.Println("before request") 240 | return nil 241 | }). 242 | AddCbAfterSend(func(client *httpc.HttpC, args ...interface{}) error { 243 | fmt.Println("after response") 244 | return nil 245 | }). 246 | AddCbOnError(func(client *httpc.HttpC, args ...interface{}) error { 247 | fmt.Println("on error") 248 | return nil 249 | }). 250 | SetConnectReadTimeout(30*time.Second, 30*time.Second) 251 | var resp string 252 | err := httpc.New("http://localhost").Path("hello").SetContext(ctx).Get(&resp) 253 | fmt.Println(err, resp) 254 | 255 | // 库默认生成了一个上下文实例 httpc.DefaultContext,它并没有加锁保护,所以尽量在所有请求前设置好它 256 | // 改变httpc.DefaultContext会影响所有未调用过SetContext的请求 257 | httpc.DefaultContext.SetConnectReadTimeout(30*time.Second, 30*time.Second) 258 | err = httpc.New("http://localhost").Path("hello").Get(&resp) 259 | fmt.Println(err, resp) 260 | ``` 261 | ## 超时设置 262 | ```go 263 | err := httpc.New("http://localhost").Path("timeout"). 264 | SetContext(httpc.NewContext().SetConnectReadTimeout(time.Second, time.Second)). 265 | Get(nil) 266 | fmt.Println(err) 267 | ``` 268 | ## 请求重试 269 | ```go 270 | err := httpc.New("http://not_exist/").Path("not_exist"). 271 | SetContext(httpc.NewContext().AddCbOnRetring(func(c *httpc.HttpC, args ...interface{}) error { 272 | fmt.Printf("retring %v, next interval %v\n", args[0], args[1]) 273 | return nil 274 | }).SetRetryConfig(3, time.Second, 2)). // 重试3次,重试时间间隔依次为:2s, 4s, 8s 275 | Get(nil) 276 | fmt.Println(err) 277 | 278 | // Output: 279 | // retring 1, next interval 2s 280 | // retring 2, next interval 4s 281 | // retring 3, next interval 8s 282 | // Get http://not_exist/not_exist: dial tcp: lookup not_exist: no such host 283 | ``` 284 | ## 自定义请求或响应处理器 285 | ```go 286 | // httpc库已经注册了一些通用的请求和响应处理器,但我们也可以额外添加处理器 287 | ctx := httpc.NewContext() 288 | ctx.BodyReaders = httpc.NewBodyReaders() 289 | ctx.BodyReaders.RespBodyTypeReaders[reflect.TypeOf((*int)(nil))] = func(resp *http.Response, reader io.ReadCloser, typ reflect.Type, out interface{}) error { 290 | output := out.(*int) 291 | *output = resp.StatusCode 292 | return nil 293 | } 294 | // 返回响应状态码 295 | var status int 296 | err := httpc.New("http://localhost").Path("hello"). 297 | SetContext(ctx). 298 | Get(&status) 299 | fmt.Println(err, status) 300 | // Output: 301 | // 200 302 | ``` 303 | ## 其他特性 304 | 请参考Api文档 305 | 306 | # License 307 | [MIT](https://github.com/recallsong/httpc/blob/master/LICENSE) 308 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Author: recallsong 3 | 4 | Email: songruiguo@qq.com 5 | 6 | Description: httpc是一个简单的发起http请求的客户端库 7 | */ 8 | package httpc 9 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package httpc_test 2 | 3 | // Author: recallsong 4 | // Email: songruiguo@qq.com 5 | 6 | import ( 7 | "bytes" 8 | "compress/gzip" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net/http" 13 | "net/http/httptest" 14 | "net/http/httputil" 15 | "net/url" 16 | "os" 17 | "reflect" 18 | "strings" 19 | "time" 20 | 21 | "github.com/recallsong/httpc" 22 | ) 23 | 24 | type Handler struct { 25 | Method string 26 | Func func(w http.ResponseWriter, r *http.Request) 27 | } 28 | 29 | var server *httptest.Server 30 | 31 | func startServer() string { 32 | server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 | if handler, ok := routers[r.URL.Path]; ok { 34 | if handler.Method != "" && handler.Method != r.Method { 35 | http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) 36 | return 37 | } 38 | handler.Func(w, r) 39 | } else { 40 | http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound) 41 | } 42 | })) 43 | return server.URL 44 | } 45 | 46 | func stopServer() { 47 | server.Close() 48 | } 49 | 50 | var routers = map[string]Handler{ 51 | "/hello": Handler{ 52 | Method: "GET", 53 | Func: func(w http.ResponseWriter, r *http.Request) { 54 | fmt.Fprintln(w, "Hello, "+r.URL.Query().Get("name")) 55 | }, 56 | }, 57 | "/dump": Handler{ 58 | Func: func(w http.ResponseWriter, r *http.Request) { 59 | bytes, err := httputil.DumpRequest(r, true) 60 | if err != nil { 61 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 62 | return 63 | } 64 | fmt.Fprintln(w, string(bytes)) 65 | }, 66 | }, 67 | "/echo": Handler{ 68 | Method: "POST", 69 | Func: func(w http.ResponseWriter, r *http.Request) { 70 | bytes, err := ioutil.ReadAll(r.Body) 71 | if err != nil { 72 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 73 | return 74 | } 75 | w.Write(bytes) 76 | }, 77 | }, 78 | "/json": Handler{ 79 | Method: "POST", 80 | Func: func(w http.ResponseWriter, r *http.Request) { 81 | bytes, err := ioutil.ReadAll(r.Body) 82 | if err != nil { 83 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 84 | return 85 | } 86 | w.Header().Add("Content-Type", "application/json; charset=UTF-8") 87 | w.Write(bytes) 88 | }, 89 | }, 90 | "/xml": Handler{ 91 | Method: "POST", 92 | Func: func(w http.ResponseWriter, r *http.Request) { 93 | bytes, err := ioutil.ReadAll(r.Body) 94 | if err != nil { 95 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 96 | return 97 | } 98 | w.Header().Add("Content-Type", "application/xml; charset=UTF-8") 99 | w.Write(bytes) 100 | }, 101 | }, 102 | "/error": Handler{ 103 | Func: func(w http.ResponseWriter, r *http.Request) { 104 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) 105 | }, 106 | }, 107 | "/timeout": Handler{ 108 | Func: func(w http.ResponseWriter, r *http.Request) { 109 | time.Sleep(2 * time.Second) 110 | fmt.Fprintln(w, "Hello") 111 | }, 112 | }, 113 | "/gzip": Handler{ 114 | Func: func(w http.ResponseWriter, r *http.Request) { 115 | var buffer bytes.Buffer 116 | gw := gzip.NewWriter(&buffer) 117 | fmt.Fprintln(gw, "Hello Hello Hello Hello") 118 | gw.Close() 119 | byt := buffer.Bytes() 120 | w.Header().Add("Content-Type", "text/html; charset=utf-8") 121 | w.Header().Add("Content-Length", fmt.Sprint(len(byt))) 122 | w.Header().Add("Content-Encoding", "gzip") 123 | w.Write(byt) 124 | }, 125 | }, 126 | } 127 | 128 | func ExampleHttpC_hello() { 129 | baseUrl := startServer() 130 | defer stopServer() 131 | var resp string 132 | // 请求 {baseUrl}/hello?name=RecallSong, 并将返回数据读入到resp变量 133 | err := httpc.New(baseUrl).Path("hello").Query("name", "RecallSong").Get(&resp) 134 | fmt.Println(err) 135 | fmt.Println(resp) 136 | // Output: 137 | // 138 | // Hello, RecallSong 139 | } 140 | 141 | func ExampleHttpC_response() { 142 | baseUrl := startServer() 143 | defer stopServer() 144 | var resp *http.Response 145 | // 请求 {baseUrl}/hello?name=RecallSong, 并将返回数据读入到resp变量 146 | err := httpc.New(baseUrl).Path("hello").Query("name", "RecallSong").Get(&resp) 147 | if err != nil { 148 | fmt.Println(err) 149 | } else { 150 | defer resp.Body.Close() 151 | body, err := ioutil.ReadAll(resp.Body) 152 | fmt.Println(resp.Status) 153 | fmt.Println(err, string(body)) 154 | } 155 | 156 | // Output: 157 | // 200 OK 158 | // Hello, RecallSong 159 | } 160 | 161 | func ExampleHttpC_dump() { 162 | baseUrl := startServer() 163 | defer stopServer() 164 | var resp []byte 165 | // 构建更复杂的请求 166 | err := httpc.New(baseUrl).Path("/dump").Query("param", "value"). 167 | Header("MyHeader", "HeaderValue"). 168 | AddCookie(&http.Cookie{Name: "cook", Value: "testcook"}). 169 | Body("body data").Post(&resp) 170 | fmt.Println(err) 171 | // fmt.Println(string(resp)) 172 | 173 | // Output: 174 | // 175 | } 176 | 177 | func ExampleHttpC_mapBody() { 178 | baseUrl := startServer() 179 | defer stopServer() 180 | body := map[string]interface{}{ 181 | "name": "RecallSong", 182 | "age": 18, 183 | } 184 | var resp map[string]interface{} 185 | // 根据请求的Content-Type自动对数据进行转换 186 | err := httpc.New(baseUrl).Path("json"). 187 | ContentType(httpc.TypeApplicationJson). 188 | Body(body). // body转变为 {"name":"RecallSong","age":18} 189 | Post(&resp) // 根据响应中的Content-Type,将返回的数据解析到resp中 190 | fmt.Println(err) 191 | fmt.Println(resp["name"], resp["age"]) 192 | 193 | // 如果请求或响应没有指定Content-Type,或是错误的Content-Type,也可以强制指定转换格式类型 194 | err = httpc.New(baseUrl).Path("json"). 195 | Body(body, httpc.TypeApplicationJson). // body转变为 {"name":"RecallSong","age":18} 196 | Post(&resp, httpc.TypeApplicationJson) // 将返回的数据按json格式解析到map中 197 | fmt.Println(err) 198 | fmt.Println(resp["name"], resp["age"]) 199 | // Output: 200 | // 201 | // RecallSong 18 202 | // 203 | // RecallSong 18 204 | } 205 | 206 | type Person struct { 207 | Name string `json:"name" form:"name" xml:"name"` 208 | Age int `json:"age" form:"age" xml:"age"` 209 | } 210 | 211 | func ExampleHttpC_structBody() { 212 | baseUrl := startServer() 213 | defer stopServer() 214 | body := Person{Name: "RecallSong", Age: 18} 215 | var resp Person 216 | // 可以使用结构体来传递数据 217 | err := httpc.New(baseUrl).Path("echo"). 218 | Body(body, httpc.TypeApplicationJson). 219 | Post(&resp, httpc.TypeApplicationJson) 220 | fmt.Println(err) 221 | fmt.Println(resp.Name, resp.Age) 222 | // Output: 223 | // 224 | // RecallSong 18 225 | } 226 | 227 | func ExampleHttpC_xmlBody() { 228 | baseUrl := startServer() 229 | defer stopServer() 230 | body := Person{Name: "RecallSong", Age: 18} 231 | var resp Person 232 | // 发送和接收xml数据 233 | err := httpc.New(baseUrl).Path("xml"). 234 | Body(body, httpc.TypeApplicationXml). // 数据转变为xml格式 235 | Post(&resp) 236 | fmt.Println(err) 237 | fmt.Println(resp) 238 | // Output: 239 | // 240 | // {RecallSong 18} 241 | } 242 | 243 | func ExampleHttpC_formBody() { 244 | baseUrl := startServer() 245 | defer stopServer() 246 | // struct body 247 | sbody := struct { 248 | Name string `form:"name"` 249 | Age int `form:"age"` 250 | }{ 251 | Name: "RecallSong", 252 | Age: 18, 253 | } 254 | var resp string 255 | // 发送form参数 256 | err := httpc.New(baseUrl).Path("echo"). 257 | Body(sbody, httpc.TypeApplicationForm). // 将结构体转变为form格式的数据体 258 | Post(&resp) 259 | fmt.Println(err) 260 | fmt.Println(resp) 261 | 262 | // map body 263 | mbody := map[string]interface{}{ 264 | "name": "RecallSong", 265 | "age": 19, 266 | } 267 | err = httpc.New(baseUrl).Path("echo"). 268 | Body(mbody, httpc.TypeApplicationForm). // 将map变为form格式的数据体 269 | Post(&resp) 270 | fmt.Println(err) 271 | fmt.Println(resp) 272 | 273 | // url.Values body 274 | ubody := url.Values{} 275 | ubody.Set("name", "RecallSong") 276 | ubody.Set("age", "20") 277 | err = httpc.New(baseUrl).Path("echo"). 278 | Body(ubody). // 将url.Values类型转变form格式的数据体 279 | Post(&resp) 280 | fmt.Println(err) 281 | fmt.Println(resp) 282 | // Output: 283 | // 284 | // age=18&name=RecallSong 285 | // 286 | // age=19&name=RecallSong 287 | // 288 | // age=20&name=RecallSong 289 | } 290 | 291 | func ExampleHttpC_error() { 292 | baseUrl := startServer() 293 | defer stopServer() 294 | err := httpc.New(baseUrl).Path("not_exist"). 295 | SetContext(httpc.NewContext().AddCbOnError(func(client *httpc.HttpC, args ...interface{}) error { 296 | fmt.Println("on error: ", client.Error) 297 | return nil 298 | })). 299 | SuccessStatus(200).Get(nil) 300 | fmt.Println(err) 301 | // Output: 302 | // on error: error http status 404 , expect 200 303 | // error http status 404 , expect 200 304 | } 305 | 306 | func ExampleHttpC_path() { 307 | baseUrl := startServer() 308 | defer stopServer() 309 | req := httpc.New(baseUrl).EscapedPath("recall/song").EscapedPath(18).DumpRequest() 310 | fmt.Println(strings.Contains(req, "/recall%2Fsong/18")) 311 | // Output: 312 | // true 313 | } 314 | 315 | func ExampleHttpC_context() { 316 | baseUrl := startServer() 317 | defer stopServer() 318 | ctx := httpc.NewContext(). 319 | AddCbBeforeSend(func(client *httpc.HttpC, args ...interface{}) error { 320 | // fmt.Println(client.DumpRequest()) 321 | fmt.Println("before request") 322 | return nil 323 | }). 324 | AddCbAfterSend(func(client *httpc.HttpC, args ...interface{}) error { 325 | // fmt.Println(client.DumpResponse()) 326 | fmt.Println("after response") 327 | return nil 328 | }). 329 | AddCbOnError(func(client *httpc.HttpC, args ...interface{}) error { 330 | // fmt.Println(client.Error) 331 | fmt.Println("on error") 332 | return nil 333 | }). 334 | SetConnectReadTimeout(30*time.Second, 30*time.Second) 335 | var resp string 336 | err := httpc.New(baseUrl).Path("hello").Query("name", "Song").SetContext(ctx).SuccessStatus(200).Get(&resp) 337 | fmt.Println(err, resp) 338 | // Output: 339 | // before request 340 | // after response 341 | // Hello, Song 342 | } 343 | 344 | func ExampleHttpC_mutipart_map() { 345 | baseUrl := startServer() 346 | defer stopServer() 347 | file, err := os.Open("doc.go") 348 | if err != nil { 349 | fmt.Println(err) 350 | return 351 | } 352 | defer file.Close() 353 | body := map[string]interface{}{ 354 | "file": file, 355 | "name": "RecallSong", 356 | "age": 18, 357 | "file2": httpc.FilePath("doc.go:hello.go"), //上传doc.go文件,参数名为file2,文件名为hello.go 358 | } 359 | var resp string 360 | err = httpc.New(baseUrl).Path("echo"). 361 | Body(body, httpc.TypeMultipartFormData).Post(&resp) 362 | fmt.Println(err) 363 | // fmt.Println(resp) 364 | 365 | // Output: 366 | // 367 | } 368 | 369 | func ExampleHttpC_mutipart_struct() { 370 | baseUrl := startServer() 371 | defer stopServer() 372 | file, err := os.Open("doc.go") 373 | if err != nil { 374 | fmt.Println(err) 375 | return 376 | } 377 | defer file.Close() 378 | body := struct { 379 | Name string `form:"name"` 380 | Address []string `form:"address"` 381 | Age int `form:"age"` 382 | File *os.File `form:"file" file:"hello.go"` 383 | File2 httpc.FilePath `form:"file2"` 384 | }{ 385 | Name: "RecallSong", 386 | Address: []string{"HangZhou", "WenZhou"}, 387 | Age: 18, 388 | File: file, 389 | File2: httpc.FilePath("doc.go:hello2.go"), //上传doc.go文件,参数名为file2,文件名为hello2.go 390 | } 391 | var resp string 392 | err = httpc.New(baseUrl).Path("echo"). 393 | Body(body, httpc.TypeMultipartFormData).Post(&resp) 394 | 395 | fmt.Println(err) 396 | // fmt.Println(resp) 397 | 398 | // Output: 399 | // 400 | } 401 | 402 | func ExampleHttpC_download() { 403 | baseUrl := startServer() 404 | defer stopServer() 405 | // 默认方式保存文件 406 | err := httpc.New(baseUrl).Body("xxx").Path("echo").Post(httpc.FilePath("download1.txt")) 407 | fmt.Println(err) 408 | _, err = os.Stat("download1.txt") 409 | if os.IsNotExist(err) { 410 | fmt.Println(err) 411 | } 412 | // 保存文件的另一种方式 413 | err = httpc.New(baseUrl).Body("zzz").Path("echo").Post(&httpc.SaveInfo{ 414 | Path: "download2.txt", 415 | Override: true, 416 | Mode: 0777}) 417 | fmt.Println(err) 418 | _, err = os.Stat("download2.txt") 419 | if os.IsNotExist(err) { 420 | fmt.Println(err) 421 | } 422 | } 423 | 424 | func ExampleHttpC_timeout() { 425 | baseUrl := startServer() 426 | defer stopServer() 427 | // 测试读数据超时 428 | err := httpc.New(baseUrl).Path("timeout"). 429 | SetContext(httpc.NewContext().SetConnectReadTimeout(time.Second, 1*time.Second)). 430 | Get(nil) 431 | fmt.Println(err != nil) 432 | // Output: 433 | // true 434 | } 435 | 436 | func ExampleHttpC_retry() { 437 | // 测试重试请求 438 | err := httpc.New("http://not_exist/").Path("not_exist"). 439 | SetContext(httpc.NewContext().AddCbOnRetring(func(c *httpc.HttpC, args ...interface{}) error { 440 | fmt.Printf("retring %v, next interval %v\n", args[0], args[1]) 441 | return nil 442 | }).SetRetryConfig(3, time.Second, 2)). 443 | Get(nil) 444 | fmt.Println(err) 445 | 446 | // Output: 447 | // retring 1, next interval 2s 448 | // retring 2, next interval 4s 449 | // retring 3, next interval 8s 450 | // Get http://not_exist/not_exist: dial tcp: lookup not_exist: no such host 451 | } 452 | 453 | func ExampleHttpC_gzip() { 454 | baseUrl := startServer() 455 | defer stopServer() 456 | // 测试重试请求 457 | var resp string 458 | err := httpc.New(baseUrl).Path("gzip").Get(&resp) 459 | fmt.Println(err) 460 | fmt.Println(resp) 461 | 462 | // Output: 463 | // 464 | // Hello Hello Hello Hello 465 | } 466 | 467 | func ExampleHttpC_body_reader() { 468 | baseUrl := startServer() 469 | defer stopServer() 470 | ctx := httpc.NewContext() 471 | ctx.BodyReaders = httpc.NewBodyReaders() 472 | ctx.BodyReaders.RespBodyTypeReaders[reflect.TypeOf((*int)(nil))] = func(resp *http.Response, reader io.ReadCloser, typ reflect.Type, out interface{}) error { 473 | output := out.(*int) 474 | *output = resp.StatusCode 475 | return nil 476 | } 477 | // 返回响应状态码 478 | var status int 479 | err := httpc.New(baseUrl).Path("hello"). 480 | SetContext(ctx). 481 | Get(&status) 482 | fmt.Println(err, status) 483 | 484 | // Output: 485 | // 200 486 | } 487 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/recallsong/httpc 2 | 3 | go 1.14 4 | -------------------------------------------------------------------------------- /http_context.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | // Author: recallsong 4 | // Email: songruiguo@qq.com 5 | 6 | import ( 7 | "crypto/tls" 8 | "net" 9 | "net/http" 10 | "net/http/cookiejar" 11 | "net/url" 12 | "reflect" 13 | "time" 14 | ) 15 | 16 | // DefaultContext 默认的请求上下文 17 | var DefaultContext = NewContext() 18 | 19 | // Callback 以回调方式通知内部所发生的事件 20 | type Callback func(c *HttpC, args ...interface{}) error 21 | 22 | // BodyReaders 请求体读取器 23 | type BodyReaders struct { 24 | ReqBodyTypeReaders map[reflect.Type]ReqBodyReader // 根据类型获取reader 25 | ReqBodyMediaReaders map[string]ReqBodyReader // 根据MediaType获取reader 26 | RespBodyTypeReaders map[reflect.Type]RespBodyReader // 根据类型获取reader 27 | RespBodyMediaReaders map[string]RespBodyReader // 根据MediaType获取reader 28 | } 29 | 30 | // NewBodyReaders 创建一个BodyReaders 31 | func NewBodyReaders() *BodyReaders { 32 | return &BodyReaders{ 33 | ReqBodyTypeReaders: make(map[reflect.Type]ReqBodyReader), 34 | ReqBodyMediaReaders: make(map[string]ReqBodyReader), 35 | RespBodyTypeReaders: make(map[reflect.Type]RespBodyReader), 36 | RespBodyMediaReaders: make(map[string]RespBodyReader), 37 | } 38 | } 39 | 40 | // Context http请求上下文,管理所有请求公用的对象 41 | type Context struct { 42 | Client *http.Client // 请求的客户端 43 | BodyReaders *BodyReaders // 获取请求和响应中的body的reader 44 | 45 | CbBeforeSend []Callback // 在发送请求前调用 46 | CbAfterSend []Callback // 在发送请求后调用 47 | CbOnError []Callback // 在发生错误时调用 48 | CbOnRetring []Callback // 在请求重试时调用 49 | Retries int // 重试次数,-1表示一直重试 50 | RetryInterval time.Duration // 重试间隔 51 | RetryFactor float64 // 重试因子,影响每次重试的时间间隔 52 | } 53 | 54 | // NewContext 创建一个Context实例 55 | func NewContext() *Context { 56 | return &Context{ 57 | Client: &http.Client{Transport: &http.Transport{}}, 58 | RetryFactor: 1, 59 | } 60 | } 61 | 62 | // AddCbBeforeSend 添加发送请求前的通知回调函数 63 | func (c *Context) AddCbBeforeSend(cb Callback) *Context { 64 | if c.CbBeforeSend == nil { 65 | c.CbBeforeSend = make([]Callback, 1) 66 | c.CbBeforeSend[0] = cb 67 | } else { 68 | c.CbBeforeSend = append(c.CbBeforeSend, cb) 69 | } 70 | return c 71 | } 72 | 73 | // AddCbAfterSend 添加发送请求后的通知回调函数 74 | func (c *Context) AddCbAfterSend(cb Callback) *Context { 75 | if c.CbAfterSend == nil { 76 | c.CbAfterSend = make([]Callback, 1) 77 | c.CbAfterSend[0] = cb 78 | } else { 79 | c.CbAfterSend = append(c.CbAfterSend, cb) 80 | } 81 | return c 82 | } 83 | 84 | // AddCbOnError 添加发生错误时的通知回调函数 85 | func (c *Context) AddCbOnError(cb Callback) *Context { 86 | if c.CbOnError == nil { 87 | c.CbOnError = make([]Callback, 1) 88 | c.CbOnError[0] = cb 89 | } else { 90 | c.CbOnError = append(c.CbOnError, cb) 91 | } 92 | return c 93 | } 94 | 95 | // AddCbOnRetring 添加请求重试的通知回调函数 96 | func (c *Context) AddCbOnRetring(cb Callback) *Context { 97 | if c.CbOnRetring == nil { 98 | c.CbOnRetring = make([]Callback, 1) 99 | c.CbOnRetring[0] = cb 100 | } else { 101 | c.CbOnRetring = append(c.CbOnRetring, cb) 102 | } 103 | return c 104 | } 105 | 106 | // invokeCbBeforeSend 调用所有CbBeforeSend回调函数 107 | func (c *Context) invokeCbBeforeSend(hc *HttpC, args ...interface{}) error { 108 | if c.CbBeforeSend != nil { 109 | length := len(c.CbBeforeSend) 110 | for i := length - 1; i >= 0; i-- { 111 | err := c.CbBeforeSend[i](hc, args...) 112 | if err != nil { 113 | return err 114 | } 115 | } 116 | } 117 | return nil 118 | } 119 | 120 | // invokeCbAfterSend 调用所有CbAfterSend回调函数 121 | func (c *Context) invokeCbAfterSend(hc *HttpC, args ...interface{}) error { 122 | if c.CbAfterSend != nil { 123 | length := len(c.CbAfterSend) 124 | for i := length - 1; i >= 0; i-- { 125 | err := c.CbAfterSend[i](hc, args...) 126 | if err != nil { 127 | return err 128 | } 129 | } 130 | } 131 | return nil 132 | } 133 | 134 | // invokeCbOnError 调用所有CbOnError回调函数 135 | func (c *Context) invokeCbOnError(hc *HttpC, args ...interface{}) error { 136 | if c.CbOnError != nil { 137 | length := len(c.CbOnError) 138 | for i := length - 1; i >= 0; i-- { 139 | err := c.CbOnError[i](hc, args...) 140 | if err != nil { 141 | return err 142 | } 143 | } 144 | } 145 | return nil 146 | } 147 | 148 | // invokeCbOnRetring 调用所有CbOnError回调函数 149 | func (c *Context) invokeCbOnRetring(hc *HttpC, args ...interface{}) error { 150 | if c.CbOnRetring != nil { 151 | length := len(c.CbOnRetring) 152 | for i := length - 1; i >= 0; i-- { 153 | err := c.CbOnRetring[i](hc, args...) 154 | if err != nil { 155 | return err 156 | } 157 | } 158 | } 159 | return nil 160 | } 161 | 162 | // getTransport 从Client中获取*http.Transport 163 | func (c *Context) getTransport() *http.Transport { 164 | if c.Client.Transport == nil { 165 | c.Client.Transport = &http.Transport{} 166 | } 167 | if t, ok := c.Client.Transport.(*http.Transport); ok { 168 | return t 169 | } 170 | c.Client.Transport = &http.Transport{} 171 | return c.Client.Transport.(*http.Transport) 172 | } 173 | 174 | // SetClient 设置http.Client 175 | func (c *Context) SetClient(client *http.Client) *Context { 176 | c.Client = client 177 | return c 178 | } 179 | 180 | // SetTotalTimeout 设置总超时,包括连接、所有从定向、读数据的时间,直到数据被读完 181 | func (c *Context) SetTotalTimeout(timeout time.Duration) *Context { 182 | c.Client.Timeout = timeout 183 | return c 184 | } 185 | 186 | // SetConnectReadTimeout 设置请求的连接超时和读数据超时 187 | func (c *Context) SetConnectReadTimeout(connectTimeout time.Duration, readTimeout time.Duration) *Context { 188 | c.getTransport().Dial = func(network, addr string) (net.Conn, error) { 189 | conn, err := net.DialTimeout(network, addr, connectTimeout) 190 | if err != nil { 191 | return nil, err 192 | } 193 | conn.SetDeadline(time.Now().Add(readTimeout)) 194 | return conn, nil 195 | } 196 | return c 197 | } 198 | 199 | // SetRetryConfig 设置重试的配置 200 | func (c *Context) SetRetryConfig(retries int, interval time.Duration, factor float64) *Context { 201 | c.Retries = retries 202 | c.RetryInterval = interval 203 | c.RetryFactor = factor 204 | return c 205 | } 206 | 207 | // EnableCookie 启用CookieJar 208 | func (c *Context) EnableCookie(enable bool) *Context { 209 | if enable { 210 | c.Client.Jar = nil 211 | } else { 212 | if c.Client.Jar == nil { 213 | jar, _ := cookiejar.New(nil) 214 | c.Client.Jar = jar 215 | } 216 | } 217 | return c 218 | } 219 | 220 | // SetTLSClientConfig 设置TLSClientConfig 221 | func (c *Context) SetTLSClientConfig(config *tls.Config) *Context { 222 | c.getTransport().TLSClientConfig = config 223 | return c 224 | } 225 | 226 | // SetCheckRedirect 设置CheckRedirect 227 | func (c *Context) SetCheckRedirect(cr func(req *http.Request, via []*http.Request) error) *Context { 228 | c.Client.CheckRedirect = cr 229 | return c 230 | } 231 | 232 | // SetProxy 设置请求代理 233 | func (c *Context) SetProxy(proxy func(*http.Request) (*url.URL, error)) *Context { 234 | c.getTransport().Proxy = proxy 235 | return c 236 | } 237 | 238 | // Copy 复制一份 239 | func (c *Context) Copy() *Context { 240 | return &Context{ 241 | Client: c.Client, 242 | BodyReaders: c.BodyReaders, 243 | CbBeforeSend: c.CbBeforeSend, 244 | CbAfterSend: c.CbAfterSend, 245 | CbOnError: c.CbOnError, 246 | CbOnRetring: c.CbOnRetring, 247 | Retries: c.Retries, 248 | RetryInterval: c.RetryInterval, 249 | RetryFactor: c.RetryFactor, 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /httpc.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | // Author: recallsong 4 | // Email: songruiguo@qq.com 5 | 6 | import ( 7 | "fmt" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "reflect" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | // 支持的HTTP method 17 | const ( 18 | POST = "POST" 19 | GET = "GET" 20 | HEAD = "HEAD" 21 | PUT = "PUT" 22 | DELETE = "DELETE" 23 | PATCH = "PATCH" 24 | OPTIONS = "OPTIONS" 25 | ) 26 | 27 | // 支持的ContentType类型 28 | const ( 29 | TypeTextHtml = "text/html" 30 | TypeTextPlain = "text/plain" 31 | TypeApplicationJson = "application/json" 32 | TypeApplicationXml = "application/xml" 33 | TypeApplicationForm = "application/x-www-form-urlencoded" 34 | TypeApplicationStream = "application/octet-stream" 35 | TypeMultipartFormData = "multipart/form-data" 36 | ) 37 | 38 | // HttpC 发起http请求的Client 39 | type HttpC struct { 40 | Context *Context // 上下文 41 | Request *http.Request // 请求 42 | Response *http.Response // 响应 43 | BaseURL string // 请求url基地址 44 | URL string // 请求的url 45 | QueryData url.Values // 请求url的query参数 46 | SendMediaType string // 请求的ContentType 47 | Data interface{} // 要发送的数据体 48 | Error error // 请求发生的错误 49 | SucStatus int // 指定成功的状态码,不匹配则以Error的形式返回 50 | } 51 | 52 | // New 创建一个HttpC类型 53 | func New(baseUrl string) *HttpC { 54 | c := &HttpC{ 55 | Context: DefaultContext, 56 | Request: &http.Request{ 57 | Proto: "HTTP/1.1", 58 | ProtoMajor: 1, 59 | ProtoMinor: 1, 60 | Header: make(http.Header), 61 | Method: "GET", 62 | }, 63 | BaseURL: baseUrl, 64 | URL: baseUrl, 65 | QueryData: url.Values{}, 66 | } 67 | return c 68 | } 69 | 70 | // Reset 重置HttpC 71 | func (c *HttpC) Reset() *HttpC { 72 | c.Request = &http.Request{ 73 | Proto: "HTTP/1.1", 74 | ProtoMajor: 1, 75 | ProtoMinor: 1, 76 | Header: make(http.Header), 77 | Method: "GET", 78 | } 79 | c.Response = nil 80 | c.URL = c.BaseURL 81 | c.QueryData = url.Values{} 82 | c.SendMediaType = "" 83 | c.Data = nil 84 | c.Error = nil 85 | c.SucStatus = 0 86 | return c 87 | } 88 | 89 | // SetContext 设置请求上下文 90 | func (c *HttpC) SetContext(context *Context) *HttpC { 91 | c.Context = context 92 | return c 93 | } 94 | 95 | // Path 追加url路径 96 | func (c *HttpC) Path(path string) *HttpC { 97 | if c.URL != "" && 98 | !strings.HasPrefix(path, "http://") && !strings.HasPrefix(path, "https://") || !strings.HasPrefix(path, "unix://") { 99 | if strings.HasSuffix(c.URL, "/") { 100 | if strings.HasPrefix(path, "/") { 101 | c.URL += path[0:] 102 | } else { 103 | c.URL += path 104 | } 105 | } else { 106 | if strings.HasPrefix(path, "/") { 107 | c.URL += path 108 | } else { 109 | c.URL += "/" + path 110 | } 111 | } 112 | } else { 113 | c.URL = path 114 | } 115 | return c 116 | } 117 | 118 | // EscapedPath 对path进行url编码后,再追加到当前的url上 119 | func (c *HttpC) EscapedPath(path interface{}) *HttpC { 120 | return c.Path(url.QueryEscape(fmt.Sprint(path))) 121 | } 122 | 123 | // Query 增加url的query参数 124 | func (c *HttpC) Query(name, value string) *HttpC { 125 | c.QueryData.Add(name, value) 126 | return c 127 | } 128 | 129 | // Header 设置http的请求头,会覆盖已存在的请求头 130 | func (c *HttpC) Header(key, value string) *HttpC { 131 | c.Request.Header.Set(key, value) 132 | return c 133 | } 134 | 135 | // AddHeader 添加http的请求头 136 | func (c *HttpC) AddHeader(key, value string) *HttpC { 137 | c.Request.Header.Add(key, value) 138 | return c 139 | } 140 | 141 | // ContentType 设置http头中的Content-Type 142 | func (c *HttpC) ContentType(value string) *HttpC { 143 | c.Request.Header.Set("Content-Type", value) 144 | return c 145 | } 146 | 147 | // BasicAuth 设置基于Basic认证的信息 148 | func (c *HttpC) BasicAuth(userName, password string) *HttpC { 149 | c.Request.SetBasicAuth(userName, password) 150 | return c 151 | } 152 | 153 | // AddCookie 添加cookie 154 | func (c *HttpC) AddCookie(ck *http.Cookie) *HttpC { 155 | c.Request.AddCookie(ck) 156 | return c 157 | } 158 | 159 | // Body 设置请求数据体 160 | func (c *HttpC) Body(body interface{}, mediaType ...string) *HttpC { 161 | if len(mediaType) > 0 { 162 | c.SendMediaType = mediaType[0] 163 | } else { 164 | header := c.Request.Header["Content-Type"] 165 | if len(header) > 0 { 166 | c.SendMediaType = header[0] 167 | } else { 168 | c.SendMediaType = "" 169 | } 170 | } 171 | c.Data = body 172 | return c 173 | } 174 | 175 | // SuccessStatus 设置成功的状态码 176 | func (c *HttpC) SuccessStatus(sucStatus int) *HttpC { 177 | c.SucStatus = sucStatus 178 | return c 179 | } 180 | 181 | // Get 发送Get请求,并根据mediaType将结果解析到out中 182 | func (c *HttpC) Get(out interface{}, mediaType ...string) error { 183 | return c.sendReuqest(GET, out, mediaType...) 184 | } 185 | 186 | // Post 发送Post请求,并根据mediaType将结果解析到out中 187 | func (c *HttpC) Post(out interface{}, mediaType ...string) error { 188 | return c.sendReuqest(POST, out, mediaType...) 189 | } 190 | 191 | // Put 发送Put请求,并根据mediaType将结果解析到out中 192 | func (c *HttpC) Put(out interface{}, mediaType ...string) error { 193 | return c.sendReuqest(PUT, out, mediaType...) 194 | } 195 | 196 | // Patch 发送Patch请求,并根据mediaType将结果解析到out中 197 | func (c *HttpC) Patch(out interface{}, mediaType ...string) error { 198 | return c.sendReuqest(PATCH, out, mediaType...) 199 | } 200 | 201 | // Delete 发送Delete请求,并根据mediaType将结果解析到out中 202 | func (c *HttpC) Delete(out interface{}, mediaType ...string) error { 203 | return c.sendReuqest(DELETE, out, mediaType...) 204 | } 205 | 206 | // Options 发送Options请求,并根据mediaType将结果解析到out中 207 | func (c *HttpC) Options(out interface{}, mediaType ...string) error { 208 | return c.sendReuqest(OPTIONS, out, mediaType...) 209 | } 210 | 211 | // throwError 返回error 212 | func (c *HttpC) throwError() error { 213 | if err := c.Context.invokeCbOnError(c); err != nil { 214 | return err 215 | } 216 | return c.Error 217 | } 218 | 219 | // sendReuqest 发送请求 220 | func (c *HttpC) sendReuqest(method string, out interface{}, recvMediaType ...string) error { 221 | if c.Error != nil { 222 | return c.throwError() 223 | } 224 | c.Request.Method = method 225 | if c.initReuqest() != nil { 226 | return c.throwError() 227 | } 228 | if c.Request.Body != nil { 229 | defer c.Request.Body.Close() 230 | } 231 | if c.Error = c.Context.invokeCbBeforeSend(c); c.Error != nil { 232 | return c.throwError() 233 | } 234 | interval := c.Context.RetryInterval 235 | retring := 0 236 | for retring = 0; c.Context.Retries == -1 || retring <= c.Context.Retries; { 237 | c.Response, c.Error = c.Context.Client.Do(c.Request) 238 | if c.Error != nil { 239 | retring++ 240 | if c.Context.Retries == -1 || retring <= c.Context.Retries { 241 | if interval > 0 && c.Context.RetryFactor > 0 { 242 | time.Sleep(interval) 243 | if c.Context.Retries != -1 { 244 | interval = time.Duration(float64(interval) * c.Context.RetryFactor) 245 | } 246 | } 247 | if err := c.Context.invokeCbOnRetring(c, retring, interval); err != nil { 248 | c.Error = err 249 | return c.throwError() 250 | } 251 | continue 252 | } 253 | } 254 | break 255 | } 256 | if c.Error != nil { 257 | return c.throwError() 258 | } 259 | defer func() { 260 | err := recover() 261 | if err != nil { 262 | c.Response.Body.Close() 263 | } else { 264 | if _, ok := out.(**http.Response); !ok { 265 | c.Response.Body.Close() 266 | } 267 | } 268 | }() 269 | if c.Error = c.Context.invokeCbAfterSend(c); c.Error != nil { 270 | return c.throwError() 271 | } 272 | if c.SucStatus != 0 { 273 | if c.SucStatus < 0 { 274 | if c.Response.StatusCode > -c.SucStatus { 275 | c.Error = fmt.Errorf("error http status %d , expect <= %d", c.Response.StatusCode, -c.SucStatus) 276 | return c.throwError() 277 | } 278 | } else { 279 | if c.Response.StatusCode != c.SucStatus { 280 | c.Error = fmt.Errorf("error http status %d , expect %d", c.Response.StatusCode, c.SucStatus) 281 | return c.throwError() 282 | } 283 | } 284 | } 285 | var mediaType string 286 | if len(recvMediaType) > 0 { 287 | mediaType = recvMediaType[0] 288 | } else { 289 | mediaType = c.Response.Header.Get("Content-Type") 290 | } 291 | c.Error = c.readResponse(c.Response, out, mediaType) 292 | if c.Error != nil { 293 | return c.throwError() 294 | } 295 | return nil 296 | } 297 | 298 | // initReuqest 初始化Request 299 | func (c *HttpC) initReuqest() error { 300 | //init url 301 | u, err := url.Parse(c.URL) 302 | if err != nil { 303 | c.Error = err 304 | return c.Error 305 | } 306 | //set query params 307 | q := u.Query() 308 | for k, v := range c.QueryData { 309 | for _, vv := range v { 310 | q.Add(k, vv) 311 | } 312 | } 313 | u.RawQuery = q.Encode() 314 | c.Request.URL = u 315 | // set body 316 | mediaType := c.SendMediaType 317 | switch c.Request.Method { 318 | case POST, PUT, PATCH: 319 | if c.Data == nil { 320 | break 321 | } 322 | var readerGetter ReqBodyReader 323 | var find bool 324 | typ := reflect.TypeOf(c.Data) 325 | if c.Context.BodyReaders != nil { 326 | readers := c.Context.BodyReaders 327 | if readers.ReqBodyTypeReaders != nil { 328 | readerGetter, find = readers.ReqBodyTypeReaders[typ] 329 | } 330 | if find == false { 331 | if readerGetter, find = GlobalReqBodyTypeReaders[typ]; find == false { 332 | if mediaType == "" { 333 | mediaType = strings.TrimSpace(strings.ToLower(strings.Split(c.Request.Header.Get("Content-Type"), ";")[0])) 334 | } else { 335 | mediaType = strings.TrimSpace(strings.ToLower(strings.Split(mediaType, ";")[0])) 336 | } 337 | if readers.ReqBodyMediaReaders != nil { 338 | if readerGetter, find = readers.ReqBodyMediaReaders[mediaType]; find == false { 339 | readerGetter, find = GlobalReqBodyMediaReaders[mediaType] 340 | } 341 | } else { 342 | readerGetter, find = GlobalReqBodyMediaReaders[mediaType] 343 | } 344 | } 345 | } 346 | } else { 347 | if readerGetter, find = GlobalReqBodyTypeReaders[typ]; find == false { 348 | if mediaType == "" { 349 | mediaType = strings.TrimSpace(strings.ToLower(strings.Split(c.Request.Header.Get("Content-Type"), ";")[0])) 350 | } else { 351 | mediaType = strings.TrimSpace(strings.ToLower(strings.Split(mediaType, ";")[0])) 352 | } 353 | readerGetter, find = GlobalReqBodyMediaReaders[mediaType] 354 | } 355 | } 356 | if find == false { 357 | c.Error = ErrReqBodyType 358 | return c.Error 359 | } 360 | c.Request.Body, c.Error = readerGetter(c.Request, typ, c.Data) 361 | case GET, HEAD, DELETE, OPTIONS: 362 | break 363 | default: 364 | c.Error = fmt.Errorf("invalid http method %s", c.Request.Method) 365 | return c.Error 366 | } 367 | return c.Error 368 | } 369 | 370 | // readResponse 从响应中读取数据到out中 371 | func (c *HttpC) readResponse(resp *http.Response, out interface{}, mediaType string) error { 372 | if out == nil { 373 | return nil 374 | } 375 | var readerGetter RespBodyReader 376 | var find bool 377 | typ := reflect.TypeOf(out) 378 | if c.Context.BodyReaders != nil { 379 | readers := c.Context.BodyReaders 380 | if readers.RespBodyTypeReaders != nil { 381 | readerGetter, find = readers.RespBodyTypeReaders[typ] 382 | } 383 | if find == false { 384 | if readerGetter, find = GlobalRespBodyTypeReaders[typ]; find == false { 385 | mediaType = strings.TrimSpace(strings.ToLower(strings.Split(mediaType, ";")[0])) 386 | if readers.RespBodyMediaReaders != nil { 387 | if readerGetter, find = readers.RespBodyMediaReaders[mediaType]; find == false { 388 | readerGetter, find = GlobalRespBodyMediaReaders[mediaType] 389 | } 390 | } else { 391 | readerGetter, find = GlobalRespBodyMediaReaders[mediaType] 392 | } 393 | } 394 | } 395 | } else { 396 | if readerGetter, find = GlobalRespBodyTypeReaders[typ]; find == false { 397 | mediaType = strings.TrimSpace(strings.ToLower(strings.Split(mediaType, ";")[0])) 398 | readerGetter, find = GlobalRespBodyMediaReaders[mediaType] 399 | } 400 | } 401 | if find == false { 402 | c.Error = ErrRespOutType 403 | return c.Error 404 | } 405 | c.Error = readerGetter(resp, resp.Body, typ, out) 406 | return c.Error 407 | } 408 | 409 | // String 将Httpc转换为字符串 410 | func (c *HttpC) String() string { 411 | urlStr := c.URL 412 | if c.Request.URL != nil { 413 | urlStr = c.Request.URL.String() 414 | } else { 415 | u, err := url.Parse(urlStr) 416 | if err == nil { 417 | q := u.Query() 418 | for k, v := range c.QueryData { 419 | for _, vv := range v { 420 | q.Add(k, vv) 421 | } 422 | } 423 | u.RawQuery = q.Encode() 424 | urlStr = u.String() 425 | } 426 | } 427 | if c.Response != nil { 428 | urlStr += " [ " + c.Response.Status + " ]" 429 | } 430 | if c.Error != nil { 431 | urlStr += " -> " + c.Error.Error() 432 | } 433 | return urlStr 434 | } 435 | 436 | // DumpRequest 将http请求明细输出为string 437 | func (c *HttpC) DumpRequest(dumpBody ...bool) string { 438 | if c.Request.URL == nil { 439 | if c.initReuqest() != nil { 440 | return fmt.Sprintln("DumpRequest error: ", c.Error.Error()) 441 | } 442 | if c.Request.Body != nil { 443 | defer c.Request.Body.Close() 444 | } 445 | } 446 | body := false 447 | if len(dumpBody) > 0 { 448 | body = dumpBody[0] 449 | } 450 | dump, err := httputil.DumpRequest(c.Request, body) 451 | if nil != err { 452 | return fmt.Sprintln("DumpRequest error: ", err.Error()) 453 | } 454 | return fmt.Sprintf("HTTP Request: \n%s\n", string(dump)) 455 | } 456 | 457 | // DumpResponse 将http请求明细输出为string 458 | func (c *HttpC) DumpResponse(dumpBody ...bool) string { 459 | body := false 460 | if len(dumpBody) > 0 { 461 | body = dumpBody[0] 462 | } 463 | dump, err := httputil.DumpResponse(c.Response, body) 464 | if nil != err { 465 | return fmt.Sprintln("DumpResponse error: ", err.Error()) 466 | } 467 | return fmt.Sprintf("HTTP Response: \n%s\n", string(dump)) 468 | } 469 | -------------------------------------------------------------------------------- /req_body_reader.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | // Author: recallsong 4 | // Email: songruiguo@qq.com 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "encoding/xml" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "mime/multipart" 15 | "net/http" 16 | "net/url" 17 | "os" 18 | "reflect" 19 | "strings" 20 | ) 21 | 22 | // ReqBodyReader 根据类型将数据转换为reader 23 | type ReqBodyReader func(req *http.Request, typ reflect.Type, data interface{}) (io.ReadCloser, error) 24 | 25 | var ( 26 | // ErrReqBodyType 请求的Body数据类型错误 27 | ErrReqBodyType = errors.New("invalid request body type") 28 | ) 29 | 30 | // GlobalReqBodyTypeReaders 默认的根据类型获取RequestBody的Reader映射 31 | var GlobalReqBodyTypeReaders = map[reflect.Type]ReqBodyReader{ 32 | reflect.TypeOf(""): stringReqBodyReader, 33 | reflect.TypeOf([]byte(nil)): bytesReqBodyReader, 34 | reflect.TypeOf(url.Values(nil)): urlValuesReqBodyReader, 35 | } 36 | 37 | // GlobalReqBodyMediaReaders 默认的根据MediaType获取RequestBody的Reader映射 38 | var GlobalReqBodyMediaReaders = map[string]ReqBodyReader{ 39 | TypeApplicationJson: jsonReqBodyReader, 40 | TypeApplicationXml: xmlReqBodyReader, 41 | TypeApplicationForm: formValuesReqBodyReader, 42 | TypeMultipartFormData: multipartReqBodyReader, 43 | } 44 | 45 | // stringReqBodyReader 将string类型的data转换为reader 46 | func stringReqBodyReader(req *http.Request, typ reflect.Type, data interface{}) (io.ReadCloser, error) { 47 | body := data.(string) 48 | req.ContentLength = int64(len(body)) 49 | return ioutil.NopCloser(strings.NewReader(body)), nil 50 | } 51 | 52 | // bytesReqBodyReader 将[]byte类型的data转换为reader 53 | func bytesReqBodyReader(req *http.Request, typ reflect.Type, data interface{}) (io.ReadCloser, error) { 54 | body := data.([]byte) 55 | req.ContentLength = int64(len(body)) 56 | return ioutil.NopCloser(bytes.NewReader(body)), nil 57 | } 58 | 59 | // urlValuesReqBodyReader 将url.Values类型的data转换为reader 60 | func urlValuesReqBodyReader(req *http.Request, typ reflect.Type, data interface{}) (io.ReadCloser, error) { 61 | body := (data.(url.Values)).Encode() 62 | req.ContentLength = int64(len(body)) 63 | return ioutil.NopCloser(strings.NewReader(body)), nil 64 | } 65 | 66 | // jsonReqBodyReader 将data转换为json格式,并返回对应的reader 67 | func jsonReqBodyReader(req *http.Request, typ reflect.Type, data interface{}) (io.ReadCloser, error) { 68 | contentJSON, err := json.Marshal(data) 69 | if err != nil { 70 | return nil, ErrReqBodyType 71 | } 72 | req.ContentLength = int64(len(contentJSON)) 73 | return ioutil.NopCloser(bytes.NewReader(contentJSON)), nil 74 | } 75 | 76 | // xmlReqBodyReader 将data转换为xml格式,并返回对应的reader 77 | func xmlReqBodyReader(req *http.Request, typ reflect.Type, data interface{}) (io.ReadCloser, error) { 78 | contentXML, err := xml.Marshal(data) 79 | if err != nil { 80 | return nil, ErrReqBodyType 81 | } 82 | req.ContentLength = int64(len(contentXML)) 83 | return ioutil.NopCloser(bytes.NewReader(contentXML)), nil 84 | } 85 | 86 | // formValuesReqBodyReader 将data转换为form参数格式,并返回对应的reader 87 | func formValuesReqBodyReader(req *http.Request, typ reflect.Type, data interface{}) (io.ReadCloser, error) { 88 | kind := typ.Kind() 89 | if kind == reflect.Map { 90 | switch v := data.(type) { 91 | case map[string]string: 92 | params := url.Values{} 93 | for key, val := range v { 94 | params.Add(key, val) 95 | } 96 | body := params.Encode() 97 | req.ContentLength = int64(len(body)) 98 | return ioutil.NopCloser(strings.NewReader(body)), nil 99 | case map[string][]string: 100 | body := url.Values(v).Encode() 101 | req.ContentLength = int64(len(body)) 102 | return ioutil.NopCloser(strings.NewReader(body)), nil 103 | case map[string]interface{}: 104 | params := url.Values{} 105 | for key, val := range v { 106 | if val != nil { 107 | params.Add(key, fmt.Sprint(val)) 108 | } 109 | } 110 | body := params.Encode() 111 | req.ContentLength = int64(len(body)) 112 | return ioutil.NopCloser(strings.NewReader(body)), nil 113 | default: 114 | return nil, ErrReqBodyType 115 | } 116 | } else if kind == reflect.Struct || (kind == reflect.Ptr && typ.Elem().Kind() == reflect.Struct) { 117 | params := url.Values{} 118 | value := reflect.ValueOf(data) 119 | if kind == reflect.Ptr { 120 | value = value.Elem() 121 | typ = typ.Elem() 122 | } 123 | num := typ.NumField() 124 | for i := 0; i < num; i++ { 125 | t := typ.Field(i) 126 | v := value.Field(i) 127 | tagVal := t.Tag.Get("form") 128 | if v.CanInterface() && tagVal != "" && tagVal != "-" { 129 | val := v.Interface() 130 | if val != nil { 131 | params.Add(tagVal, fmt.Sprint(val)) 132 | } 133 | } 134 | } 135 | body := params.Encode() 136 | req.ContentLength = int64(len(body)) 137 | return ioutil.NopCloser(strings.NewReader(body)), nil 138 | } 139 | return nil, ErrReqBodyType 140 | } 141 | 142 | // getFileShortName 获取简短的文件名 143 | func getFileShortName(file *os.File) string { 144 | longName := file.Name() 145 | return longName[strings.LastIndex(longName, "/")+1:] 146 | } 147 | 148 | // writeToMultipartFormData 将数据写入writer中 149 | func writeToMultipartFormData(key string, val interface{}, fileName string, writer *multipart.Writer, closeList *[]*os.File) error { 150 | switch realVal := val.(type) { 151 | case string: 152 | err := writer.WriteField(key, realVal) 153 | if err != nil { 154 | return err 155 | } 156 | case []string: 157 | for _, val := range realVal { 158 | err := writer.WriteField(key, val) 159 | if err != nil { 160 | return err 161 | } 162 | } 163 | case *os.File: 164 | if realVal == nil { 165 | return nil 166 | } 167 | if fileName == "" { 168 | fileName = getFileShortName(realVal) 169 | } 170 | part, err := writer.CreateFormFile(key, fileName) 171 | if err != nil { 172 | return err 173 | } 174 | _, err = io.Copy(part, realVal) 175 | if err != nil { 176 | return err 177 | } 178 | case FilePath: 179 | if realVal == "" { 180 | return nil 181 | } 182 | path := string(realVal) 183 | idx := strings.LastIndex(path, ":") 184 | if idx >= 0 { 185 | fileName = path[idx+1:] 186 | path = path[0:idx] 187 | } 188 | file, err := os.Open(path) 189 | if err != nil { 190 | return err 191 | } 192 | *closeList = append(*closeList, file) 193 | if fileName == "" { 194 | fileName = getFileShortName(file) 195 | } 196 | part, err := writer.CreateFormFile(key, fileName) 197 | if err != nil { 198 | return err 199 | } 200 | _, err = io.Copy(part, file) 201 | if err != nil { 202 | return err 203 | } 204 | default: 205 | if val != nil { 206 | err := writer.WriteField(key, fmt.Sprint(val)) 207 | if err != nil { 208 | return err 209 | } 210 | } 211 | } 212 | return nil 213 | } 214 | 215 | // fileReqBodyReader 将data转换为multipart格式,并返回对应的reader 216 | func multipartReqBodyReader(req *http.Request, typ reflect.Type, data interface{}) (reader io.ReadCloser, err error) { 217 | pr, pw := io.Pipe() 218 | writer := multipart.NewWriter(pw) 219 | closeList := make([]*os.File, 0) 220 | go func() (err error) { 221 | defer func() { 222 | var errs string 223 | werr := writer.Close() 224 | if werr != nil { 225 | errs += fmt.Sprintf("multipart.Writer.Close: %s\n", werr.Error()) 226 | } 227 | for _, item := range closeList { 228 | err := item.Close() 229 | if err != nil { 230 | errs += fmt.Sprintf("close %s: %s\n", item.Name(), err.Error()) 231 | } 232 | } 233 | if err != nil { 234 | errs += fmt.Sprintf("process: %s\n", err.Error()) 235 | } 236 | if errs != "" { 237 | errs = errs[0 : len(errs)-1] 238 | pw.CloseWithError(errors.New(errs)) 239 | } else { 240 | pw.Close() 241 | } 242 | }() 243 | kind := typ.Kind() 244 | if kind == reflect.Map { 245 | switch v := data.(type) { 246 | case map[string]string: 247 | for key, val := range v { 248 | err = writer.WriteField(key, val) 249 | if err != nil { 250 | return err 251 | } 252 | } 253 | case map[string][]string: 254 | for key, vals := range v { 255 | for _, val := range vals { 256 | err = writer.WriteField(key, val) 257 | if err != nil { 258 | return err 259 | } 260 | } 261 | } 262 | case map[string]interface{}: 263 | for key, val := range v { 264 | err = writeToMultipartFormData(key, val, "", writer, &closeList) 265 | if err != nil { 266 | return err 267 | } 268 | } 269 | case map[string]FilePath: 270 | for key, val := range v { 271 | err = writeToMultipartFormData(key, val, "", writer, &closeList) 272 | if err != nil { 273 | return err 274 | } 275 | } 276 | case map[string]*os.File: 277 | for key, val := range v { 278 | err = writeToMultipartFormData(key, val, "", writer, &closeList) 279 | if err != nil { 280 | return err 281 | } 282 | } 283 | default: 284 | return ErrReqBodyType 285 | } 286 | } else if kind == reflect.Struct || (kind == reflect.Ptr && typ.Elem().Kind() == reflect.Struct) { 287 | value := reflect.ValueOf(data) 288 | if kind == reflect.Ptr { 289 | value = value.Elem() 290 | typ = typ.Elem() 291 | } 292 | num := typ.NumField() 293 | for i := 0; i < num; i++ { 294 | t := typ.Field(i) 295 | v := value.Field(i) 296 | tagVal := t.Tag.Get("form") 297 | fileName := t.Tag.Get("file") 298 | if v.CanInterface() && tagVal != "" && tagVal != "-" { 299 | if fileName == "" || fileName == "-" { 300 | fileName = "" 301 | } 302 | err = writeToMultipartFormData(tagVal, v.Interface(), fileName, writer, &closeList) 303 | if err != nil { 304 | return err 305 | } 306 | } 307 | } 308 | } else { 309 | return ErrReqBodyType 310 | } 311 | return 312 | }() 313 | req.Header.Set("Content-Type", writer.FormDataContentType()) 314 | return pr, nil 315 | } 316 | -------------------------------------------------------------------------------- /resp_body_reader.go: -------------------------------------------------------------------------------- 1 | package httpc 2 | 3 | // Author: recallsong 4 | // Email: songruiguo@qq.com 5 | 6 | import ( 7 | "encoding/json" 8 | "encoding/xml" 9 | "errors" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "net/http" 14 | "os" 15 | "reflect" 16 | ) 17 | 18 | // RespBodyReader 根据类型将数据读取到out中 19 | type RespBodyReader func(resp *http.Response, reader io.ReadCloser, typ reflect.Type, out interface{}) error 20 | 21 | var ( 22 | // ErrRespOutType 接收响应Body的数据类型错误 23 | ErrRespOutType = errors.New("invalid response output type") 24 | ) 25 | 26 | // FilePath 保存文件的路径 27 | type FilePath string 28 | 29 | // SaveInfo 保存文件的设置 30 | type SaveInfo struct { 31 | Path string 32 | Mode os.FileMode 33 | Override bool 34 | } 35 | 36 | // GlobalRespBodyTypeReaders 默认的根据类型获取ResponseBody的Reader映射 37 | var GlobalRespBodyTypeReaders = map[reflect.Type]RespBodyReader{ 38 | reflect.TypeOf((*string)(nil)): readAllRespWrapper(stringRespBodyReader), 39 | reflect.TypeOf((*[]byte)(nil)): readAllRespWrapper(bytesRespBodyReader), 40 | reflect.TypeOf((**http.Response)(nil)): responseReader, 41 | reflect.TypeOf((FilePath)("")): downloadRespBodyReader, 42 | reflect.TypeOf((*SaveInfo)(nil)): downloadRespBodyReader, 43 | reflect.TypeOf(SaveInfo{}): downloadRespBodyReader, 44 | } 45 | 46 | // GlobalRespBodyMediaReaders 默认的根据MediaType获取ResponseBody的Reader映射 47 | var GlobalRespBodyMediaReaders = map[string]RespBodyReader{ 48 | TypeApplicationJson: readAllRespWrapper(jsonRespBodyReader), 49 | TypeApplicationXml: readAllRespWrapper(xmlRespBodyReader), 50 | TypeApplicationStream: streamRespBodyReader, 51 | } 52 | 53 | // readAllRespWrapper 从响应中读取全部数据的包装器 54 | func readAllRespWrapper(readFunc func(resp *http.Response, data []byte, typ reflect.Type, out interface{}) error) RespBodyReader { 55 | return func(resp *http.Response, reader io.ReadCloser, typ reflect.Type, out interface{}) error { 56 | body, _ := ioutil.ReadAll(reader) 57 | return readFunc(resp, body, typ, out) 58 | } 59 | } 60 | 61 | // stringRespBodyReader 将数据读取到*string类型的out参数中 62 | func stringRespBodyReader(resp *http.Response, data []byte, typ reflect.Type, out interface{}) error { 63 | *(out.(*string)) = string(data) 64 | return nil 65 | } 66 | 67 | // bytesRespBodyReader 将数据读取到*[]byte类型的out参数中 68 | func bytesRespBodyReader(resp *http.Response, data []byte, typ reflect.Type, out interface{}) error { 69 | *(out.(*[]byte)) = data 70 | return nil 71 | } 72 | 73 | // jsonRespBodyReader 将json格式的数据的解析到out参数中 74 | func jsonRespBodyReader(resp *http.Response, data []byte, typ reflect.Type, out interface{}) error { 75 | kind := typ.Elem().Kind() 76 | if kind == reflect.Struct || kind == reflect.Map { 77 | return json.Unmarshal(data, out) 78 | } 79 | return ErrRespOutType 80 | } 81 | 82 | // xmlRespBodyReader 将xml格式的数据的解析到out参数中 83 | func xmlRespBodyReader(resp *http.Response, data []byte, typ reflect.Type, out interface{}) error { 84 | kind := typ.Elem().Kind() 85 | if kind == reflect.Struct || kind == reflect.Map { 86 | return xml.Unmarshal(data, out) 87 | } 88 | return ErrRespOutType 89 | } 90 | 91 | // responseReader 返回http response 92 | func responseReader(resp *http.Response, reader io.ReadCloser, typ reflect.Type, out interface{}) error { 93 | output := out.(**http.Response) 94 | (*output) = resp 95 | return nil 96 | } 97 | 98 | // downloadRespBodyReader 将响应的数据保存到文件 99 | func downloadRespBodyReader(resp *http.Response, reader io.ReadCloser, typ reflect.Type, out interface{}) error { 100 | var info *SaveInfo 101 | switch output := out.(type) { 102 | case FilePath: 103 | info = &SaveInfo{ 104 | Path: string(output), 105 | Override: true, 106 | Mode: 0666, 107 | } 108 | case SaveInfo: 109 | info = &output 110 | case *SaveInfo: 111 | info = output 112 | default: 113 | return ErrRespOutType 114 | } 115 | var file *os.File 116 | var err error 117 | if info.Override { 118 | file, err = os.OpenFile(info.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, info.Mode) 119 | } else { 120 | file, err = os.OpenFile(info.Path, os.O_RDWR|os.O_CREATE|os.O_EXCL, info.Mode) 121 | } 122 | if err != nil { 123 | return err 124 | } 125 | file.Chmod(info.Mode) 126 | defer file.Close() 127 | written, err := io.Copy(file, resp.Body) 128 | if resp.ContentLength != written { 129 | return fmt.Errorf("save file size is %d, but content length is %d", written, resp.ContentLength) 130 | } 131 | return nil 132 | } 133 | 134 | // streamRespBodyReader 将响应的数据保存到文件 135 | func streamRespBodyReader(resp *http.Response, reader io.ReadCloser, typ reflect.Type, out interface{}) error { 136 | kind := typ.Kind() 137 | if kind == reflect.String { 138 | return downloadRespBodyReader(resp, reader, typ, FilePath(out.(string))) 139 | } 140 | return ErrRespOutType 141 | } 142 | --------------------------------------------------------------------------------