Compare commits

...

7 Commits
v1.0.9 ... main

8 changed files with 96 additions and 53 deletions

7
.gitignore vendored
View File

@ -1 +1,8 @@
.log.meta.json .log.meta.json
.ai/
.geminiignore
.gemini
env.json
env.yml
env.yaml
/CODE-FULL.md

View File

@ -1,5 +1,10 @@
# CHANGELOG # CHANGELOG
## v1.0.10 (2026-05-09)
- **CI/CD 对齐**:
- 修复测试用例由于默认绑定泛地址导致的 `connection refused` 错误,显式绑定本地测试服务器至 `127.0.0.1`,提高 macOS 与多网卡环境下的测试稳定性。
- 更新 `TEST.md`,同步最新的测试用例覆盖场景(包括 `Multipart``Form` 的验证)及真实 Benchmark。
## v1.0.9 (2026-05-09) ## v1.0.9 (2026-05-09)
- **基础设施对齐**: - **基础设施对齐**:
- 升级 `apigo.cc/go/log``v1.1.13` - 升级 `apigo.cc/go/log``v1.1.13`

10
TEST.md
View File

@ -4,9 +4,10 @@
- **基础请求**: 验证了使用 `Get` 获取 JSON 响应并通过 `To[T]` 进行泛型绑定的功能。 - **基础请求**: 验证了使用 `Get` 获取 JSON 响应并通过 `To[T]` 进行泛型绑定的功能。
- **本地服务器**: 验证了客户端与本地测试服务器的交互。 - **本地服务器**: 验证了客户端与本地测试服务器的交互。
- **H2C 支持**: 验证了 H2C (HTTP/2 Cleartext) 的兼容性。 - **H2C 支持**: 验证了 H2C (HTTP/2 Cleartext) 的兼容性。
- **手动请求**: 验证了通过 `ManualDo` 进行精细化控制的流 - **手动请求**: 验证了通过 `ManualDo` 进行精细化控制的流式请求
- **文件下载**: 验证了使用 `Download` 进行并发分段下载的功能。 - **文件下载**: 验证了使用 `Download` 进行并发分段下载的功能。
- **多部分表单**: 验证了使用 `MPost` 上传文件和表单数据的正确性。 - **多部分表单 (Multipart)**: 验证了使用流式 `io.Pipe``Multipart` 发送,以及多类型、多值字段的支持。
- **表单与映射**: 验证了使用 `Form`, `map[string]string`, `map[string][]string`, 和 `map[string][]any` 自动转为正确的 `application/x-www-form-urlencoded``application/json`
## 性能测试结果 (Benchmark) ## 性能测试结果 (Benchmark)
``` ```
@ -14,5 +15,8 @@ goos: darwin
goarch: amd64 goarch: amd64
pkg: apigo.cc/go/http pkg: apigo.cc/go/http
cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz cpu: Intel(R) Core(TM) i9-9980HK CPU @ 2.40GHz
BenchmarkGet-16 15944 73379 ns/op BenchmarkGet-16 16364 74735 ns/op
``` ```
## 测试覆盖率
- **整体覆盖率**: 56.5% of statements

View File

