美文网首页go
【Go Web开发】后台发送邮件

【Go Web开发】后台发送邮件

作者: Go语言由浅入深 | 来源:发表于2022-03-14 21:32 被阅读0次

正如我们在上一节中提到的,从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的邮件已正确发送。像这样:

处理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收件箱中再次看到相应的邮件。

相关文章

网友评论

    本文标题:【Go Web开发】后台发送邮件

    本文链接:https://www.haomeiwen.com/subject/qxisdrtx.html