0%

这里所说的 responseWriterContext 里面的 writermem responseWriter

这个 responseWriter 实际上只是对 http.ResponseWriter 的一个扩展 struct

也就是说,对于 responseWriter,我们可以通过它来调用 http.ResponseWriter 的几个方法,也可以调用 responseWriter 自身实现的一些其他的方法,具体有下面这些方法:

  • WriteHeaderNow: 写响应头(如果还没开始写响应)
  • WriteString: 返回字符串响应。
  • Status: 获取状态码
  • Size: 获取响应大小
  • Written: 是否已经开始往 http.ResponseWriter 写入数据

它也重写了 http.ResponseWriter 的一些方法:

  • WriteHeader: 如果已经开始写响应数据了,就打印警告,否则直接修改 响应状态码
  • Write: 还没有写响应头的时候,先写响应头。然后写入数据,并且记录响应的大小。
  • Hijack: 一个 Hijack 的包裹函数,没有其他作用
  • Flush: 将数据发送给客户端。如果没有写入 header,就先写入 header

总之,这里只是封装了一些最基本的函数,就是满足我们写 header 跟 body 的需求。如果我们需要写入诸如 JSON 之类的数据,其实是需要继续在此基础上进行封装的。

思维导图

前言

有一定 go 使用经验的人应该知道,context 在 go 的 web 开发中往往代表着一个请求的上下文。

作为开发者,我们可以通过这个 context 实例,实现对请求 goroutine 链条的管理,比如请求的超时控制等等。

不过,net/http 里面的 ServeHTTP 方法只是给了开发者两个参数,一个是 http.ResponseWriter,是用来返回响应给客户端用的,另一个是 *http.Request,是用来获取请求信息的。但实际上 *http.Request 包含了请求相关的 context,这个 context 就是整个请求最初的那个 context

对于 Web 开发来说,有了 http.ResponseWriter*http.Request 这两个参数也足够了。但是想要更高效地进行应用开发,我们往往需要在此基础上再封装一下。

在 gin 里面,也对 baseContext 进行了一定的扩展吧,应该这么说,实现了很多 Web 基础功能,比如参数获取、中间件、参数验证等等。

gin.context 思维导图

看不清可以右键在新页面打开.

Go 中常用的切片 slice 数据结构是动态数组,切片长度并不固定,在容量不足的时候会自动扩容。

切片实质上是对一个底层数组的抽象视图,由 Go 运行时维护。在运行时,切片由如下的 SliceHeader 结构体表示,其中 Data 字段是指向底层数组的指针,Len 表示当前切片的长度,而 Cap 表示当前切片的容量,也就是 Data 数组的大小。

1
2
3
4
5
type SliceHeader struct {
Data uintptr
Len int
Cap int
}

切片作为参数入参

我们知道 slice 是一个指针类型,所以我们可能会习惯性地认为:传递切片,等同于传递指针,函数内部对切片的修改,将会影响到函数外部的切片。这一习惯性认知在大部分情况下都是正确的,如以下代码所示:在 test 函数中修改切片,外部的 TestSlice 受到了影响。

1
2
3
4
5
6
7
8
9
10
11
12
func test(s []string) {
for i := 0; i < len(s); i++ {
s[i] = "b"
}
}

func TestSlice(t *testing.T) {
s := []string{"a", "b"}
fmt.Println(s)
test(s)
fmt.Println(s)
}

输出:

1
2
[a b]
[b b]

我们对上面的代码做一些修改,在调用函数的时候触发切片的扩容机制,然后再看看输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func test1(s []string) {
s = append(s, "b1")

for i := 0; i < len(s); i++ {
s[i] = "b"
}
}

func TestSlice1(t *testing.T) {
s := []string{"a", "b"}
fmt.Println(s)
test1(s)
fmt.Println(s)
}

输出:

1
2
[a b]
[a b]

我们可以发现,test1 里面对切片的修改并没有完全影响到外部的切片。

原因

在 Go 中,函数参数传递机制为 值拷贝

将切片做为函数参数传递,实际上是拷贝了 SliceHeader 结构体传入参数,结构体包含了指向底层数组的指针,因此在函数内部修改切片,操作的底层数组是一样的。

但是如果函数内的切片触发了切片扩容(如:使用 append 追加元素),Go 运行时会为切片分配一块新的内存空间并将原切片的所有元素拷贝过去,函数内部切片的底层数组指针指向了 新分配 的内存空间,而函数外部切片底层数组指针仍指向 分配前 的地址空间,由此出现了内外切片不一致的情形。

建议

  • 操作不涉及切片容量变化,直接传递切片
  • 操作涉及切片容量变化,且需要反馈给调用放,传递切片指针。

在 gin 里面,我们可以定义一个带切片类型字段的结构体来对切片做表单验证,需要注意的是: * 对应字段为切片类型 * form tag 里面的键为 ids[] 的形式(如果是 query 的数组或者 form 里面的数组,如果是 json body 里面就不用 ids[]= 这样传递数组参数) * binding 里面可以使用 dive 来对数组的每一项做验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
"log"
"net/http"

"github.com/gin-gonic/gin"
)

type TestStruct struct {
// 关键代码
Ids []int `form:"ids[]" binding:"dive,numeric,gt=0"`
}

func test(c *gin.Context) {
var testStruct TestStruct
if c.ShouldBindQuery(&testStruct) == nil {
log.Println(testStruct.Ids)
c.String(http.StatusOK, "Success")
return
}

c.String(http.StatusBadRequest, "Failed")
return
}

func main() {
route := gin.Default()
route.Any("/test", test)
route.Run(":8085")
}