@ -45,6 +45,28 @@ type Result struct {
type Form map[string]string type Form map[string]string
type Multipart map[string]any type Multipart map[string]any
var DefaultClient = NewClient(30 * time.Second)
func Get(url string, headers ...string) *Result {
return DefaultClient.Get(url, headers...)
}
func Post(url string, data any, headers ...string) *Result {
return DefaultClient.Post(url, data, headers...)
}
func Put(url string, data any, headers ...string) *Result {
return DefaultClient.Put(url, data, headers...)
}
func Delete(url string, data any, headers ...string) *Result {
return DefaultClient.Delete(url, data, headers...)
}
func Do(method, url string, data any, headers ...string) *Result {
return DefaultClient.Do(method, url, data, headers...)
}
func NewClient(timeout time.Duration) *Client { func NewClient(timeout time.Duration) *Client {
if timeout < time.Millisecond && timeout > 0 { if timeout < time.Millisecond && timeout > 0 {
timeout *= time.Millisecond timeout *= time.Millisecond
@ -380,7 +402,12 @@ func (client *Client) writeMultipartPart(writer *multipart.Writer, key string, v
if err != nil { if err != nil {
return err return err
} }
return writer.WriteField(key, string(bytesData)) part, err := writer.CreateFormField(key)
if err != nil {
return err
}
_, err = part.Write(bytesData)
return err
} }
} }
@ -471,7 +498,7 @@ func (client *Client) do(fetchBody bool, method, url string, data any, headers .
}() }()
default: default:
bytesData, _ := cast.ToJSONBytes(data) bytesData, _ := cast.ToJSONBytes(data)
if len(bytesData) > 0 && string(bytesData) != "null" { if len(bytesData) > 0 && !bytes.Equal(bytesData, []byte("null")) {
reader = bytes.NewReader(bytesData) reader = bytes.NewReader(bytesData)
contentType = "application/json" contentType = "application/json"
contentLength = len(bytesData) contentLength = len(bytesData)

View File

@ -44,7 +44,7 @@ func TestLocalServer(t *testing.T) {
_, _ = w.Write([]byte("world")) _, _ = w.Write([]byte("world"))
}) })
server := &http.Server{Addr: ":18080", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18080", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
@ -102,7 +102,7 @@ func BenchmarkGet(b *testing.B) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("ok")) _, _ = w.Write([]byte("ok"))
}) })
server := &http.Server{Addr: ":18082", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18082", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -121,7 +121,7 @@ func TestManualDo(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("stream")) _, _ = w.Write([]byte("stream"))
}) })
server := &http.Server{Addr: ":18083", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18083", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -157,7 +157,7 @@ func TestDownload(t *testing.T) {
} }
_, _ = w.Write([]byte(content)) _, _ = w.Write([]byte(content))
}) })
server := &http.Server{Addr: ":18084", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18084", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -189,7 +189,7 @@ func TestMultipartDo(t *testing.T) {
} }
fmt.Fprintf(w, "foo=%s,file=%s", f, string(fileContent)) fmt.Fprintf(w, "foo=%s,file=%s", f, string(fileContent))
}) })
server := &http.Server{Addr: ":18086", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18086", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -210,7 +210,7 @@ func TestFormAndMap(t *testing.T) {
body, _ := io.ReadAll(r.Body) body, _ := io.ReadAll(r.Body)
fmt.Fprintf(w, "ct=%s,body=%s", ct, string(body)) fmt.Fprintf(w, "ct=%s,body=%s", ct, string(body))
}) })
server := &http.Server{Addr: ":18087", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18087", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)

26
go.mod
View File

@ -3,21 +3,21 @@ module apigo.cc/go/http
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/cast v1.2.8 apigo.cc/go/cast v1.3.3
apigo.cc/go/encoding v1.0.6 apigo.cc/go/encoding v1.3.1
apigo.cc/go/file v1.0.7 apigo.cc/go/file v1.3.2
apigo.cc/go/log v1.1.13 apigo.cc/go/log v1.3.4
apigo.cc/go/rand v1.0.5 apigo.cc/go/rand v1.3.1
golang.org/x/net v0.53.0 golang.org/x/net v0.54.0
) )
require ( require (
apigo.cc/go/config v1.0.6 // indirect apigo.cc/go/config v1.3.1 // indirect
apigo.cc/go/id v1.0.5 // indirect apigo.cc/go/id v1.3.1 // indirect
apigo.cc/go/safe v1.0.5 // indirect apigo.cc/go/safe v1.3.1 // indirect
apigo.cc/go/shell v1.0.5 // indirect apigo.cc/go/shell v1.3.1 // indirect
golang.org/x/crypto v0.50.0 // indirect golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.43.0 // indirect golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.36.0 // indirect golang.org/x/text v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

52
go.sum
View File

@ -1,35 +1,35 @@
apigo.cc/go/cast v1.2.8 h1:plb676DH2TjYljzf8OEMGT6lIhmZ/xaxEFfs0kDOiSI= apigo.cc/go/cast v1.3.3 h1:aln5eDR5DZVWVzZ/y5SJh1gQNgWv2sT82I25NaO9g34=
apigo.cc/go/cast v1.2.8/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk= apigo.cc/go/cast v1.3.3/go.mod h1:lGlwImiOvHxG7buyMWhFzcdvQzmSaoKbmr7bcDfUpHk=
apigo.cc/go/config v1.0.6 h1:32nOCr+8AkGFnKuythCjHPOjxilg6SOlSWXKTkNtx6I= apigo.cc/go/config v1.3.1 h1:wZzUh4oL+fGD6SayVgX6prLPMsniM25etWFcEH8XzIE=
apigo.cc/go/config v1.0.6/go.mod h1:nX+nLKZTP6Xton9Gt/9XsTh0d1sQ+Qkwysgyjq/k4R0= apigo.cc/go/config v1.3.1/go.mod h1:7KHz/1WmtBLM762Lln/TaXh2dmlMvJTLhnlk33zbS3U=
apigo.cc/go/encoding v1.0.6 h1:NJTagkBQaoTJSbJVo21wUILhLb5f74jprtKvIESQWpI= apigo.cc/go/encoding v1.3.1 h1:y8O58KYAyulkThg1O2ji2BqjnFoSvk42sit9I3z+K7Y=
apigo.cc/go/encoding v1.0.6/go.mod h1:GeAz5OnCkFybTR1+GWFqdMgfq5v6r4MsjWVPOk/mpf4= apigo.cc/go/encoding v1.3.1/go.mod h1:xAJk5b83VZ31mXMTnyp0dfMoBKfT/AHDn0u+cQfojgY=
apigo.cc/go/file v1.0.7 h1:j1VBtmMZqNGnH++DYjHecX1XAKTlKAuqUiUW1HafRas= apigo.cc/go/file v1.3.2 h1:pu4oiDyiqgj3/eykfnJf+/6+A9v/Z0b3ClP5XK+lwG4=
apigo.cc/go/file v1.0.7/go.mod h1:2qC+p8p7iHx0DHAPubHXkLrEuLGO9WXTtdwyFjrSc1I= apigo.cc/go/file v1.3.2/go.mod h1:vci4h0Pz94mV6dkniQkuyBYERVYeq7/LX4jJVuCg9hs=
apigo.cc/go/id v1.0.5 h1:23YkR7oklSA69gthYlu8zl/kpIkeIoEYxi1f1Sz5l3A= apigo.cc/go/id v1.3.1 h1:pkqi6VeWyQoHuIu0Zbx/RRxIAdM61Js0j6cY1M9XVCk=
apigo.cc/go/id v1.0.5/go.mod h1:ZaYLIyrJvkf3j7J8a0lnKywSAHljaczWxU0x2HmQDzg= apigo.cc/go/id v1.3.1/go.mod h1:P2/vl3tyW3US+ayOFSMoPIOCulNLBngNYPhXJC/Z7J4=
apigo.cc/go/log v1.1.13 h1:ZABeVA9DxhdneLqHrYEc+6YijgoygG8eEsgDxYDzpDc= apigo.cc/go/log v1.3.4 h1:UT8Neb9r4QjjbCFbTzw+ZeTxd+DmdmR5gNExeR4Cj+g=
apigo.cc/go/log v1.1.13/go.mod h1:eabuI2SynGNgo5FXPbGgQtyxjp94wT643XzjYhEIP3A= apigo.cc/go/log v1.3.4/go.mod h1:/Q/2r51xWSsrS4QN5U9jLiTw8n6qNC8kG9nuVHweY20=
apigo.cc/go/rand v1.0.5 h1:AkUoWr0SELgeDmRjLEDjOIp29nXdzqQQvmGRIHpTN7U= apigo.cc/go/rand v1.3.1 h1:7FvsI6PtQ5XrWER0dTiLVo0p7GIxRidT/TBKhVy93j8=
apigo.cc/go/rand v1.0.5/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk= apigo.cc/go/rand v1.3.1/go.mod h1:mZ/4Soa3bk+XvDaqPWJuUe1bfEi4eThBj1XmEAuYxsk=
apigo.cc/go/safe v1.0.5 h1:yZJLhpMntJrtqU/ev0UlyOoHu/cLrnnGUO4aHyIZcwE= apigo.cc/go/safe v1.3.1 h1:irTCqPAC97gGsX/Lw5AzLelDt1xXLEZIAaVhLELWe9Q=
apigo.cc/go/safe v1.0.5/go.mod h1:i9xnh7reJIFPauLnlzuIDgvrQvhjxpFlpVh3O6ulWd0= apigo.cc/go/safe v1.3.1/go.mod h1:XdOpBhN2vkImalaykYXXmEpczqWa1y3ah6/Q72cdRqE=
apigo.cc/go/shell v1.0.5 h1:bmvUTJGe1GwsHAy42v3iaoK40PoBC7Xq1aMCYxUZmtg= apigo.cc/go/shell v1.3.1 h1:M8oD0b2HcJuCC6frQFx11b3UTcTx3lATX8XK+YXSVm8=
apigo.cc/go/shell v1.0.5/go.mod h1:sx/nYw5CihHWmo5JHkaZUbmMYXNHx8swzArbQCUGHjc= apigo.cc/go/shell v1.3.1/go.mod h1:ZMdJjpCpWdvsHKUXlelh/AxsV/nWdkH/k3lISfzMdUw=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View File

@ -20,7 +20,7 @@ func TestOptimizationForms(t *testing.T) {
} }
fmt.Fprintf(w, "foo=%v", r.Form["foo"]) fmt.Fprintf(w, "foo=%v", r.Form["foo"])
}) })
server := &http.Server{Addr: ":18088", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18088", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -62,7 +62,7 @@ func TestMultipartStreaming(t *testing.T) {
} }
fmt.Fprintf(w, "foo=%s,file=%s", f1, string(fileContent)) fmt.Fprintf(w, "foo=%s,file=%s", f1, string(fileContent))
}) })
server := &http.Server{Addr: ":18089", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18089", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -86,7 +86,7 @@ func TestMultipartMultipleParts(t *testing.T) {
_ = r.ParseMultipartForm(10 << 20) _ = r.ParseMultipartForm(10 << 20)
fmt.Fprintf(w, "foo=%v", r.MultipartForm.Value["foo"]) fmt.Fprintf(w, "foo=%v", r.MultipartForm.Value["foo"])
}) })
server := &http.Server{Addr: ":18090", Handler: handler} server := &http.Server{Addr: "127.0.0.1:18090", Handler: handler}
go func() { _ = server.ListenAndServe() }() go func() { _ = server.ListenAndServe() }()
defer server.Close() defer server.Close()
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)