diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index e057b17..868a7de 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -68,6 +68,7 @@ const Navbar = [ ]; export default defineUserConfig({ + port: 8081, lang: "zh-CN", title: "瓢儿白施肥记", description: "记录一些学习内容", diff --git a/docs/backend/go/web/index.md b/docs/backend/go/web/index.md index d00299a..79a708e 100644 --- a/docs/backend/go/web/index.md +++ b/docs/backend/go/web/index.md @@ -335,7 +335,7 @@ URL Fragment Request Header -- 请求和相应(Request、Response)的 headers 是通过 Header 类型来描述的,它是一个 map,用来表述 HTTP Header 里的 Key-Value 对 +- 请求和响应(Request、Response)的 headers 是通过 Header 类型来描述的,它是一个 map,用来表述 HTTP Header 里的 Key-Value 对 - Header map 的 key 是 string 类型,value 是一个字符串切片 []string - 设置 key 的时候会创建一个空的 []string 作为 value,value 里面第一个元素就是新 header 的值 - 为指定的 key 添加一个新的 header 值,执行 append 操作即可 @@ -359,7 +359,7 @@ server.ListenAndServe() Request Body -- 请求和相应的 bodies 都是使用 Body 字段来表示的 +- 请求和响应的 bodies 都是使用 Body 字段来表示的 - Body 是一个 io.ReadCloser 接口 ```go @@ -421,3 +421,354 @@ URL Query server.ListenAndServe() ``` + +#### Form + +Request 上的函数允许从 URL 或 / 和 Body 中提取数据,通过如下字段 + +- Form +- PostForm +- MultipartForm +- FormValue +- PostFormValue +- FormFile +- MultiPartReader + +**Form 里面的数据是 key-value 对** + +通常的做法是: + +- 先调用 ParseForm 或 ParseMultipartForm 来解析 Request +- 然后相应地访问 Form、PostForm、MultipartForm 字段 + +```html +
+ + + +
+``` + +```go +server := http.Server{ + Addr: "localhost:8080", +} +http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号]] +}) +server.ListenAndServe() +``` + +**PostForm 字段** + +- 上例中,如果只想得到 name 这个 Key 的 Value,可以使用 r.Form["name"],它返回含有一个元素的 slice:["客户 1 号"] +- 如果表单和 URL 里有同样的 Key,那么它们都会放在一个 slice 里:表单里的值靠前,URL 的值靠后 +- 如果只想要表单的 key-value 对,不要 URL 的,可以使用 PostForm 字段 + +```html {2} +
+ + + +
+``` + +```go {6-7} +server := http.Server{ + Addr: "localhost:8080", +} +http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) { + r.ParseForm() + fmt.Fprintln(w, r.Form) // map[email:[2439639832@qq.com] name:[客户1号 客户2号]] + fmt.Fprintln(w, r.PostForm) // map[email:[2439639832@qq.com] name:[客户1号]] +}) +server.ListenAndServe() +``` + +**MultipartForm 字段** + +- PostForm 只支持 application/x-www-form-urlencoded 编码 +- 要想得到 multipart/form-data 对,必须使用 MultipartForm 字段 +- 要想使用 MultiPartForm 字段,必须先调用 ParseMultipartForm 方法 + - 该方法会在必要时调用 ParseForm 方法 + - 参数是需要读取数据的长度 +- MultipartForm 只包含表单的 key-value 对 +- 返回类型是一个 struct,这个 struct 里面有两个 map: + - key 是 string,value 是 []string + - key 是 string,value 是 文件 + +```html {4} +
+ + + +
+``` + +```go {6} +server := http.Server{ + Addr: "localhost:8080", +} +http.HandleFunc("/process", func(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(1024) + fmt.Fprintln(w, r.MultipartForm) // &{map[email:[2439639832@qq.com] name:[客户1号]] map[]} +}) +server.ListenAndServe() +``` + +**FormValue 和 PostFormValue 方法** + +- FormValue 会返回 Form 字段中指定 key 对应的第一个 value + - 无需调用 ParseForm 或 ParseMultipartForm +- PostFormValue 也一样,但只能读取 PostForm 字段 +- FormValue 和 PostFormValue 都会调用 ParseMultipartForm 方法 + +**上传文件** + +multipart/form-data 最常见的应用场景就是上传文件 + +- 首先调用 ParseMultiPartForm 方法 +- 从 File 字段获得 FileHeader,调用其 Open 方法来获得文件 +- 可以使用 io.ReadAll 函数把文件内容读取到 byte 切片里 + +```html +
+ + + + +
+``` + +```go +func process(w http.ResponseWriter, r *http.Request) { + r.ParseMultipartForm(1024) + + fileHeader := r.MultipartForm.File["file"][0] + file, err := fileHeader.Open() + if err == nil { + data, err := io.ReadAll(file) + if err == nil { + fmt.Fprintln(w, string(data)) + } + } +} + +func main() { + server := http.Server{ + Addr: "localhost:8080", + } + + http.HandleFunc("/process", process) + + server.ListenAndServe() +} + +``` + +**FormFile 方法** + +- 上传文件还有一个简便方法:FormFile + - 无需调用 ParseMultipartForm 方法 + - 返回指定 key 对应的第一个 value + - 同时返回 File 和 FileHeader,以及错误信息 + - 如果只上传一个文件,那么这种方式会快一些 + +```go +func process(w http.ResponseWriter, r *http.Request) { + // r.ParseMultipartForm(1024) + + // fileHeader := r.MultipartForm.File["file"][0] + // file, err := fileHeader.Open() + + file, _, err := r.FormFile("file") + + if err == nil { + data, err := io.ReadAll(file) + if err == nil { + fmt.Fprintln(w, string(data)) + } + } +} +``` + +**MultiPartReader()** + +- func (r *Request) MultiPartReader() (*multipart.Reader, error) +- 如果是 multipart/form-data 或 multipart 混合的 POST 请求 + - MultiPartReader 方法会返回一个 MIME multipart reader + - 否则返回一个错误 +- 可以使用该函数代替 ParseMultipartForm,来把请求的 body 作为 stream 进行处理 + - 不是把表单作为一个对象来处理的,不是一次性获得整个 map + - 逐个检查来自表单的值,然后每次处理一个 + +#### POST 请求 - JSON Body + +- 不是所有的 POST 请求都来自 Form +- 客户端框架(例如 Angular 等)会议不同的方式对 POST 请求编码: + - jQuery 通常使用 application/x-www-form-urlencoded + - Angular 通常使用 application/json +- ParseForm 方法无法处理 application/json + +### 响应 + +#### ResponseWriter + +- 从服务器向客户端返回响应需要使用 ResponseWriter +- ResponseWriter 是一个接口,handler 用它来返回响应 +- 真正支撑 ResponseWriter 的幕后 struct 是非导出的 http.response + +**写入到 ResponseWriter** + +- Write 方法接收一个 byte 切片作为参数,然后把它写入到 HTTP 响应的 body 里 +- 如果在 Write 方法被调用时, header 里面没有设定 content-type,那么数据的前 512 字节就会用来被检测 content type + +```go +func writeExample(w http.ResponseWriter, r *http.Request) { + str := ` +Go Web +

Hello World

+ +` + w.Write([]byte(str)) +} + + +func main() { + server := http.Server{ + Addr: "localhost:8080", + } + http.HandleFunc("/write", writeExample) + server.ListenAndServe() +} +``` + +```bash +curl -i localhost:8080/write + +# HTTP/1.1 200 OK +# Date: Sun, 15 Oct 2023 08:38:27 GMT +# Content-Length: 84 +# Content-Type: text/html; charset=utf-8 + +# +# Go Web +#

Hello World

+# +``` + +#### WriteHeader 方法 + +- WriteHeader 方法接收一个整数类型(HTTP 状态码)作为参数,并把它作为 HTTP 响应的状态码返回 +- 如果该方法没有显示调用,那么在第一次调用 Write 方法前,会隐式地调用 WriteHeader(http.StatusOK) + - 所以 WriteHeader 主要用来发送错误类的 HTTP 状态码 +- 调用完 WriteHeader 方法之后,仍然可以写入到 ResponseWriter,但无法再修改 header 了 + +```go +func writeHeaderExample(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(501) + fmt.Fprintln(w, "No such service, try next door") +} + + +func main() { + server := http.Server{ + Addr: "localhost:8080", + } + http.HandleFunc("/writeheader", writeHeaderExample) + server.ListenAndServe() +} +``` + +```bash +curl -i localhost:8080/writeheader + +# HTTP/1.1 501 Not Implemented +# Date: Sun, 15 Oct 2023 12:44:53 GMT +# Content-Length: 31 +# Content-Type: text/plain; charset=utf-8 + +# No such service, try next door +``` + +#### Header 方法 + +- Header 方法返回 headers 的 map,可以进行修改 +- 修改后的 headers 将会体现在返回给客户端的 HTTP 响应里 + +```go +type Post struct { + User string + Threads []string +} + +func headerExample(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Location", "http://google.com") + w.WriteHeader(302) +} + +func jsonExample(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + post := &Post{ + User: "Sau Sheong", + Threads: []string{"first", "second", "third"}, + } + json, _ := json.Marshal(post) + w.Write(json) +} + + +func main() { + server := http.Server{ + Addr: "localhost:8080", + } + http.HandleFunc("/header", headerExample) + http.HandleFunc("/json", jsonExample) + server.ListenAndServe() +} +``` + +```bash +curl -i localhost:8080/header + +# HTTP/1.1 302 Found +# Location: http://google.com +# Date: Sun, 15 Oct 2023 12:50:47 GMT +# Content-Length: 0 + +curl -i localhost:8080/json + +# HTTP/1.1 200 OK +# Content-Type: application/json +# Date: Sun, 15 Oct 2023 12:56:51 GMT +# Content-Length: 58 + +# {"User":"Sau Sheong","Threads":["first","second","third"]} +``` + +#### 内置的 Response + +- NotFound 函数,包装一个 404 状态码和一个额外的信息 +- ServeFile 函数,从文件系统提供文件,返回给请求者 +- ServeContent 函数,它可以把实现了 io.ReadSeeker 接口的任何东西里面的内容返回给请求者 + - 还可以处理 Range 请求(范围请求),如果只请求了资源的一部分内容,那么 ServeContent 就可以如此响应。而 ServeFile 或 io.Copy 就不行 +- Redirect 函数,告诉客户端重定向到另一个 URL + +#### 模板