前一章,我们已经为我们的互联网大厦搭建好了“前台总机”和“用户部”,并且制定了高效的“电话转接”规则(路由)。现在,当一个用户填好了“注册申请表”(JSON数据)并提交过来,我们的“注册专员”(SignUp方法)该如何处理这份表格呢?


我们的“用户部-注册专员”(SignUp方法)收到了一个“信封”(HTTP 请求)。他的工作流程,就像把大象放进冰箱一样,也分三步:

  1. 拆开信封,拿出表格 (接收并解析请求)
  2. 仔细审查表格内容 (校验数据,调用业务逻辑)
  3. 盖上“通过”或“驳回”的章 (返回响应)

来,我们一步步看这位专员是如何工作的。

1 设计一张“临时登记表” (定义结构体)

专员总不能把收到的信息随手写在纸巾上吧?他需要一张标准化的“临时登记表”来存放从用户“信封”里拿出的信息。在 Go 语言里,这张表就是 struct

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// web/user.go

func (u *UserHandler) SignUp(ctx *gin.Context) {
	// 这就是专为本次注册任务设计的“临时登记表”
	type SignUpReq struct {
		Email           string `json:"email"`
		Password        string `json:"password"`
		ConfirmPassword string `json:"confirmPassword"` // 咱们顺便加个确认密码
	}

	var req SignUpReq // 拿出一张空白的登记表,准备填写

    // ... 后续操作
    
	ctx.String(http.StatusOK, "注册成功!假装的 :)")
}

注意看,我把 SignUpReq 这张“登记表”的设计图,直接画在了 SignUp 这个方法(办公室)里面。这叫内部结构体

为啥要藏在办公室里?

想象一下,这张“临时登记表”只有处理注册的这位专员需要用。如果我把它放在公司的“公共文件柜”(定义在方法外面),那财务部的老王、销售部的小李都能看到,万一他们不小心拿去用了,或者在上面乱涂乱画,这不就乱套了?

所以,遵循 “东西没必要让别人知道,就别拿出来” 的原则,我把它定义在方法内部。这让我们的代码更安全、更清爽。当然,如果这张表是全公司通用的标准表格,那肯定要拿出来放公共区域的。

再看 json:"email" 这部分,这像是在登记表上做的“备注”,告诉我们的智能助理 Gin:“嘿,如果信封里的表格上写着 email 的栏位,就把它的内容抄到我这张表的 Email 栏里来!”

2用“魔法扫描仪”自动填表 (ctx.Bind)

好了,空白表格有了,信封也来了。难道要我们的专员一个字一个字地手动抄写吗?不,我们有高科技——ctx.Bind(),一台能自动识别 JSON 格式的魔法扫描仪!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// web/user.go -> SignUp 方法内

func (u *UserHandler) SignUp(ctx *gin.Context) {
	type SignUpReq struct {
		Email           string `json:"email"`
		Password        string `json:"password"`
		ConfirmPassword string `json:"confirmPassword"`
	}

	var req SignUpReq
	
	// 把信封 (ctx) 喂给魔法扫描仪 (Bind)
	// 扫描仪会自动把内容填写到我们的登记表 (req) 上
	if err := ctx.Bind(&req); err != nil {
		// 如果信封是空的、或者格式不对(比如寄了张风景明信片过来)
		// 扫描仪会直接卡纸,并自动给用户退回一封“格式错误”的信
		return 
	}
	
	fmt.Printf("收到的表格内容:%v\n", req)
	
	ctx.String(http.StatusOK, "表格收到,内容正确!")
}

ctx.Bind(&req) 这行代码是关键!它会聪明地检查信封上的“内容类型”(Content-Type),发现是 application/json 后,就启动 JSON 解析程序,把数据完美地填入 req

敲黑板: 注意 &req 这个 & 符号!它的意思是,我们告诉扫描仪:“请把内容填在 我手上这张 登记表上”,而不是“给你看看我这张表的模板”。如果你忘了 &,扫描仪就不知道该往哪儿填,那可就白忙活了。

3 化身“福尔摩斯”,开始审查!(数据校验)

表格内容是填好了,但我们能相信用户填的所有信息吗?万一他邮箱写成了“我家住在黄土高坡”,密码写了个“123”,这能让他注册成功吗?

绝对不能!

灵魂拷问:不能让前端(用户浏览器)自己检查吗?

答案是:前端校验是君子协定,后端校验是法律底线!

前端校验就像是你家门口的“请随手关门”提示牌,防君子不防小人。懂点技术的人完全可以绕过你的网页,直接用工具给我们的“前台总机”寄一封奇奇怪怪的“信”(恶意请求)。

所以,无论前端做了多么华丽的校验,我们后端审查员,必须铁面无私,把每一份收到的表格都当成“嫌疑犯”来审查!

请出我们的“规则大师”:正则表达式!

对于“邮箱格式是否正确”、“密码强度是否足够”这种复杂的规则,我们得请一位专家——正则表达式(Regex)

