正如我们在上一节中提到的,从registerUserHandler方法直接发送用户注册的欢迎邮件,造成客户端的请求/响应增加了相当大的延迟。
可以减少这种延迟的一种方法是通过创建gorroutine单独发送邮件。这将有效地将发送电子邮件的任务与registerUseHandler中的其余代码“解耦”,这意味着我们可以立即向客户端返回HTTP响应,而无需等待电子邮件发送完成。
在最简单的情况下,我们可以调整处理程序,通过创建goroutine执行电子邮件的发送:
File:cmd/api/users.go
package main
...
func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
...//前面代码不变
//创建goroutine异步发送邮件
go func() {
err = app.mailer.Send(user.Email, "/user_welcome.tmpl", user)
if err != nil {
//如果发送邮件报错,将错误信息打印到日志中,而不是返回给客户端
app.logger.Error(err, nil)
}
}()
//将返回码改为202,表示客户端请求被接受,但处理没有完成。
err = app.writeJSON(w, http.StatusAccepted, envelope{"user": user}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
当用户注册程序执行时,创建一个后台goroutine来发送欢迎邮件。发送邮件goroutine会和registerUserHandler处理程序并行执行,意味着我们不再等待邮件发送成功就返回JSON响应给客户端。很可能发送邮件的goroutine在registerUserHandler返回后一段时间还在执行。
需要指出的是:
- 上面使用app.logger.Error()帮助函数来管理错误。因为在goroutine中发生错误时,处理程序很可能已经向客户端返回202 Accepted响应了。我们不应该在后台goroutine中使用app.serverErrorResponse()帮助函数,这样做可能导致写入多次HTTP响应,http.Server会返回"http: superfluous response.WriteHeader call"错误。
- 需要注意的是在goroutine内部不能改变app和user变量值,否则会影响其他代码。
在我们的例子中,没有以任何方式改变这些变量的值,所以这种行为不会给我们带来任何问题。但重要的是要记住这点。
好了,我们再试试用户注册接口。
重启API服务,然后使用carol@example.com来注册一个新的用户。
$ BODY='{"name": "Carol Smith", "email": "carol@example.com", "password": "pa55word"}'
$ curl -w '\nTime: %{time_total}\n' -d "$BODY" localhost:4000/v1/users
{
"user": {
"id": 8,
"create_at": "2021-12-26T16:45:55+08:00",
"name": "Carol Smith",
"email": "carol@example.com",
"activated": false
}
}
Time: 0.313171
这一次,您应该会看到返回响应所消耗的时间要快得多——在我的例子中是0.31秒,而在之前的例子中是5.3秒。如果您查看Mailtrap收件箱,应该会看到carol@example.com的邮件已正确发送。像这样:
![](https://img.haomeiwen.com/i21436181/ddd6695f9ccdf96a.png)
处理panics
重要的是要记住,在这个后台goroutine中发生的任何panic都不会被我们的recoverPanic()中间件或Go的http.Server自动恢复,这将导致我们的整个应用程序意外终止。
在简单的后台goroutine中(类似我们这种情况),不用太多担心。但发送邮件所涉及的代码相当复杂(包括调用第三方包)因此不能忽视它们可能会发生panic。因此,我们要确保任何后台goroutine发生panic都要人为处理,可以借鉴recoverPainc()中间件中的处理方式。
重新打开cmd/api/users.go文件并更新registerUserHandler为:
File: cmd/api/users.go
package main
...
func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
...//前面代码不变
go func() {
//运行defer函数,使用recover()来捕获panic,并将错误信息打印到日志。
defer func() {
if err := recover(); err != nil{
app.logger.Error(fmt.Errorf("%s", err), nil)
}
}()
err = app.mailer.Send(user.Email, "/user_welcome.tmpl", user)
if err != nil {
//如果发送邮件报错,将错误信息打印到日志中,而不是返回给客户端
app.logger.Error(err, nil)
}
}()
//将返回码改为202,表示客户端请求被接受,但处理没有完成。
err = app.writeJSON(w, http.StatusAccepted, envelope{"user": user}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
使用帮助函数
如果您需要在应用程序中执行大量的后台任务,那么不断重复相同的panic recovery代码会变得单调乏味——而且可能会有忘记添加捕获panic代码。
为了解决这个问题,可以创建一个简单的帮助函数来包装panic恢复逻辑。打开cmd/api/helpers.go文件,创建一个新的background()方法,如下所示:
File:cmd/api/helpers.go
package main
...
//background()方法接收任意函数作为参数
func (app *application)background(fn func()) {
//创建后台goroutine
go func() {
defer func() {
if err := recover(); err != nil {
app.logger.Error(fmt.Errorf("%s", err), nil)
}
}()
//执行传入的任意函数
fn()
}()
}
这个background()帮助函数利用了Go的函数可以当变量来使用特性,这意味着函数可以被赋值给变量,并作为参数传递给其他函数。
我们创建了background()帮助函数,后面任何以func()为签名的函数都可以作为参数传给它。然后创建一个后台goroutine,使用defer函数捕获任何panic并记录错误日志,然后通过调用fn()执行传入的函数。
现在我们可以更新registerUserHandler来使用background帮助函数:
File:cmd/api/users.go
package main
import (
"errors"
"net/http"
"greenlight.alexedwards.net/internal/data"
"greenlight.alexedwards.net/internal/validator"
)
func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
...
//使用background创建goroutine异步发送邮件
app.background(func() {
err = app.mailer.Send(user.Email, "/user_welcome.tmpl", user)
if err != nil {
app.logger.Error(err, nil)
}
})
//将返回码改为202,表示客户端请求被接受,但处理没有完成。
err = app.writeJSON(w, http.StatusAccepted, envelope{"user": user}, nil)
if err != nil {
app.serverErrorResponse(w, r, err)
}
}
我们再确认下上面的代码是否正常工作。重启服务,然后使用dave@example.com邮件地址注册一个新的用户。
$ BODY='{"name": "Dave Smith", "email": "dave@example.com", "password": "pa55word"}'
$ curl -w '\nTime: %{time_total}\n' -d "$BODY" localhost:4000/v1/users
{
"user": {
"id": 9,
"create_at": "2021-12-26T17:23:46+08:00",
"name": "Dave Smith",
"email": "dave@example.com",
"activated": false
}
}
Time: 0.324778
如果一切设置正确,现在您将在Mailtrap收件箱中再次看到相应的邮件。
![](https://img.haomeiwen.com/i21436181/31ee7b3787843bff.png)
网友评论