前一章,我们已经为我们的互联网大厦搭建好了“前台总机”和“用户部”,并且制定了高效的“电话转接”规则(路由)。现在,当一个用户填好了“注册申请表”(JSON数据)并提交过来,我们的“注册专员”(SignUp方法)该如何处理这份表格呢?
我们的“用户部-注册专员”(SignUp方法)收到了一个“信封”(HTTP 请求)。他的工作流程,就像把大象放进冰箱一样,也分三步:
- 拆开信封,拿出表格 (接收并解析请求)
- 仔细审查表格内容 (校验数据,调用业务逻辑)
- 盖上“通过”或“驳回”的章 (返回响应)
来,我们一步步看这位专员是如何工作的。
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")
}
|
看到了吗?通过预编译,我们将一次性的“学习成本”放在了程序启动时,之后每次处理请求,都只是高效的“条件反射”。这对于一个需要处理成千上万请求的网站来说,是至关重要的性能优化!
现在,我们的“注册专员”不仅会收表格,还会严格、高效地审查表格了!下一步,就是把审查通过的表格,正式录入公司的“人事档案系统”(数据库)了。我们下回分解!