这家伙长得有点潦草,像一串乱码,但它是个识别文本模式的天才。

程序员的悄悄话: 你需要精通正则表达式吗?我的建议是:不用! 这玩意儿像一门外语,不常用很快就忘。你需要做的,是知道有这么个工具,然后在需要用的时候,去网上搜索“邮箱正则表达式”、“密码强度正则表达式”,复制粘贴,搞定!让巨人的肩膀成为你的办公椅。

我们来给审查员配上“邮箱格式识别镜”和“密码强度探测器”。

 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
// 在 SignUp 方法中,Bind 成功后...

// 规则手册
const (
    emailRegexPattern    = `^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$`
    // 密码规则:至少8位,包含字母、数字、特殊字符
    passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
)

// 开始审查邮箱
ok, _ := regexp.Match(emailRegexPattern, []byte(req.Email))
if !ok {
    ctx.String(http.StatusBadRequest, "你这邮箱地址是火星来的吧?格式不对!")
    return
}

// 再审查密码
ok, _ = regexp.Match(passwordRegexPattern, []byte(req.Password))
if !ok {
    ctx.String(http.StatusBadRequest, "密码太弱了,出门别说我认识你!")
    return
}

// 别忘了检查两次密码是否一致
if req.Password != req.ConfirmPassword {
    ctx.String(http.StatusBadRequest, "两次输入的密码不一样,你是不是手抖了?")
    return
}

关于错误处理的碎碎念: regexp.Match 可能会返回一个 err。但这个 err 只有在你的正则表达式本身写错时才会出现。这是我们程序员自己的锅,跟用户没关系。所以真出错了,我们应该返回一个“系统错误”(500),并悄悄记下日志,而不是把“我的代码写错了”这种糗事告诉用户。

4 给“规则大师”提提速!(预编译)

上面的代码有个隐藏的问题。每次有注册请求进来,SignUp 专员都要把厚厚的“规则手册”(正则表达式字符串)拿给“规则大师”看,大师每次都要从头到尾读一遍,再进行比对。用户一多,大师就忙不过来了,直喊累!

这太低效了!聪明的老板会怎么做?—— 让大师把规则背下来!

这个“背规则”的动作,在程序里叫做预编译。我们不希望每次审查都编译一次,而是在“用户部”成立之初,就让大师们把规则背得滚瓜烂熟。

我们来改造一下我们的“用户部” UserHandler

 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// web/user.go
package web

import (
	// ...
	"github.com/dlclark/regexp2" // 换一个更强大的“规则大师”
	"github.com/gin-gonic/gin"
)

// 给用户部配备两名“已背下规则”的常驻专家
type UserHandler struct {
	emailExp    *regexp2.Regexp
	passwordExp *regexp2.Regexp
}

// NewUserHandler 变成了“用户部成立大会暨岗前培训”
func NewUserHandler() *UserHandler {
	const (
		emailRegexPattern    = `^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$`
		passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
	)
    // 在这里,让两位大师把规则背下来(预编译),一次搞定!
	emailExp := regexp2.MustCompile(emailRegexPattern, regexp2.None)
	passwordExp := regexp2.MustCompile(passwordRegexPattern, regexp2.None)

    // 两位专家正式上岗!
	return &UserHandler{
		emailExp:    emailExp,
		passwordExp: passwordExp,
	}
}

// SignUp 专员现在可以愉快地工作了
func (u *UserHandler) SignUp(ctx *gin.Context) {
	// ... (接收数据的部分不变)
    type SignUpReq struct { /* ... */ }
    var req SignUpReq
    if err := ctx.Bind(&req); err != nil { return }

    // 直接喊常驻专家来干活,不用再给他们看规则手册了
	ok, err := u.emailExp.MatchString(req.Email)
	if err != nil {
		ctx.String(http.StatusInternalServerError, "系统错误") // 专家出错了,是我们的问题
		return
	}
	if !ok {
		ctx.String(http.StatusBadRequest, "邮箱格式不正确!")
		return
	}

    // ... (用 u.passwordExp 检查密码,以及确认密码是否一致)
    // ...

	ctx.String(http.StatusOK, "恭喜!你的注册表格已通过初步审查!")
}

最后,别忘了在 main.go 里,我们要用新的“成立大会”方式来组建用户部:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// main.go
func main() {
	server := gin.Default()

    // u := &web.UserHandler{} // 旧的、没有专家的用户部
	u := web.NewUserHandler()  // 通过“岗前培训”,聘请一支有常驻专家的精锐团队!
	u.RegisterRoutes(server)

	server.Run(":8080")
}

看到了吗?通过预编译,我们将一次性的“学习成本”放在了程序启动时,之后每次处理请求,都只是高效的“条件反射”。这对于一个需要处理成千上万请求的网站来说,是至关重要的性能优化!

现在,我们的“注册专员”不仅会收表格,还会严格、高效地审查表格了!下一步,就是把审查通过的表格,正式录入公司的“人事档案系统”(数据库)了。我们下回分解!