通过示例学习-Go-语言-2023-十七-
通过示例学习 Go 语言 2023(十七)
如何在 Go (Golang)中创建恐慌
目录
-
概述
-
运行时错误恐慌
-
显式调用 panic 函数
概述
恐慌类似于 golang 中的异常。恐慌是为了在异常条件下退出程序。程序中可能以两种方式发生恐慌。
-
程序中的运行时错误
-
通过显式调用 panic 函数。这可以由程序员在程序无法继续并必须退出时调用。
Go 提供了一个特殊函数来创建恐慌。下面是该函数的语法。
func panic(v interface{})
该函数可以由程序员显式调用以创建恐慌。它接受一个空接口作为参数。当程序中发生恐慌时,它输出两件事。
-
作为参数传递给 panic 函数的错误信息
-
恐慌发生时的堆栈跟踪
运行时错误恐慌
程序中的运行时错误可能发生在以下情况
-
超出边界的数组访问
-
在 nil 指针上调用函数
-
在封闭通道上发送
-
错误的类型断言
让我们看一个因超出边界的数组访问而导致的运行时错误的例子。
package main
import "fmt"
func main() {
a := []string{"a", "b"}
print(a, 2)
}
func print(a []string, index int) {
fmt.Println(a[index])
}
输出
panic: runtime error: index out of range [2] with length 2
goroutine 1 [running]:
main.checkAndPrint(...)
main.go:12
main.main()
/main.go:8 +0x1b
exit status 2
在上述程序中,我们有一个长度为 2 的切片,而我们在print函数中尝试访问索引 3。超出边界的访问是不允许的,这将导致恐慌,如输出所示。请注意,在输出中有两件事。
-
错误信息
-
恐慌发生时的堆栈跟踪
在程序中发生运行时错误的情况还有很多。我们不会列出所有情况,但你应该明白。
显式调用 panic 函数
一些情况下程序员可以显式调用 panic 函数:
-
该函数期望一个有效的参数,但却提供了一个 nil 参数。在这种情况下,程序无法继续并会因为传递了 nil 参数而引发恐慌。
-
任何其他程序无法继续的场景。
让我们看一个例子。
package main
import "fmt"
func main() {
a := []string{"a", "b"}
checkAndPrint(a, 2)
}
func checkAndPrint(a []string, index int) {
if index > (len(a) - 1) {
panic("Out of bound access for slice")
}
fmt.Println(a[index])
}
输出
panic: runtime error: index out of range [2] with length 2
goroutine 1 [running]:
main.checkAndPrint(0xc00009af58, 0x2, 0x2, 0x2)
main.go:15 +0x31
main.main()
main.go:8 +0x7d
exit status 2
在上述程序中,我们再次有一个函数checkAndPrint,它接受一个切片作为参数和一个索引。然后它检查传递的索引是否大于切片长度减 1。如果是,那么它就是切片的超出边界访问,所以它会引发恐慌。如果不是,它将打印该索引处的值。再次请注意,在输出中有两件事。
-
错误信息
-
恐慌发生时的堆栈跟踪
如何在 Go(Golang)中初始化具有数组或切片字段的结构。
目录
-
概述
-
程序
概述
一个结构可以包含一个字段,该字段是另一种类型的切片或数组。要初始化这样的结构,我们可以首先初始化另一种类型的切片/数组。之后,我们可以初始化父结构。在下面的示例中,class结构类型有一个student结构类型的切片。
type student struct {
name string
rollNo int
city string
}
type class struct {
className string
students []student
}
要初始化这种类型的结构,我们需要首先初始化嵌套结构的切片。也就是说,我们将首先初始化student结构的切片。例如如下所示。
goerge := student{"Goerge", 35, "Newyork"}
john := student{"Goerge", 25, "London"}
students := []student{goerge, john}
然后我们可以如下初始化class结构。
class := class{"firstA", students}class := class{"firstA", students}
另一种方法是在初始化class结构时直接初始化student结构的切片。如下所示。
class := class{"firstA", []student{goerge, john}}
程序
让我们看一下完整的程序。
package main
import "fmt"
type class struct {
className string
students []student
}
type student struct {
name string
rollNo int
city string
}
func main() {
goerge := student{"Goerge", 35, "Newyork"}
john := student{"Goerge", 25, "London"}
students := []student{goerge, john}
class := class{"firstA", students}
fmt.Printf("class is %v\n", class)
}
输出
class is {firstA [{Goerge 35 Newyork} {Goerge 25 London}]}
如何在 Go(Golang)中初始化一个包含嵌套结构体的结构体
目录
-
概述
-
程序
概述
一个结构体可以嵌套另一个结构体。让我们来看一个嵌套结构体的示例。在下面的示例中,员工结构体嵌套了地址结构体。
type address struct {
city string
country string
}
type employee struct {
name string
age int
salary int
address address
}
要初始化这种结构体,我们需要先初始化嵌套结构体。也就是说,我们首先要初始化地址结构体。例如如下所示
address := address{city: "London", country: "UK"}
然后我们可以如下初始化员工结构体
emp := employee{name: "Sam", age: 31, salary: 2000, address: address}
另一种方法是在员工结构体初始化时直接初始化地址结构体。如下所示
emp := employee{name: "Sam", age: 31, salary: 2000, address: address{city: "London", country: "UK"}}
程序
package main
import "fmt"
type employee struct {
name string
age int
salary int
address address
}
type address struct {
city string
country string
}
func main() {
address := address{city: "London", country: "UK"}
emp := employee{name: "Sam", age: 31, salary: 2000, address: address}
fmt.Printf("City: %s\n", emp.address.city)
fmt.Printf("Country: %s\n", emp.address.country)
}
输出
City: London
Country: UK
如何在按下回车键之前暂停 Go 程序
来源:
golangbyexample.com/how-to-pause-a-go-program-until-enter-key-is-pressed/
有时需要暂停 Go 程序,直到按下某个键。例如,只有在按下回车键时,才希望 Go 程序退出。
下面的程序会打印递增的数字,并会在你按下键时立即停止。试试看。
package main
import (
"fmt"
"time"
)
func main() {
go counter()
fmt.Println("Press the Enter Key to stop anytime")
fmt.Scanln()
}
func counter() {
i := 0
for {
fmt.Println(i)
time.Sleep(time.Second * 1)
i++
}
}
如何在 Go(Golang)中设置 HTTP 响应状态码
目录
-
概述
-
示例
概述
WriteHeader方法在net/http包中的 ResponseWriter 接口可以用于从 Golang 服务器返回状态码。
在 Go 中,响应由ResponseWriter接口表示。这里是接口的链接 –
golang.org/pkg/net/http/#ResponseWriter
ResponseWriter 接口由 HTTP 处理程序用于构建 HTTP 响应。它提供了三个函数来设置响应参数
-
Header – 用于写入响应头
-
Write([]byte) – 用于写入响应正文
-
WriteHeader(statusCode int) – 用于写入 HTTP 状态码
可以看到,WriteHeader函数以状态码作为输入,该状态码会在 HTTP 响应中发送。而Write函数则可以用于设置响应正文。需要注意的是,如果没有显式调用WriteHeader,则对Write函数的调用会内部调用WriteHeader函数,并返回状态码 200,即 StatusOk。
示例
让我们看看发送 HTTP 状态码和正文的示例
以下是相同程序的内容
package main
import (
"encoding/json"
"log"
"net/http"
)
func main() {
handler := http.HandlerFunc(handleRequest)
http.Handle("/example", handler)
http.ListenAndServe(":8080", nil)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusCreated)
w.Header().Set("Content-Type", "application/json")
resp := make(map[string]string)
resp["message"] = "Status Created"
jsonResp, err := json.Marshal(resp)
if err != nil {
log.Fatalf("Error happened in JSON marshal. Err: %s", err)
}
w.Write(jsonResp)
return
}
在这里,我们使用WriteHeader函数指定 201 HTTP 状态码。同样,我们可以将此处列出的任何状态码传递给WriteHeader函数
golang.org/src/net/http/status.go
它还使用Write函数返回响应正文。上述代码将以下 JSON 请求正文作为响应返回
{"message":"Status Created"}
运行上述程序。这将在你本地机器的 8080 端口上启动一个服务器。现在可以向服务器发出以下 curl 调用
curl -v -X POST http://localhost:8080/example
下面是输出结果
* Connected to localhost (::1) port 8080 (#0)
> POST /example HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 201 Created
< Date: Sat, 10 Jul 2021 10:40:33 GMT
< Content-Length: 28
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
{"message":"Status Created"}
从输出中可以看到,它会正确返回201状态码及其正文。
你也可以直接将 201 传递给 WriteHeader 函数,以发送 201 响应。
w.WriteHeader(201)
尝试一下,它会有效。
我们提到过,我们并没有显式调用 WriteHeader,因此对Write函数的调用将内部调用 WriteHeader 函数,并返回状态码 200,即 StatusOk。我们来看看一个例子-
package main
import (
"encoding/json"
"log"
"net/http"
)
func main() {
handler := http.HandlerFunc(handleRequest)
http.Handle("/example", handler)
http.ListenAndServe(":8080", nil)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
resp := make(map[string]string)
resp["message"] = "Success"
jsonResp, err := json.Marshal(resp)
if err != nil {
log.Fatalf("Error happened in JSON marshal. Err: %s", err)
}
w.Write(jsonResp)
return
}
查看上面的代码。我们没有在任何地方调用WriteHeader函数。因此,程序应该默认发送状态码 200。
运行上述程序。这将在你本地机器的 8080 端口上启动一个服务器。现在可以向服务器发出以下 curl 调用
curl -v -X POST http://localhost:8080/example
下面是输出结果
* Connected to localhost (::1) port 8080 (#0)
> POST /example HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Content-Type: application/json
< Date: Sat, 10 Jul 2021 16:24:11 GMT
< Content-Length: 21
<
* Connection #0 to host localhost left intact
{"message":"Success"}
从输出中可以注意到,它返回了 200 状态码。
同时,请查看我们的 Golang 高级教程系列 - Golang 高级教程
HTTP - 理解 multipart/form-data 内容类型
来源:
golangbyexample.com/multipart-form-data-content-type-golang/
在 HTTP 上下文中,multipart/form-data内容类型用于提交 HTML 表单。在multipart/form-data的情况下,正如其名称所示,主体由不同部分组成,部分之间用分隔符或边界分隔,每个部分都有自己的头部描述。分隔符或边界也作为头部的一部分发送。
当你通过浏览器在 HTTP 调用中发送 HTML 表单时,数据内容可以以下两种格式发送为请求体。
-
application/x-www-form-urlencoded
-
multipart/form-data
在大多数情况下,可以使用application/x-www-form-urlencoded。application/x-www-form-urlencoded的效率不高
-
发送文件或图像
-
发送大量二进制数据或包含非 ASCII 字符的文本
例如,假设以下数据需要发送。
-
姓名
-
年龄
然后application/x-www-form-urlencoded可以用于发送上述数据。但假设你还需要在请求中发送用户的个人照片。那么数据现在如下
-
姓名
-
年龄
-
照片
在上述情况下,使用application/x-www-form-urlencoded内容类型效率不高。在这种情况下应使用multipart/form-data。因此,对于发送简单表单数据,请使用application/x-www-form-urlencoded,但如果表单数据还需要发送二进制数据,则请使用multipart/form-data
为什么会这样?在我们理解application/x-www-form-urlencoded和multipart/form-data的格式后,我们将回答这个问题。
application/x-www-form-urlencoded将每个非 ASCII 字节编码为 3 个字节。基本上,application/x-www-form-urlencoded内容类型请求体就像一个巨大的查询字符串。类似于 URI 中的查询字符串,它是一个具有以下格式的键值对
key1=value1&key2=value21&key2=value22&key3=value3
发送application/x-www-form-urlencoded时,所有非字母数字字符都会被 URL 编码。所有非字母数字字符(仅限保留)将以以下格式进行 URL 编码
%WW
其中WW是以十六进制格式表示的字母数字字符的 ASCII 码。由于二进制数据中的所有非字母数字字符都经过 URL 编码,1 个字节转换为 3 个字节。因此,大小增加了三倍。因此,如果你发送的是一个文件或图像(这是一大堆二进制数据),那么你的负载将非常大,即几乎是实际负载的三倍。因此,发送大型二进制文件或大量非 ASCII 数据时效率不高。
要全面了解application/x-www-form-urlencoded的格式,请参考此链接
现在让我们理解multipart/form-data的格式。
正如我们之前提到的,multipart/form-data 有不同的部分由分隔符或边界分隔。每个部分都有自己的头部。multipart/form-data 的格式如下
-- <delimiter_or_boundary>Content-Disposition: form-data; name="<key1>"
Content-Type: <content-type>
[DATA]
-- <delimiter_or_boundary>Content-Disposition: form-data; name="<key2>"; filename="<filename>"
Content-Type: <content-type>
[DATA]
--<delimiter_or_boundary>--</delimiter_or_boundary></content-type></filename></key2></delimiter_or_boundary></content-type></key1></delimiter_or_boundary>
如上所述,格式以分隔符或边界开始,并以分隔符或边界结束。上述格式分为两个部分。同时,
-
每个部分由分隔符或边界分隔。
-
每个部分包含自己的头部以描述数据的类型
-
每个部分的 Content-Disposition 头部将是 form-data. 包含名称字段。该字段包含键名。如果该部分是文件,还会有一个 filename 字段
-
每个部分也将包含自己的数据。
每个部分的 Content-Disposition 头部将是 form-data. 如果你发送的是二进制数据,那么
假设我们要将以下数据作为 multipart/form-data 请求的一部分发送
-
name = John
-
age = 23
-
photo = 一些二进制数据
假设分隔符或边界是
xyz
那么格式将如下
--xyz
Content-Disposition: form-data; name="name"
Content-Type: text/plain
John
--xyz
Content-Disposition: form-data; name="age"
Content-Type: text/plain
23
--xyz
Content-Disposition: form-data; name="photo"; filename="photo.jpeg"
Content-Type: image/jpeg
[JPEG DATA]
--xyz--
由于 multipart/form-data 将二进制数据按原样发送,这就是它用于发送文件和大二进制数据的原因。那么问题是,为什么不一直使用 form-data 呢?
原因是,对于小数据,额外的边界字符串和头部的要求会超过任何优化。例如,假设我们要发送以下数据
-
name=John
-
age=23
然后在使用 application/x-www-form-urlencoded 时,数据将被发送为
name=John&age=23
但是在发送 multipart/form-data 时,数据将如下发送,几乎是以 application/x-www-form-urlencoded 发送的数据的十倍。
--xyz
Content-Disposition: form-data; name="name"
Content-Type: text/plain
John
--xyz
Content-Disposition: form-data; name="age"
Content-Type: text/plain
23
--xyz--
结论
这就是关于 multipart/form-data 的所有内容。希望你喜欢这篇文章。请在评论中分享反馈
HTTP 客户端或在 Go(Golang)中发送 x-www-form-urlencoded 请求主体
目录
-
概述
-
示例
概述
application/x-www-form-urlencoded内容类型的请求主体就像一个巨大的查询字符串。类似于 URI 中的查询字符串,它是一个键值对,具有以下格式。
key1=value1&key2=value21&key2=value22&key3=value3
下面是以下键值对
-
key1, value1
-
key2 有两个值,即 value21 和 value22。
-
key3, value3
每个键值对用 & 分隔,如果同一键有多个值,则会有两个条目。并且,每个键和值都经过 URL 编码,类似于查询字符串。
现在可能会有人问,如果x-www-form-urlencoded就像查询字符串,那它为什么存在。原因是查询字符串是 URI 的一部分,而 URI 的长度有限,因此你可以在查询字符串中发送有限数量的键值对。而x-www-form-urlencoded请求主体的长度没有限制。然而,它受到服务器允许的最大请求主体大小的限制,对于大多数服务器通常为 10MB。现在让我们看看如何在 golang 中解析x-www-form-urlencoded
基本上,由于 x-www-form-urlencoded 主体与查询字符串类似,因此在 golang 中使用url.Values表示。golang.org/pkg/net/url/#Values
url.Values 只不过是一个映射
-
key 是一个字符串
-
value 是一个字符串切片
它的表示如下
type Values map[string][]string
示例
下面是相应的程序。这就是我们在程序中创建主体的方式
data := url.Values{}
data.Set("name", "John")
data.Set("age", "18")
data.Add("hobbies", "sports")
data.Add("hobbies", "music")
添加与设置的区别
-
添加将在url.Values映射中的键后追加一个额外值
-
设置将替换 url.Values 映射中现有的键
之后,我们需要对数据进行编码。然后编码后的数据传递给http.NewRequest函数。也不要忘记将内容类型头设置为application/x-www-form-urlencoded
encodedData := data.Encode()
http.NewRequest(method, urlPath, strings.NewReader(encodedData))
相应的完整程序。
package main
import (
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
func main() {
call("http://localhost:8080/employee", "POST")
}
func call(urlPath, method string) error {
client := &http.Client{
Timeout: time.Second * 10,
}
data := url.Values{}
data.Set("name", "John")
data.Set("age", "18")
data.Add("hobbies", "sports")
data.Add("hobbies", "music")
encodedData := data.Encode()
fmt.Println(encodedData)
req, err := http.NewRequest(method, urlPath, strings.NewReader(encodedData))
if err != nil {
return fmt.Errorf("Got error %s", err.Error())
}
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
req.Header.Add("Content-Length", strconv.Itoa(len(data.Encode())))
response, err := client.Do(req)
if err != nil {
return fmt.Errorf("Got error %s", err.Error())
}
defer response.Body.Close()
return nil
}
如果你运行一个监听在:8080 端口的服务器,类似于下面的链接,那么你会看到服务器能够正确打印传入请求的主体
golangbyexample.com/url-encoded-body-golang
/
也是
fmt.Println(encodedData)
将会是
age=18&hobbies=sports&hobbies=music&name=John
它与我们上面讨论的格式相同。
Go 语言中的 HTTP 客户端/服务器 multipart 表单数据请求体示例
目录
** 概览
-
HTTP 服务器示例
-
HTTP 客户端示例
概览
在 HTTP 上下文中,multipart/form-data内容类型用于提交 HTML 表单。在multipart/form-data的情况下,正如名称所示,主体由不同部分组成,这些部分由分隔符或边界分开,每个部分都有自己的头部描述。分隔符或边界也作为头部的一部分发送。
当你通过浏览器在 HTTP 调用中发送 HTML 表单时,数据内容可以通过以下两种格式作为请求体发送。
-
application/x-www-form-urlencoded
-
multipart/form-data
在大多数情况下,可以使用application/x-www-form-urlencoded。但如果需要上传文件,则application/x-www-form-urlencoded就不太高效。
例如,假设需要发送以下数据。
-
姓名
-
年龄
然后可以使用application/x-www-form-urlencoded发送上述数据。但假设你还需要在请求中发送用户的头像。所以现在的数据如下
-
姓名
-
年龄
-
照片
在上述情况下,使用application/x-www-form-urlencoded内容类型将不高效。在这种情况下,应使用multipart/form-data。因此,发送简单表单数据时使用application/x-www-form-urlencoded,但如果表单数据还需要发送二进制数据,则使用multipart/form-data。
现在让我们看一个例子
-
解析 multipart/form-data 请求的 HTTP 服务器
-
用于发送 multipart/form-data 请求的 HTTP 客户端
HTTP 服务器示例
对于 HTTP 服务器,传入的请求由请求结构表示
golang.org/src/net/http/request.go
要解析multipart/form-data请求体,我们需要先在请求对象上调用以下函数
request.ParseMultipartForm()
上述函数的作用是解析传入的请求体,并将数据加载到请求对象的以下字段中
- MultipartForm – 整个multipart/form-data请求体将加载到此字段中。例如,在上述情况下,它将保存姓名、年龄字段以及照片字段。其表示格式如下
type Form struct {
Value map[string][]string
File map[string][]*FileHeader
}
它有两个部分。Value 保存所有非文件数据。因此,它将保存姓名和年龄的键数据。文件部分保存所有文件数据。因此,它将保存照片键的数据。这两个部分的值部分都是数组,因为对于同一个键可以有多个值。
- 表单 – 它包含multipart/form-data请求体的查询字符串和非文件字段的组合数据。例如,在上述情况下,它只会包含名称和年龄字段。它的格式如下:
map[string][]string
- PostForm – 它只包含multipart/form-data请求体的非文件字段。因此在上述情况下,它只会包含名称和年龄字段。
它的格式也与表单相同。
map[string][]string
除上述字段外,请求对象还提供以下实用函数。
-
FormFile – 此函数可用于获取给定键的第一个文件。因此,该函数只会返回照片键的数据,而不返回名称和年龄键的数据。
-
PostFormValue – 此函数返回给定表单键的第一个值。该函数只会返回名称和年龄键的数据,而不返回照片键的数据。
让我们看看可以以不同方式访问名称、键和照片值。
名称键
request.Form["name"]
request.PostForm["name"]
request.MultipartForm.Value["name"]
将返回
["John"]
它返回一个数组,因为对于同一个键可以有不同的值。
当
request.PostFormValue("name")
将返回
John
它不是一个数组,因为如上所述,此函数返回与键关联的第一个值。
年龄键
request.Form["age"]
request.PostForm["age"]
request.MultipartForm.Value["age"]
将返回
["21"]
当
request.PostFormValue("age")
将返回
21
照片键
request.MultipartForm.File["photo"]
将返回该图像的FileHeader。图像的字节可以通过它访问。
[FileHeader_object_of_the_image]
当
request.FormFile("photo")
将返回
FileHeader_object_of_the_image
它不是一个数组,因为如上所述,此函数返回与键关联的第一个文件。
现在让我们看看一个示例。
package main
import (
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
createEmployeeHanlder := http.HandlerFunc(createEmployee)
http.Handle("/employee", createEmployeeHanlder)
http.ListenAndServe(":8080", nil)
}
func createEmployee(w http.ResponseWriter, request *http.Request) {
err := request.ParseMultipartForm(32 << 20) // maxMemory 32MB
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
//Access the name key - First Approach
fmt.Println(request.Form["name"])
//Access the name key - Second Approach
fmt.Println(request.PostForm["name"])
//Access the name key - Third Approach
fmt.Println(request.MultipartForm.Value["name"])
//Access the name key - Fourth Approach
fmt.Println(request.PostFormValue("name"))
//Access the age key - First Approach
fmt.Println(request.Form["age"])
//Access the age key - Second Approach
fmt.Println(request.PostForm["age"])
//Access the age key - Third Approach
fmt.Println(request.MultipartForm.Value["age"])
//Access the age key - Fourth Approach
fmt.Println(request.PostFormValue("age"))
//Access the photo key - First Approach
_, h, err := request.FormFile("photo")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
saveFile(h, "formfile")
//Access the photo key - Second Approach
for _, h := range request.MultipartForm.File["photo"] {
err := saveFile(h, "mapaccess")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
}
w.WriteHeader(200)
return
}
注意在上述程序中,我们是如何使用所有方法打印名称键的。
//Access the name key - First Approach
fmt.Println(request.Form["name"])
//Access the name key - Second Approach
fmt.Println(request.PostForm["name"])
//Access the name key - Third Approach
fmt.Println(request.MultipartForm.Value["name"])
//Access the name key - Fourth Approach
fmt.Println(request.PostFormValue("name"))
对于年龄键也是类似的。
然后我们使用FormFile方法首先保存请求体中的文件,然后使用request.MultipartForm.File映射进行保存。由于我们使用这两种方法保存同一个文件,因此我们向saveFile函数发送一个前缀参数,以便它以不同的名称保存文件。这是为了确保FormFile保存的文件不会被request.MultipartForm.File保存的文件覆盖。这只是为了演示这两种方法都可以用于从请求中提取文件数据。FormFile方法使用前缀formfile,而request.MultipartForm.File使用mapaccess前缀来保存文件。
现在让我们进行一些 curl 调用。在运行 curl 命令的同一文件夹中创建一个名为test.png的文件。
- 发送名称、年龄和照片。
curl --location --request PUT 'http://localhost:8080/employee' \
--header 'Content-Type: multipart/form-data' \
--form 'name=John' \
--form 'age=23' \
--form 'photo=@test.png'
输出
从输出中可以注意到,它正好打印了我们之前讨论的内容。
[John]
[John]
[John]
John
[23]
[23]
[23]
23
它还会保存两个文件,文件名如下:
formfile-test.png
mapaccess-test.png
现在让我们看看第二个 curl 示例。
- 发送名称、年龄和照片,但在此示例中,同一个键'照片'用于发送两个文件。
curl --location --request PUT 'http://localhost:8080/employee' \
--header 'Content-Type: multipart/form-data' \
--form 'name=John' \
--form 'age=23' \
--form 'photo=@test1.png' \
--form 'photo=@test2.png'
输出
[John]
[John]
[John]
John
[23]
[23]
[23]
23
这次将保存三个文件,文件名如下。由于FormFile只访问给定键下的第一个文件,因此只有formfile-test1.png会通过FormFile方法保存。
formfile-test1.png
mapaccess-test2.png
mapaccess-test2.png
让我们看看另一个 curl 示例。
- 发送两个 name 键的值
curl --location --request PUT 'http://localhost:8080/employee' \
--header 'Content-Type: multipart/form-data' \
--form 'name=John' \
--form 'name=John2' \
--form 'age=23' \
--form 'photo=@test.png'
输出
[John John2]
[John John2]
[John John2]
John
[23]
[23]
[23]
23
rqeuest.PostFormValue 返回
John
而所有其他选项则返回两个值
[John John2]
HTTP 客户端示例
以下是一个 HTTP 客户端示例代码,它在 HTTP 请求中将 multipart/form-data 请求体发送到上面示例中创建的服务器。
首先,我们必须创建一个多部分写入器 golang.org/pkg/mime/multipart/#Writer
writer := multipart.NewWriter(body)
多部分写入器提供两种方法
-
CreateFormField - 用于创建要在多部分请求体中发送的文本字段。我们将使用此方法创建 name 和 age 字段。
-
CreateFormFile - 用于创建要在多部分请求体中发送的文件字段。我们将使用此方法创建 photo 字段。
现在让我们看看程序
package main
import (
"bytes"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"strings"
"time"
)
func main() {
call("http://localhost:8080/employee", "POST")
}
func call(urlPath, method string) error {
client := &http.Client{
Timeout: time.Second * 10,
}
// New multipart writer.
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormField("name")
if err != nil {
}
_, err = io.Copy(fw, strings.NewReader("John"))
if err != nil {
return err
}
fw, err = writer.CreateFormField("age")
if err != nil {
}
_, err = io.Copy(fw, strings.NewReader("23"))
if err != nil {
return err
}
fw, err = writer.CreateFormFile("photo", "test.png")
if err != nil {
}
file, err := os.Open("test.png")
if err != nil {
panic(err)
}
_, err = io.Copy(fw, file)
if err != nil {
return err
}
// Close multipart writer.
writer.Close()
req, err := http.NewRequest("POST", "http://localhost:8080/employee", bytes.NewReader(body.Bytes()))
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
rsp, _ := client.Do(req)
if rsp.StatusCode != http.StatusOK {
log.Printf("Request failed with response code: %d", rsp.StatusCode)
}
return nil
}
运行这个程序,观察在服务器端的输出,因为上述客户端会调用你在第一个示例中创建的服务器。服务器程序将给出类似于 curl 调用的输出。
[John]
[John]
[John]
John
[23]
[23]
[23]
23
此外,服务器将保存两个文件,名称与 curl 调用类似
formfile-test.png
mapaccess-test.png
Go(Golang)中的带基本身份验证的 HTTP 客户端/服务器。
目录
概述
-
HTTP 服务器基本身份验证示例
-
HTTP 客户端基本身份验证示例 * * # 概述
基本身份验证是为网络服务器上的资源提供访问控制的最简单形式。基本访问认证是在进行 HTTP 请求时向服务器提供用户名和密码的一种方式。凭据在请求的头部发送。以下是发送凭据的头部和格式。
Authorization : Basic <credentials></credentials>
-
头部名称是 “Authorization”
-
凭据是通过冒号(:)连接的用户名和密码进行 Base64 编码发送的。基本上是对以下字符串的 Base64 编码。
<username>:<password></password></username>
- 凭据以 Basic 开头。
基本身份验证在 RFC 7617 中有说明。
基本身份验证不需要任何类型的会话标识符或 Cookies。此外,由于凭据仅作为 Base64 编码发送,因此没有加密涉及。因此,基本身份验证仅在 HTTPS 中使用以确保安全。
HTTP 服务器基本身份验证示例
首先,让我们看看与 HTTP 服务器相关的基本身份验证。Golang 的 net/http 包提供了一种在 *http.Request 结构上定义的方法,可以返回传入请求的 Authorization 头中存在的用户名和密码。以下是该方法的签名。
func (r *Request) BasicAuth() (username, password string, ok bool)
这个方法的作用是检查 Authorization 头,然后从 Base64 编码值中提取用户名和密码并返回。如果解析时出现任何问题,它将返回 ok 变量为 false。因此,在使用此函数时,我们首先需要检查 ok 变量的值。如果 ok 变量为 true,则可以进一步匹配用户名和密码并验证其是否正确。
让我们看看一个程序。
package main
import (
"fmt"
"net/http"
)
var (
username = "abc"
password = "123"
)
func main() {
handler := http.HandlerFunc(handleRequest)
http.Handle("/example", handler)
http.ListenAndServe(":8080", nil)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
u, p, ok := r.BasicAuth()
if !ok {
fmt.Println("Error parsing basic auth")
w.WriteHeader(401)
return
}
if u != username {
fmt.Printf("Username provided is correct: %s\n", u)
w.WriteHeader(401)
return
}
if p != password {
fmt.Printf("Password provided is correct: %s\n", u)
w.WriteHeader(401)
return
}
fmt.Printf("Username: %s\n", u)
fmt.Printf("Password: %s\n", p)
w.WriteHeader(200)
return
}
服务器接受以下用户名和密码。
-
用户名是 abc。
-
密码是 123。
为了发出请求,我们必须对以下字符串进行 Base64 编码,该字符串是由一个冒号(:)连接的用户名和密码。
abc:123
上述字符串的 Base64 编码将是。
YWJjOjEyMw==
你可以在这里查看 – www.base64encode.org/
现在发出以下请求。
curl -v -X POST http://localhost:8080/example -H "Authorization: Basic YWJjOjEyMw=="
此请求将获得状态代码 200。同时,它还会在日志中正确打印用户名和密码。
Username: abc
Password: 123
现在发送格式错误的 Base64 编码值。
curl -v -X POST http://localhost:8080/example -H "Authorization: Basic YWJjOjEy"
它将获得状态代码 401。它还将在日志中打印以下内容。
Error parsing basic auth
HTTP 客户端基本身份验证示例
Golang 的 net/http 包还提供了一种在 *http.Request 结构上定义的方法,可用于设置基本身份验证头。以下是该方法的签名。
func (r *Request) SetBasicAuth(username, password string)
该方法的作用是接收用户名和密码,并使用以冒号(:)连接的用户名和密码的 base64 编码值设置 Authorization 头。
让我们看看一个程序。
package main
import (
"fmt"
"net/http"
"time"
)
var (
username = "abc"
password = "123"
)
func main() {
call("https://localhost:8080/example", "POST")
}
func call(url, method string) error {
client := &http.Client{
Timeout: time.Second * 10,
}
req, err := http.NewRequest(method, url, nil)
if err != nil {
return fmt.Errorf("Got error %s", err.Error())
}
req.SetBasicAuth(username, password)
response, err := client.Do(req)
if err != nil {
return fmt.Errorf("Got error %s", err.Error())
}
defer response.Body.Close()
return nil
}
在上面的程序中,我们正在调用之前设置的服务器。看看我们如何在发出的请求中设置基本认证。
req.SetBasicAuth(username, password)
如果你运行上面的程序,你将从我们之前设置的服务器收到 200 的响应。
注意: 我们在上面打印用户名和密码仅用于示例目的。在实际程序中,它们需要以某种安全的方式加密存储。同时,我们在下面发送的是 http 请求而非 https 用于基本认证。这仅用于说明,客户端和服务器位于同一台机器上。这并不推荐,基本认证应仅在HTTPS下使用。
HTTP 客户端超时:所有主要类型的客户端超时
来源:
golangbyexample.com/all-types-client-timeouts-http-tcp-udp-unix/
目录
-
概述
-
拨号超时:
-
TLS 握手超时:
-
响应头超时:
-
请求超时:
-
套接字超时:
-
等待连接超时:
-
空闲连接超时:
概述
在本文中,我们将研究在连接客户端与服务器时可能在 HTTP、TCP、UDP 或 Unix 套接字通信中出现的不同类型的超时。
还有一个称为服务器超时的概念,指定服务器何时可以关闭传入请求。补充一下,服务器超时存在是为了保护服务器免受恶意客户端或长时间保持连接的慢客户端的影响。
对于客户端超时,请注意,不同编程语言中的不同库可能无法为您提供设置以下描述的所有超时的选项。例如,在 Golang 的 net http 包中,Client 结构体(golang.org/pkg/net/http/#Client
)允许您设置单个超时。该超时指定以下两个超时。
-
请求超时
-
套接字超时
让我们了解所有类型的超时:
拨号超时:
Dial Timeout 指定拨号在命名网络上完成连接所等待的最长时间。命名网络可以是“TCP”,“UDP”,“IP”,“unix”等。这是指从服务器完全没有响应的情况。可能发生的原因包括:
-
错误的 DNS
-
远程服务器没有响应
-
远程服务器已经挂掉
-
连接到远程服务器时使用了错误的端口
示例
假设 DialTimeout 为 1 秒。客户端正在拨打 TCP 连接,但在 1 秒内未收到来自服务器的任何响应,则会发生 DialTimeout。
TLS 握手超时:
适用于 SSL 请求。每当与远程服务器建立连接时,会发生 TLS 握手,在此过程中交换 SSL 密钥并进行服务器的 SSL 证书验证。TLS 握手 超时指定了等待 TLS 握手建立的最大时间。
示例:
假设一个 HTTP 请求的情况。假设 TLS 握手超时为 1 秒。客户端最多等待 1 秒以完成 TLS 握手。
响应头超时:
在客户端与服务器完成 TLS 握手并完全发送请求头和请求体后,响应头超时就会生效。这个超时指定了等待接收服务器响应头的最长时间。
示例:
假设响应头超时是 1 秒。客户端发起 HTTP 请求,TLS 握手成功。然后客户端将请求头和请求体发送给服务器。在这种情况下,如果在 1 秒内没有接收到服务器的响应头,响应头超时将会发生。
请求超时:
请求超时从拨号连接到服务器开始,到接收到远程服务器的整个响应体为止。它表示等待整个客户端请求完成的最长时间。想象一下一个 HTTP 请求,请求超时指定了它等待整个 HTTP 请求完成的最长时间。这是完整请求的超时,因此包括拨号超时、TLS 握手超时以及响应头超时,再加上等待响应体的额外时间。
示例:
假设你的请求超时是 1 秒,那么客户端将从拨号开始等待最长 1 秒以接收整个响应,之后会超时。
套接字超时:
这个超时主要与长连接相关。与远程服务器建立了连接,并且也传输了一些字节。如果这段字节的数据流在特定时间内中断,那么套接字超时就会出现。
示例:
假设你的套接字超时时间是 1 秒,如果在接下来的 1 秒内没有新字节传输,那么将会导致套接字超时。将套接字超时时间设置为 1 秒意味着每秒应该接收到一些数据。因此,套接字超时是等待单个数据包,并不意味着等待完整响应的超时。
等待连接超时:
这个超时适用于固定连接池。这是等待池中可用的活动连接的超时。
示例:
以数据库连接池为例。假设你正在使用 MYSQL 服务器,池中的最大连接数是 20,而所有连接目前都被使用。你的“等待连接超时”设置为 1 秒。新的请求到来,并需要一个连接来连接到数据库。由于所有 20 个连接都被占用,因此在这种情况下,新请求将最多等待 1 秒以获得可用连接,然后超时。在这种情况下,你可能会得到“连接数过多”的错误。
闲置连接超时:
这个超时也适用于连接池。闲置连接超时指定了一个未使用连接应该保留的时间。
示例:
想象一个包含 20 个连接的连接池。在某个特定时刻,5 个连接正在并行使用。但之后,没有新的请求。因此,这 5 个连接将在“闲置连接超时”的值之前等待释放。闲置连接超时为 1 秒应该意味着某个连接不得闲置超过 1 秒,否则它将被释放,并为任何新请求创建一个新连接。
HTTP 客户端在 Go (Golang) 中不跟随重定向。
目录
-
概述
-
CheckDirect 是 nil
-
CheckDirect 是非 nil
概述
http.Client 结构用于在 golang 中发起 HTTP 请求。
golang.org/src/net/http/client.go
http.Client 让你指定重定向处理的策略。以下是 http.Client 结构的结构。
type Client struct {
Transport RoundTripper
CheckRedirect func(req *Request, via []*Request) error
Jar CookieJar
Timeout time.Duration
}
CheckRedirect 字段实际上是一个函数,它允许你指定处理重定向的方式。此函数在跟随任何未来的重定向之前被调用。因此,基本上,每当服务器响应重定向时,客户端会首先调用 CheckRedirect 函数以检查是否应跟随重定向。以下是 CheckRedirect 函数的签名。
func(req *Request, via []*Request) error
-
第一个参数 req 是即将到来的请求。
-
第二个参数 via 是已经发出的请求。这就是它是一个切片的原因。第一个元素是最旧的请求。
有两种情况需要考虑。
CheckDirect 是 nil
在初始化 http.Client 结构时,你不指定 CheckRedirect 字段。在这种情况下,将启用默认行为,HTTP 客户端将跟随 10 次重定向,然后返回错误。以下是 defaultCheckRedirect 函数,它在尝试 10 次后退出。
func defaultCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 10 {
return errors.New("stopped after 10 redirects")
}
return nil
}
CheckDirect 是非 nil
在初始化 http.Client 结构时,你指定自己的 CheckRedirect 函数。当你指定此函数时,它将在跟随任何未来的重定向之前被调用。当你编写自己的 CheckRedirect 函数时,需要记住以下内容。
当 CheckRedirect 函数返回错误时,HTTP 客户端将返回两件事。
-
上一个响应以及请求体已关闭。
-
实际错误(包装在 url.Error 中)。
还有一种特殊情况,当 CheckRedirect 函数返回 ErrUseLastResponse 错误时。在这种情况下,客户端将返回。
*** 最近的响应以及未关闭的请求体。
- 非 nil 错误
所以,基本上当你不想让 HTTP 客户端跟随重定向时,请指定自己的 CheckRedirect 函数以控制行为。例如,以下是一个示例 CheckRedirect 函数,它会通知 HTTP 客户端不跟随任何重定向,并且不返回任何错误,仅返回最后的响应以及未关闭的请求体。
func MyCheckRedirect(req *Request, via []*Request) error {
return http.ErrUseLastResponse
}
以下是 http 包中 ErrUseLastResponse 的定义。
var ErrUseLastResponse = errors.New("net/http: use last response")
创建 http.Client 如下,指定 CheckRedirect 函数。
client := &http.Client{
CheckRedirect: func(req *Request, via []*Request) error {
return http.ErrUseLastResponse
},
}
这是另一个示例 CheckRedirect 函数,它将只跟随重定向两次,然后返回错误。
func MyCheckRedirect(req *Request, via []*Request) error {
if len(via) >= 2 {
return errors.New("stopped after 10 redirects")
}
return nil
}
- 重定向***
在 Go(Golang)中发送/接收application/octet-stream
请求主体。
目录
-
概述
-
HTTP 客户端示例
-
HTTP 服务器示例
概述
application/octet-stream内容类型用于在 HTTP 请求主体中传输二进制数据。因此,application/octet-stream用于通过 HTTP 请求发送文件。让我们看一个 golang 中 HTTP 客户端和服务器的示例,用于发送和接收application/octet-stream数据。
让我们首先创建两个文件夹。
-
客户端
-
服务器
同时在client文件夹中创建一个名为photo.png的文件。
HTTP 客户端示例
下面是相应的程序。
client/main.go
package main
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"net/http"
"time"
)
func main() {
err := call("http://localhost:8080/photo", "POST")
if err != nil {
fmt.Printf("Error occurred. Err: %s", err.Error())
}
}
func call(urlPath, method string) error {
client := &http.Client{
Timeout: time.Second * 10,
}
b, err := ioutil.ReadFile("photo.png")
if err != nil {
return err
}
req, err := http.NewRequest(method, urlPath, bytes.NewReader(b))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/octet-stream")
rsp, _ := client.Do(req)
if rsp.StatusCode != http.StatusOK {
log.Printf("Request failed with response code: %d", rsp.StatusCode)
}
return nil
}
在上述程序中,我们将文件photo.png作为application/octet-stream请求主体发送在 POST 请求中。首先将 photo.png 转换为字节,这些字节作为application/octet-stream主体发送。为此,首先读取文件的字节。
b, err := ioutil.ReadFile("photo.png")
文件转换为字节后,它将作为第三个参数传递给http.NewRequest方法,如下所示。
bytes.NewReader(b)
bytes.NewReader返回bytes.Reader实例。bytes.Reader实现了io.Reader和io.Writer。http.NewRequest方法接受io.Reader
作为主体部分。因此,bytes.Reader实例可以作为第三个参数传递给 http.NewRequest。
req, err := http.NewRequest(method, urlPath, bytes.NewReader(b))
在上述程序中,我们还调用了以下 API。
http://localhost:8080/photo
让我们创建一个在端口8080上监听的服务器,以便我们也可以测试上述客户端。此外,服务器将解析application/octet-stream
主体并将其保存为photo.png。现在让我们看看服务器的程序。
HTTP 服务器示例
下面是相应的程序。
server/main.go
package main
import (
"io/ioutil"
"net/http"
"os"
)
func main() {
createEmployeeHanlder := http.HandlerFunc(createEmployee)
http.Handle("/photo", createEmployeeHanlder)
http.ListenAndServe(":8080", nil)
}
func createEmployee(w http.ResponseWriter, request *http.Request) {
d, err := ioutil.ReadAll(request.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
tmpfile, err := os.Create("./" + "photo.png")
defer tmpfile.Close()
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
}
tmpfile.Write(d)
w.WriteHeader(200)
return
}
在上述程序中,我们创建了一个将在端口 8080 上监听的 API。API 签名将是:
http://localhost:8080/photo
它从 POST 主体读取字节并将其保存到文件photo.png中。
让我们运行服务器和客户端。先运行服务器。
go run server/main.go
它将开始监听端口 8080。之后运行客户端。
go run client/main.go
API 成功执行后,请检查服务器端。服务器端将创建一个名为photo.png的文件。
HTTP 在请求体中发送/接收 jpeg 文件的示例(使用 Go 语言)
目录
概览
-
HTTP 服务器
-
HTTP 客户端
概览
multipart/form-data 内容类型可用于在 HTTP POST 调用中发送 jpeg 文件。表单数据将包含
-
jpeg 文件名 - test.jpeg,在本教程中我们将看到的示例
-
将包含 jpeg 文件内容的键 – 本教程示例中的 photo
让我们来看一个 HTTP 客户端 和 服务器 的示例
HTTP 服务器
以下是相同的程序。
package main
import (
"io"
"net/http"
"os"
)
func main() {
createImageHandler := http.HandlerFunc(createImage)
http.Handle("/image", createImageHandler)
http.ListenAndServe(":8080", nil)
}
func createImage(w http.ResponseWriter, request *http.Request) {
err := request.ParseMultipartForm(32 << 20) // maxMemory 32MB
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
//Access the photo key - First Approach
file, h, err := request.FormFile("photo")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
tmpfile, err := os.Create("./" + h.Filename)
defer tmpfile.Close()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = io.Copy(tmpfile, file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(200)
return
}
让我们也理解一下程序。我们需要做的第一件事是调用 ParseMultipartForm 函数在 request 对象上
request.ParseMultipartForm()
它将解析表单数据请求体。之后,我们可以在请求对象上调用 FormFile 函数,传入键作为参数。它将返回给定键的 multipart.File 对象,这里是 "photo"。该对象是访问该键的多部分消息文件部分的接口。程序使用它将文件保存到磁盘。
_, err = io.Copy(tmpfile, file)
这就是 HTTP 服务器示例。运行服务器。它将在 8080 端口监听。让我们创建一个 HTTP 客户端来测试上述服务器。以下是该客户端的代码。
HTTP 客户端
package main
import (
"bytes"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"time"
)
func main() {
call("http://localhost:8080/image", "POST")
}
func call(urlPath, method string) error {
client := &http.Client{
Timeout: time.Second * 10,
}
// New multipart writer.
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormFile("photo", "test.jpg")
if err != nil {
return err
}
file, err := os.Open("test.jpg")
if err != nil {
return err
}
_, err = io.Copy(fw, file)
if err != nil {
return err
}
writer.Close()
req, err := http.NewRequest(method, urlPath, bytes.NewReader(body.Bytes()))
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
rsp, _ := client.Do(req)
if rsp.StatusCode != http.StatusOK {
log.Printf("Request failed with response code: %d", rsp.StatusCode)
}
return nil
}
以下是 HTTP 客户端示例的代码。它在 HTTP 请求中向上述创建的服务器发送 multipart/form-data 请求体。
首先,我们必须创建一个多部分写入器 golang.org/pkg/mime/multipart/#Writer
writer := multipart.NewWriter(body)
多部分写入器提供了 CreateFormFile 方法,可以用于创建要在多部分请求体中发送的文件字段。文件名 是 test.jpg,键 名称是 "photo"。运行上述文件。
它将把 test.jpg 发送到上述创建的服务器。请求完成后,你可以在服务器端检查。服务器将解析表单数据请求体,然后将文件内容保存到同名文件中。
在 Go(Golang)中请求体中发送/接收 pdf 文件的示例
目录
-
概述
-
HTTP 服务器
-
HTTP 客户端
概述
multipart/form-data内容类型可用于在 HTTP POST 调用中发送 pdf 文件。表单数据将包含
-
pdf 文件名 - 本教程中将看到的test.pdf
-
将包含 pdf 文件内容的键 - 本教程示例中的photo
让我们看看 HTTP 客户端和服务器的示例
HTTP 服务器
以下是相应的程序。
package main
import (
"io"
"net/http"
"os"
)
func main() {
createImageHandler := http.HandlerFunc(createImage)
http.Handle("/pdf", createImageHandler)
http.ListenAndServe(":8080", nil)
}
func createImage(w http.ResponseWriter, request *http.Request) {
err := request.ParseMultipartForm(32 << 20) // maxMemory 32MB
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
//Access the photo key - First Approach
file, h, err := request.FormFile("photo")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
tmpfile, err := os.Create("./" + h.Filename)
defer tmpfile.Close()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = io.Copy(tmpfile, file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(200)
return
}
让我们也理解程序。我们需要做的第一件事是在请求对象上调用ParseMultipartForm函数
request.ParseMultipartForm()
它将解析表单数据请求体。之后,我们可以在请求对象上调用FormFile函数,传入键作为参数。它将返回给定键的multipart.File对象,这里的键是"photo"。该对象是访问该键的多部分消息文件部分的接口。程序使用它将文件保存到磁盘。
_, err = io.Copy(tmpfile, file)
这是 HTTP 服务器示例。运行服务器。它将在8080端口监听。让我们创建一个 HTTP 客户端来测试上述服务器。以下是相关代码。
HTTP 客户端
package main
import (
"bytes"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"time"
)
func main() {
call("http://localhost:8080/pdf", "POST")
}
func call(urlPath, method string) error {
client := &http.Client{
Timeout: time.Second * 10,
}
// New multipart writer.
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormFile("photo", "test.pdf")
if err != nil {
return err
}
file, err := os.Open("test.pdf")
if err != nil {
return err
}
_, err = io.Copy(fw, file)
if err != nil {
return err
}
writer.Close()
req, err := http.NewRequest(method, urlPath, bytes.NewReader(body.Bytes()))
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
rsp, _ := client.Do(req)
if rsp.StatusCode != http.StatusOK {
log.Printf("Request failed with response code: %d", rsp.StatusCode)
}
return nil
}
以下是一个 HTTP 客户端的示例代码。它在 HTTP 请求中向上面创建的服务器发送multipart/form-data请求体。
首先,我们必须创建一个多部分写入器 golang.org/pkg/mime/multipart/#Writer
writer := multipart.NewWriter(body)
多部分写入器提供了CreateFormFile方法,可以用来创建一个在多部分请求体中发送的文件字段。文件名是test.pdf,键名是"photo"。
运行上述文件。它将把test.pdf发送到上面创建的服务器。请求完成后,你可以在服务器端检查。服务器会解析表单数据请求体,然后将文件内容保存到同名文件中。
Go(Golang)中 HTTP 发送/接收 png 文件的请求体示例
目录
-
概述
-
HTTP 服务器
-
HTTP 客户端
概述
multipart/form-data内容类型可以用于在 HTTP POST 调用中发送 png 文件。表单数据将包含
-
png 文件名- 示例中的test.png,我们将在本教程中看到。
-
包含 png 文件内容的 key – 在教程示例中的photo
让我们来看一下 HTTP 客户端和服务器的示例。
HTTP 服务器
下面是相应的程序。
package main
import (
"io"
"net/http"
"os"
)
func main() {
createImageHandler := http.HandlerFunc(createImage)
http.Handle("/png", createImageHandler)
http.ListenAndServe(":8080", nil)
}
func createImage(w http.ResponseWriter, request *http.Request) {
err := request.ParseMultipartForm(32 << 20) // maxMemory 32MB
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
//Access the photo key - First Approach
file, h, err := request.FormFile("photo")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
tmpfile, err := os.Create("./" + h.Filename)
defer tmpfile.Close()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
_, err = io.Copy(tmpfile, file)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(200)
return
}
让我们也理解一下程序。我们需要做的第一件事是调用ParseMultipartForm函数,传入request对象。
request.ParseMultipartForm()
它将解析表单数据请求体。之后,我们可以在请求对象上调用FormFile函数,并传入 key 作为参数。它将返回给定 key 的multipart.File对象,在这里是"photo"。该对象是访问多部分消息中该 key 的文件部分的接口。程序使用它将文件保存到磁盘。
_, err = io.Copy(tmpfile, file)
这就是 HTTP 服务器示例。运行服务器。它将监听8080端口。让我们创建一个 HTTP 客户端来测试上述服务器。下面是相关代码。
HTTP 客户端
package main
import (
"bytes"
"io"
"log"
"mime/multipart"
"net/http"
"os"
"time"
)
func main() {
call("http://localhost:8080/png", "POST")
}
func call(urlPath, method string) error {
client := &http.Client{
Timeout: time.Second * 10,
}
// New multipart writer.
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
fw, err := writer.CreateFormFile("photo", "test.png")
if err != nil {
return err
}
file, err := os.Open("test.png")
if err != nil {
return err
}
_, err = io.Copy(fw, file)
if err != nil {
return err
}
writer.Close()
req, err := http.NewRequest(method, urlPath, bytes.NewReader(body.Bytes()))
if err != nil {
return err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
rsp, _ := client.Do(req)
if rsp.StatusCode != http.StatusOK {
log.Printf("Request failed with response code: %d", rsp.StatusCode)
}
return nil
}
下面是相同的 HTTP 客户端示例代码。它在 HTTP 请求中向上面创建的服务器发送multipart/form-data请求体。
首先,我们必须创建一个多部分写入器 golang.org/pkg/mime/multipart/#Writer
writer := multipart.NewWriter(body)
多部分写入器提供了CreateFormFile方法,可以用来在多部分请求体中创建一个文件字段。filename是test.png,key名称是"photo"。
运行上述文件。它会将test.png发送到上面创建的服务器。请求完成后,你可以在服务器端检查。服务器将解析表单数据请求体,然后将文件内容保存为同名文件。
HTTP 服务器或在 Go(Golang)中解析传入的 application/x-www-form-urlencoded 请求主体。
注意:这篇文章在服务器端解析application/x-www-form-urlencoded请求。如果你使用 HTTP 客户端并尝试发送application/x-www-form-urlencoded请求,请查看以下链接。
golangbyexample.com/http-client-urlencoded-body-go
/
目录
-
概述
-
示例
概述
application/x-www-form-urlencoded内容类型的请求主体就像一个巨大的查询字符串。类似于 URI 中的查询字符串,它是一个具有以下格式的键值对。
key1=value1&key2=value21&key2=value22&key3=value3
其中有以下键值对。
-
key1, value1
-
key2 有两个值,即 value21 和 value22。
-
key3, value3
每个键值对由&分隔,如果同一键有多个值,则会有两个该键值对的条目。此外,每个键和值都经过 URL 编码,类似于查询字符串。
现在可能会出现的问题是,如果x-www-form-urlencoded就像查询字符串,那么它为何存在。原因是查询字符串是 URI 的一部分,而 URI 的长度有限,因此你只能在查询字符串中发送有限数量的键值对。虽然x-www-form-urlencoded请求主体的长度没有限制,但它受到服务器允许的最大请求主体大小的限制,通常大多数服务器为 10MB。现在,让我们看看如何在 golang 中解析x-www-form-urlencoded。
示例
net/http 包的请求对象有两个字段可以保存表单数据。golang.org/src/net/http/request.go
这两个字段是。
-
Form – 它包含查询字符串和x-www-form-urlencoded请求主体的组合数据。
-
PostForm– 它仅包含x-www-form-urlencoded请求主体。
要获取x-www-form-urlencoded请求主体,我们需要首先在请求对象上调用以下函数。
request.ParseForm()
此函数基本上执行以下操作。
- 它解析 URL 中存在的查询字符串,并填充请求对象的Form字段。
request.Form
- 之后,如果请求方法是 PUT、POST 或 PATCH,且请求主体的内容类型是 x-www-form-urlencoded,则它也会解析请求主体,并填充请求对象的上述两个字段。
request.Form
request.PostForm
-
如果请求主体的内容类型不是 x-www-form-urlencoded,或者请求方法不是 PUT、POST 或 PATCH,则request.PostForm将初始化为非空的空值。
-
主体参数优先于 URL 查询字符串值,即如果在表单主体和查询参数中都有一个键,则表单主体中的值将优先。
-
此外,请注意这个函数是幂等的。
Form 和 PostForm 字段都是以下类型
type Values map[string][]string
这是一个地图,其中
-
键是一个字符串
-
值是一个字符串数组。它是字符串数组,因为可能有多个值。
因此,基本上为了获取 x-www-url-encoded 请求体,我们需要先在请求对象上调用 ParseForm 函数。这将填充 Form 和 PostForm 字段。然后我们可以访问这些字段以获取 x-www-form-urlencoded 请求体。例如,假设我们有以下 x-www-form-urlencoded 请求体。
name=John
age=21
hobbies=sports
hobbies=music
然后你可以像下面这样访问 name 字段
request.Form["name"]
request.PostForm["name"]
两者都会返回
["John"]
它是一个数组,因为键可以有多个值。
此外,请求对象还定义了一个函数 FormValue,可用于获取与键关联的第一个值。它仅返回第一个值,而不是数组。例如
request.FormValue("hobbies")
将返回
sports
让我们看看一个程序
package main
import (
"fmt"
"net/http"
)
type employee struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
createEmployeeHanlder := http.HandlerFunc(createEmployee)
http.Handle("/employee", createEmployeeHanlder)
http.ListenAndServe(":8080", nil)
}
func createEmployee(w http.ResponseWriter, r *http.Request) {
headerContentTtype := r.Header.Get("Content-Type")
if headerContentTtype != "application/x-www-form-urlencoded" {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
r.ParseForm()
fmt.Println("request.Form::")
for key, value := range r.Form {
fmt.Printf("Key:%s, Value:%s\n", key, value)
}
fmt.Println("\nrequest.PostForm::")
for key, value := range r.PostForm {
fmt.Printf("Key:%s, Value:%s\n", key, value)
}
fmt.Printf("\nName field in Form:%s\n", r.Form["name"])
fmt.Printf("\nName field in PostForm:%s\n", r.PostForm["name"])
fmt.Printf("\nHobbies field in FormValue:%s\n", r.FormValue("hobbies"))
w.WriteHeader(200)
return
}
在上述程序中,我们在端口 8080 上运行一个服务器。同时注意,我们首先调用了 ParseForm 函数,然后访问请求对象的 Form 和 PostForm 字段。
另外,看看我们如何在 Form 和 PostForm 中访问 name 字段的值。
r.Form["name"]
r.PostForm["name"]
此外,我们还在请求对象上调用 FormValue 函数以访问 hobbies 字段,如下所示。
r.FormValue("hobbies")
运行上述程序。它将在端口 8080 启动一个服务器。
现在让我们尝试一些 curl 调用。
- 仅传递 x-www-form-urlencoded 请求体。没有查询字符串。
curl -v -X POST 'http://localhost:8080/employee' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'name=John' \
--data-urlencode 'age=18' \
--data-urlencode 'hobbies=sports' \
--data-urlencode 'hobbies=music'
输出
request.Form::
Key:name, Value:[John]
Key:age, Value:[18]
Key:hobbies, Value:[sports music]
request.PostForm::
Key:name, Value:[John]
Key:age, Value:[18]
Key:hobbies, Value:[sports music]
Name field in Form:[John]
Name field in PostForm:[John]
Hobbies field in FormValue:sports
- 传递 x-www-form-urlencoded 请求体。在查询字符串中传递一个额外字段 gender。在输出中注意,性别字段不在 PostForm 中,但在 Form 中。
curl -v -X POST 'http://localhost:8080/employee?gender=male' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'name=John' \
--data-urlencode 'age=18' \
--data-urlencode 'hobbies=sports' \
--data-urlencode 'hobbies=music'
输出
request.Form::
Key:age, Value:[18]
Key:hobbies, Value:[sports music]
Key:gender, Value:[male]
Key:name, Value:[John]
request.PostForm::
Key:name, Value:[John]
Key:age, Value:[18]
Key:hobbies, Value:[sports music]
Name field in Form:[John]
Name field in PostForm:[John]
Hobbies field in FormValue:sports
- 传递 x-www-form-urlencoded 请求体。同时在查询字符串中传递相同的键,即 age。在查询字符串中传递 age=20,在请求体中传递 age=18。age 的值 20 将不在 PostForm 字段中,但会在 Form 字段中。由于请求体参数优先,因此 age=18 是 Form 字段数组中的第一个值。如果你运行 r.FormValue(“age”),则将返回 18 而不是 20。
curl -v -X POST 'http://localhost:8080/employee?age=20' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'name=John' \
--data-urlencode 'age=18' \
--data-urlencode 'hobbies=sports' \
--data-urlencode 'hobbies=music'
输出
request.Form::
Key:hobbies, Value:[sports music]
Key:name, Value:[John]
Key:age, Value:[18 20]
request.PostForm::
Key:name, Value:[John]
Key:age, Value:[18]
Key:hobbies, Value:[sports music]
Name field in Form:[John]
Name field in PostForm:[John]
Hobbies field in FormValue:sports