Skip to content

Commit

Permalink
go-web4
Browse files Browse the repository at this point in the history
  • Loading branch information
carla-cn committed Oct 15, 2023
1 parent e66bf4c commit 1eab942
Show file tree
Hide file tree
Showing 2 changed files with 354 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const Navbar = [
];

export default defineUserConfig({
port: 8081,
lang: "zh-CN",
title: "瓢儿白施肥记",
description: "记录一些学习内容",
Expand Down
355 changes: 353 additions & 2 deletions docs/backend/go/web/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 操作即可
Expand All @@ -359,7 +359,7 @@ server.ListenAndServe()

Request Body

- 请求和相应的 bodies 都是使用 Body 字段来表示的
- 请求和响应的 bodies 都是使用 Body 字段来表示的
- Body 是一个 io.ReadCloser 接口

```go
Expand Down Expand Up @@ -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
<form
action="http://localhost:8080/process"
method="post"
enctype="application/x-www-form-urlencoded"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
```

```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}
<form
action="http://localhost:8080/process?name=客户2号"
method="post"
enctype="application/x-www-form-urlencoded"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
```

```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}
<form
action="http://localhost:8080/process?name=客户2号"
method="post"
enctype="multipart/form-data"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<button type="submit">Submit</button>
</form>
```

```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
<form
action="http://localhost:8080/process?name=客户2号"
method="post"
enctype="multipart/form-data"
>
<input type="text" name="name" placeholder="Name" />
<input type="text" name="email" placeholder="Email" />
<input type="file" name="file" />
<button type="submit">Submit</button>
</form>
```

```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 := `<html>
<head><title>Go Web</title></head>
<body><h1>Hello World</h1></body>
</html>
`
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

# <html>
# <head><title>Go Web</title></head>
# <body><h1>Hello World</h1></body>
# </html>
```

#### 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

#### 模板

0 comments on commit 1eab942

Please sign in to comment.