跳转到主要内容

装饰器在 Python 和 TypeScript 等其他编程语言中肯定更为突出,但这并不是说你不能在 Go 中使用它们。事实上,对于某些问题,使用装饰器是完美的解决方案,我们希望在本教程中能找到答案。

了解装饰器模式


装饰器本质上允许您包装现有功能并在顶部附加或添加您自己的自定义功能。

在 Go 中,函数被视为第一类对象,这本质上意味着您可以像传递变量一样传递它们。让我们用一个非常简单的例子来看看这个:

package main

import (
  "fmt"
  "time"
)

func myFunc() {
  fmt.Println("Hello World")
  time.Sleep(1 * time.Second)
}

func main() {
  fmt.Printf("Type: %T\n", myFunc)
}

所以,在这个例子中,我们定义了一个名为 myFunc 的函数,它简单地打印出 Hello World。但是,在 main() 函数的主体中,我们调用了 fmt.Printf 并使用 %T 打印出作为第二个参数传入的值的类型。在这种情况下,我们传递了 myFunc,它随后将打印出以下内容:

$ go run test.go
Type: func()


那么,这对我们 Go 开发人员意味着什么?好吧,它强调了函数可以在我们代码库的其他部分中传递并用作参数的事实。

让我们通过进一步扩展我们的代码库并添加一个coolFunc()函数来看看这一点,该函数接受一个函数作为其唯一参数:

package main

import (
  "fmt"
  "time"
)

func myFunc() {
  fmt.Println("Hello World")
  time.Sleep(1 * time.Second)
}

// coolFunc takes in a function
// as a parameter
func coolFunc(a func()) {
    // it then immediately calls that functino
  a()
}

func main() {
  fmt.Printf("Type: %T\n", myFunc)
  // here we call our coolFunc function
  // passing in myFunc
    coolFunc(myFunc)
}

当我们尝试运行它时,我们应该看到我们的新输出包含我们期望的 Hello World 字符串:

$ go run test.go
Type: func()
Hello World


现在,这可能会让您感到有些奇怪。 你为什么想做这样的事情? 它本质上为您对 myFunc 的调用添加了一层抽象,并使代码复杂化,但并没有真正增加太多价值。

一个简单的装饰器


让我们看看如何使用这种模式为我们的代码库添加一些价值。 如果需要,我们可以在特定函数的执行周围添加一些额外的日志记录,以突出显示它的开始和结束时间。

package main

import (
    "fmt"
    "time"
)

func myFunc() {
  fmt.Println("Hello World")
    time.Sleep(1 * time.Second)
}

func coolFunc(a func()) {
    fmt.Printf("Starting function execution: %s\n", time.Now())
    a()
    fmt.Printf("End of function execution: %s\n", time.Now())
}

func main() {
    fmt.Printf("Type: %T\n", myFunc)
    coolFunc(myFunc)
}

调用此命令后,您应该会看到如下所示的日志:

$ go run test.go
Type: func()
Starting function execution: 2018-10-21 11:11:25.011873 +0100 BST m=+0.000443306
Hello World
End of function execution: 2018-10-21 11:11:26.015176 +0100 BST m=+1.003743698


如您所见,我们已经能够有效地包装我的原始函数,而无需更改它的实现。 我们现在能够清楚地看到该函数何时启动以及何时完成执行,并且它向我们强调了该函数只需大约一秒钟即可完成执行。

现实世界的例子


让我们再看几个例子,看看我们如何使用装饰器来获得更多的名声和财富。 我们将使用一个非常简单的 http Web 服务器并装饰我们的端点,以便我们可以验证传入请求是否具有特定的标头集。

如果您想了解更多关于在 Go 中编写简单 REST API 的信息,那么我建议您在此处查看我的另一篇文章:在 Go 中创建 REST API

package main

import (
    "fmt"
    "log"
    "net/http"
)

func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Endpoint Hit: homePage")
    fmt.Fprintf(w, "Welcome to the HomePage!")
}

func handleRequests() {
    http.HandleFunc("/", homePage)
    log.Fatal(http.ListenAndServe(":8081", nil))
}

func main() {
    handleRequests()
}

你可以看到,在我们的代码中没有什么特别复杂的。 我们设置了一个服务于单个 / 端点的 net/http 路由器。

让我们添加一个非常简单的身份验证装饰器函数,该函数将检查传入请求的 Authorized 标头是否设置为 true。

package main

import (
    "fmt"
    "log"
    "net/http"
)

func isAuthorized(endpoint func(http.ResponseWriter, *http.Request)) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

        fmt.Println("Checking to see if Authorized header set...")

        if val, ok := r.Header["Authorized"]; ok {
            fmt.Println(val)
            if val[0] == "true" {
                fmt.Println("Header is set! We can serve content!")
                endpoint(w, r)
            }
        } else {
            fmt.Println("Not Authorized!!")
            fmt.Fprintf(w, "Not Authorized!!")
        }
    })
}

func homePage(w http.ResponseWriter, r *http.Request) {
    fmt.Println("Endpoint Hit: homePage")
    fmt.Fprintf(w, "Welcome to the HomePage!")
}

func handleRequests() {

    http.Handle("/", isAuthorized(homePage))
    log.Fatal(http.ListenAndServe(":8081", nil))
}

func main() {
    handleRequests()
}

注意:这绝对不是保护 REST API 的正确方法,我建议您考虑使用 JWT 或 OAuth2 来实现该目标!

所以,让我们分解一下并尝试了解发生了什么!

我们创建了一个名为 isAuthorized() 的新装饰器函数,它接收一个与原始 homePage 函数具有相同签名的函数。然后返回一个 http.Handler。

在我们的 isAuthorized() 函数的主体中,我们返回一个新的 http.HandlerFunc,在该函数中验证我们的 Authorized 标头已设置并等于 true。现在,这是一个大大简化的 OAuth2 身份验证/授权版本,有一些细微的差异,但它让您大致了解它是如何工作的。

然而,要注意的关键是,我们已经设法装饰现有端点并在所述端点周围添加某种形式的身份验证,而无需更改该功能的现有实现。

现在,如果我们要添加一个我们想要保护的新端点,我们可以轻松地这样做:

 

// define our newEndpoint function. Notice how, yet again,
// we don't do any authentication based stuff in the body
// of this function
func newEndpoint(w http.ResponseWriter, r *http.Request) {
    fmt.Println("My New Endpoint")
    fmt.Fprintf(w, "My second endpoint")
}

func handleRequests() {

    http.Handle("/", isAuthorized(homePage))
  // register our /new endpoint and decorate our
  // function with our isAuthorized Decorator
  http.Handle("/new", isAuthorized(newEndpoint))
    log.Fatal(http.ListenAndServe(":8081", nil))
}

 

这突出了装饰器模式的主要优点,在我们的代码库中包装代码非常简单。 我们可以使用相同的方法轻松添加新的经过身份验证的端点

结论


希望本教程有助于揭开装饰器的神秘面纱,以及如何在自己的基于 Go 的程序中使用装饰器模式。 我们了解了装饰器模式的好处以及如何使用它来用新功能包装现有功能。

在本教程的第二部分,我们查看了一个更现实的示例,说明如何在自己的生产级 Go 系统中使用它。

如果您喜欢本教程,请随时广泛分享这篇文章,它确实对网站有帮助,我将不胜感激! 如果您有任何问题和/或意见,请在下面的评论部分告诉我!

文章链接