Go-标准库秘籍(四)

57 阅读18分钟

Go 标准库秘籍(四)

原文:zh.annas-archive.org/md5/F3FFC94069815F41B53B3D7D6E774406

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:来到服务器端

本章包含以下配方:

  • 创建 TCP 服务器

  • 创建 UDP 服务器

  • 处理多个客户端

  • 创建 HTTP 服务器

  • 处理 HTTP 请求

  • 创建 HTTP 中间件层

  • 提供静态文件

  • 提供使用模板生成的内容

  • 处理重定向

  • 处理 cookies

  • 优雅地关闭 HTTP 服务器

  • 提供安全的 HTTP 内容

  • 解析表单变量

介绍

本章涵盖了从实现简单的 TCP 和 UDP 服务器到启动 HTTP 服务器的主题。这些配方将引导您从处理 HTTP 请求、提供静态内容,到提供安全的 HTTP 内容。

检查 Go 是否已正确安装。第一章准备就绪部分中的检索 Golang 版本配方将有所帮助。

确保端口80807070没有被其他应用程序使用。

创建 TCP 服务器

连接网络章节中,介绍了 TCP 连接的客户端部分。在本配方中,将描述服务器端。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe01

  2. 导航到该目录。

  3. 创建servertcp.go文件,内容如下:

        package main

        import (
          "bufio"
          "fmt"
          "io"
          "net"
        )

        func main() {

          l, err := net.Listen("tcp", ":8080")
          if err != nil {
            panic(err)
          }
          for {
            fmt.Println("Waiting for client...")
            conn, err := l.Accept()
            if err != nil {
              panic(err)
            }

            msg, err := bufio.NewReader(conn).ReadString('\n')
            if err != nil {
              panic(err)
            }
            _, err = io.WriteString(conn, "Received: "+string(msg))
            if err != nil {
              fmt.Println(err)
            }
            conn.Close()
          }
        }
  1. 通过go run servertcp.go执行代码:

  1. 打开另一个终端并执行nc localhost 8080

  2. 写入任何文本,例如Hello

  3. 查看输出:

工作原理...

可以使用net包创建 TCP 服务器。net 包包含Listen函数,用于创建TCPListener,可以Accept客户端连接。Accept方法调用TCPListener上的方法,直到接收到客户端连接。如果客户端连接成功,Accept方法会返回TCPConn连接。TCPConn是连接到客户端的连接,用于读取和写入数据。

TCPConn实现了ReaderWriter接口。可以使用所有写入和读取数据的方法。请注意,读取数据时有一个分隔符字符,否则,如果客户端强制关闭连接,则会收到 EOF。

请注意,此实现一次只能处理一个客户端。

创建 UDP 服务器

用户数据报协议(UDP)是互联网的基本协议之一。本篇将向您展示如何监听 UDP 数据包并读取内容。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe02

  2. 导航到该目录。

  3. 创建serverudp.go文件,内容如下:

        package main

        import (
          "fmt"
          "log"
          "net"
        )

        func main() {

          pc, err := net.ListenPacket("udp", ":7070")
          if err != nil {
            log.Fatal(err)
          }
          defer pc.Close()

          buffer := make([]byte, 2048)
          fmt.Println("Waiting for client...")
          for {
            _, addr, err := pc.ReadFrom(buffer)
            if err == nil {
              rcvMsq := string(buffer)
              fmt.Println("Received: " + rcvMsq)
              if _, err := pc.WriteTo([]byte("Received: "+rcvMsq), addr);
              err != nil {
                fmt.Println("error on write: " + err.Error())
              }
            } else {
              fmt.Println("error: " + err.Error())
            }
          }
        }
  1. 通过go run serverudp.go启动服务器:

  1. 打开另一个终端并执行nc -u localhost 7070

  2. 在终端中写入任何消息,例如Hello,然后按Enter

  3. 查看输出:

工作原理...

与 TCP 服务器一样,可以使用net包创建 UDP 服务器。使用ListenPacket函数创建PacketConn

PacketConn不像TCPConn那样实现ReaderWriter接口。要读取接收到的数据包,应该使用ReadFrom方法。ReadFrom方法会阻塞,直到接收到数据包。然后返回客户端的Addr(记住 UDP 不是基于连接的)。要响应客户端,可以使用PacketConnWriteTo方法;这会消耗消息和Addr,在这种情况下是客户端的Addr

处理多个客户端

前面的配方展示了如何创建 UDP 和 TCP 服务器。示例代码尚未准备好同时处理多个客户端。在本配方中,我们将介绍如何同时处理更多客户端。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe03

  2. 导航到该目录。

  3. 创建multipletcp.go文件,内容如下:

        package main

        import (
          "fmt"
          "log"
          "net"
        )

        func main() {

          pc, err := net.ListenPacket("udp", ":7070")
          if err != nil {
            log.Fatal(err)
          }
          defer pc.Close()

          buffer := make([]byte, 2048)
          fmt.Println("Waiting for client...")
          for {

            _, addr, err := pc.ReadFrom(buffer)
            if err == nil {
              rcvMsq := string(buffer)
              fmt.Println("Received: " + rcvMsq)
              if _, err := pc.WriteTo([]byte("Received: "+rcvMsq), addr);
              err != nil {
                fmt.Println("error on write: " + err.Error())
              }
            } else {
              fmt.Println("error: " + err.Error())
            }
          }

        }
  1. 通过go run multipletcp.go执行代码。

  2. 打开另外两个终端并执行nc localhost 8080

  3. 在两个打开的终端中写入一些内容并查看输出。以下两个图像是连接的客户端。

    • 终端 1 连接到localhost:8080

    • 终端 2 连接到localhost:8080

服务器运行的终端中的输出:

工作原理...

TCP 服务器的实现与本章的前一个配方创建 TCP 服务器相同。实现已增强,具有同时处理多个客户端的能力。请注意,我们现在在单独的goroutine中处理接受的连接。这意味着服务器可以继续使用Accept方法接受客户端连接。

因为 UDP 协议不是有状态的,也不保持任何连接,所以处理多个客户端的工作被移动到应用程序逻辑中,您需要识别客户端和数据包序列。只有向客户端写入响应才能使用 goroutines 并行化。

创建 HTTP 服务器

在 Go 中创建 HTTP 服务器非常容易,标准库提供了更多的方法来实现。让我们看看最基本的方法。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe04

  2. 导航到目录。

  3. 创建httpserver.go文件,内容如下:

        package main

        import (
          "fmt"
          "net/http"
        )

        type SimpleHTTP struct{}

        func (s SimpleHTTP) ServeHTTP(rw http.ResponseWriter,
                            r *http.Request) {
          fmt.Fprintln(rw, "Hello world")
        }

        func main() {
          fmt.Println("Starting HTTP server on port 8080")
          // Eventually you can use
          // http.ListenAndServe(":8080", SimpleHTTP{})
          s := &http.Server{Addr: ":8080", Handler: SimpleHTTP{}}
          s.ListenAndServe()
        }
  1. 通过go run httpserver.go执行代码。

  2. 查看输出:

  1. 在浏览器中访问 URL http://localhost:8080,或使用curl。应该显示Hello world内容:

工作原理...

net/http包包含了几种创建 HTTP 服务器的方法。最简单的方法是实现net/http包中的Handler接口。Handler接口要求类型实现ServeHTTP方法。这个方法处理请求和响应。

服务器本身以net/http包中的Server结构的形式创建。Server结构需要HandlerAddr字段。通过调用ListenAndServe方法,服务器开始在给定地址上提供内容。

如果使用ServerServe方法,则必须提供Listener

net/http包还提供了默认服务器,如果从net/http包中调用ListenAndServe作为函数,则可以使用。它消耗HandlerAddr,与Server结构相同。在内部,创建了Server

处理 HTTP 请求

应用程序通常使用 URL 路径和 HTTP 方法来定义应用程序的行为。本配方将说明如何利用标准库来处理不同的 URL 和方法。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe05

  2. 导航到目录。

  3. 创建handle.go文件,内容如下:

        package main

        import (
          "fmt"
          "net/http"
        )

        func main() {

          mux := http.NewServeMux()
          mux.HandleFunc("/user", func(w http.ResponseWriter, 
                         r *http.Request) {
            if r.Method == http.MethodGet {
              fmt.Fprintln(w, "User GET")
            }
            if r.Method == http.MethodPost {
              fmt.Fprintln(w, "User POST")
            }
          })

          // separate handler
          itemMux := http.NewServeMux()
          itemMux.HandleFunc("/items/clothes", func(w http.ResponseWriter,
                             r *http.Request) {
            fmt.Fprintln(w, "Clothes")
          })
          mux.Handle("/items/", itemMux)

          // Admin handlers
          adminMux := http.NewServeMux()
          adminMux.HandleFunc("/ports", func(w http.ResponseWriter,
                              r *http.Request) {
            fmt.Fprintln(w, "Ports")
          })

          mux.Handle("/admin/", http.StripPrefix("/admin",
                                adminMux))

          // Default server
          http.ListenAndServe(":8080", mux)

        }
  1. 通过go run handle.go执行代码。

  2. 在浏览器中或通过curl检查以下 URL:

  • http://localhost:8080/user

  • http://localhost:8080/items/clothes

  • http://localhost:8080/admin/ports

  1. 查看输出:

工作原理...

net/http包包含ServeMux结构,该结构实现了Handler接口,可用于Server结构,但还包含了如何定义不同路径处理的机制。ServeMux指针包含HandleFuncHandle方法,接受路径,HandlerFunc函数处理给定路径的请求,或者另一个处理程序执行相同的操作。

参见前面的示例,了解如何使用这些。Handler接口和HandlerFunc需要实现带有请求和响应参数的函数。这样你就可以访问这两个结构。请求本身可以访问Headers、HTTP 方法和其他请求参数。

创建 HTTP 中间件层

具有 Web UI 或 REST API 的现代应用程序通常使用中间件机制来记录活动或保护给定接口的安全性。在本示例中,将介绍实现这种中间件层。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe06

  2. 导航到目录。

  3. 创建具有以下内容的middleware.go文件:

        package main

        import (
          "io"
          "net/http"
        )

        func main() {

          // Secured API
          mux := http.NewServeMux()
          mux.HandleFunc("/api/users", Secure(func(w http.ResponseWriter,
                         r *http.Request) {
            io.WriteString(w,  `[{"id":"1","login":"ffghi"},
                           {"id":"2","login":"ffghj"}]`)
          }))

          http.ListenAndServe(":8080", mux)

        }

        func Secure(h http.HandlerFunc) http.HandlerFunc {
          return func(w http.ResponseWriter, r *http.Request) {
            sec := r.Header.Get("X-Auth")
            if sec != "authenticated" {
              w.WriteHeader(http.StatusUnauthorized)
              return
            }
            h(w, r) // use the handler
          }

        }
  1. 通过go run middleware.go执行代码。

  2. 使用curl检查 URLhttp://localhost:8080/api/users,通过执行这两个命令(第一个不带X-Auth头,第二个带X-Auth头):

  • curl -X GET -I http://localhost:8080/api/users

  • curl -X GET -H "X-Auth: authenticated" -I http://localhost:8080/api/users

  1. 查看输出:

  1. 使用X-User头测试 URLhttp://localhost:8080/api/profile

  2. 查看输出:

工作原理...

在前面的示例中,中间件的实现利用了 Golang 的函数作为一等公民功能。原始的HandlerFunc被包装成检查X-Auth头的HandlerFunc。然后使用Secure函数来保护HandlerFunc,并在ServeMuxHandleFunc方法中使用。

请注意,这只是一个简单的示例,但是您可以实现更复杂的解决方案。例如,用户身份可以从Header令牌中提取,随后可以定义新类型的处理程序,如type AuthHandler func(u *User,w http.ResponseWriter, r *http.Request)。然后,WithUser函数为ServeMux创建HandlerFunc

提供静态文件

几乎任何 Web 应用程序都需要提供静态文件。使用标准库可以轻松实现 JavaScript 文件、静态 HTML 页面或 CSS 样式表的提供。本示例将展示如何实现。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe07

  2. 导航到目录。

  3. 创建具有以下内容的文件welcome.txt

        Hi, Go is awesome!
  1. 创建文件夹html,导航到该文件夹并创建具有以下内容的文件page.html
        <html>
          <body>
            Hi, I'm HTML body for index.html!
          </body>
        </html>
  1. 创建具有以下内容的static.go文件:
        package main

        import (
          "net/http"
        )

        func main() {

          fileSrv := http.FileServer(http.Dir("html"))
          fileSrv = http.StripPrefix("/html", fileSrv)

          http.HandleFunc("/welcome", serveWelcome)
          http.Handle("/html/", fileSrv)
          http.ListenAndServe(":8080", nil)
        }

        func serveWelcome(w http.ResponseWriter, r *http.Request) {
          http.ServeFile(w, r, "welcome.txt")
        }
  1. 通过go run static.go执行代码。

  2. 使用浏览器或curl实用程序检查以下 URL:

  • http://localhost:8080/html/page.html

  • http://localhost:8080/welcome

  1. 查看输出:

工作原理...

net/http包提供了ServeFileFileServer函数,用于提供静态文件。ServeFile函数只消耗给定文件路径参数的ResponseWriterRequest,并将文件内容写入响应。

FileServer函数创建整个消耗FileSystem参数的Handler。前面的示例使用了Dir类型,它实现了FileSystem接口。FileSystem接口需要实现Open方法,该方法消耗字符串并返回给定路径的实际File

使用模板生成的内容

对于某些目的,不需要使用所有 JavaScript 创建高度动态的 Web UI,生成内容的静态内容可能已经足够。Go 标准库提供了一种构建动态生成内容的方法。本示例将引导您进入 Go 标准库模板化。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe08

  2. 导航到目录。

  3. 创建具有以下内容的文件template.tpl

        <html>
          <body>
            Hi, I'm HTML body for index.html!
          </body>
        </html>
  1. 创建文件dynamic.go,内容如下:
        package main

        import "net/http"
        import "html/template"

        func main() {
          tpl, err := template.ParseFiles("template.tpl")
          if err != nil {
            panic(err)
          }

          http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
            err := tpl.Execute(w, "John Doe")
            if err != nil {
              panic(err)
            }
          })
          http.ListenAndServe(":8080", nil)
        }
  1. 通过go run dynamic.go执行代码。

  2. 检查 URL http://localhost:8080并查看输出:

工作原理...

Go 标准库还包含用于模板化内容的包。html/templatetext/template包提供了解析模板和使用它们创建输出的函数。解析是使用ParseXXX函数或新创建的Template结构指针的方法完成的。前面的示例使用了html/template包的ParseFiles函数。

模板本身是基于文本的文档或包含动态变量的文本片段。模板的使用基于将模板文本与包含模板中的变量值的结构进行合并。为了将模板与这些结构进行合并,有ExecuteExecuteTemplate方法。请注意,这些方法使用写入器接口,其中写入输出;在这种情况下使用ResponseWriter

模板语法和特性在文档中有很好的解释。

处理重定向

重定向是告诉客户端内容已经移动或需要在其他地方完成请求的常用方式。本教程描述了如何使用标准库实现重定向。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe09

  2. 导航到目录。

  3. 创建文件redirect.go,内容如下:

        package main

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

        func main() {
          log.Println("Server is starting...")

          http.Handle("/secured/handle",
               http.RedirectHandler("/login", 
                      http.StatusTemporaryRedirect))
          http.HandleFunc("/secured/hadlefunc", 
               func(w http.ResponseWriter, r *http.Request) {
            http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
          })
          http.HandleFunc("/login", func(w http.ResponseWriter,
                          r *http.Request) {
            fmt.Fprintf(w, "Welcome user! Please login!\n")
          })
          if err := http.ListenAndServe(":8080", nil); err != nil {
            panic(err)
          }
        }
  1. 通过go run redirect.go执行代码。

  2. 使用curl -v -L http://localhost:8080/s

ecured/handle以查看重定向是否有效:

工作原理...

net/http包中包含了执行重定向的简单方法。可以利用RedirectHandler。该函数接受请求将被重定向的URL和将发送给客户端的状态码。该函数本身将结果发送给Handler,可以在ServeMuxHandle方法中使用(示例直接使用包中的默认方法)。

第二种方法是使用Redirect函数,它可以为您执行重定向。该函数接受ResponseWriter、请求指针和与RequestHandler相同的 URL 和状态码,这些将发送给客户端。

重定向也可以通过手动设置Location头并编写适当的状态码来完成。Go 库使开发人员能够轻松使用这一功能。

处理 cookies

Cookies 提供了一种在客户端方便地存储数据的方式。本教程演示了如何使用标准库设置、检索和删除 cookies。

如何做...

  1. 打开控制台并创建文件夹chapter09/recipe10

  2. 导航到目录。

  3. 创建文件cookies.go,内容如下:

        package main

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

        const cookieName = "X-Cookie"

        func main() {
          log.Println("Server is starting...")

          http.HandleFunc("/set", func(w http.ResponseWriter,
                          r *http.Request) {
            c := &http.Cookie{
              Name: cookieName,
              Value: "Go is awesome.",
              Expires: time.Now().Add(time.Hour),
              Domain: "localhost",
            }
            http.SetCookie(w, c)
            fmt.Fprintln(w, "Cookie is set!")
          })
          http.HandleFunc("/get", func(w http.ResponseWriter,
                          r *http.Request) {
            val, err := r.Cookie(cookieName)
            if err != nil {
              fmt.Fprintln(w, "Cookie err: "+err.Error())
              return
            }
            fmt.Fprintf(w, "Cookie is: %s \n", val.Value)
            fmt.Fprintf(w, "Other cookies")
            for _, v := range r.Cookies() {
              fmt.Fprintf(w, "%s => %s \n", v.Name, v.Value)
            }
          })
          http.HandleFunc("/remove", func(w http.ResponseWriter,
                          r *http.Request) {
            val, err := r.Cookie(cookieName)
            if err != nil {
              fmt.Fprintln(w, "Cookie err: "+err.Error())
              return
            }
            val.MaxAge = -1
            http.SetCookie(w, val)
            fmt.Fprintln(w, "Cookie is removed!")
          })
          if err := http.ListenAndServe(":8080", nil); err != nil {
            panic(err)
          }
        }
  1. 通过go run cookies.go执行代码。

  2. 按照以下顺序访问 URL 并查看:

    • 在浏览器中访问 URL http://localhost:8080/set的响应:

    • 在浏览器中访问 URL http://localhost:8080/get的响应(响应包含可用的 cookies):

    • 在浏览器中访问 URL http://localhost:8080/remove的响应(这将删除 cookie):

    • 在浏览器中访问 URL http://localhost:8080/get的响应(证明 cookie X-Cookie已被移除):

工作原理...

net/http包还提供了操作 cookie 的函数和机制。示例代码介绍了如何设置/获取和删除 cookie。SetCookie函数接受代表 cookie 的Cookie结构指针,自然也接受ResponseWriterNameValueDomain和过期时间直接在Cookie结构中设置。在幕后,SetCookie函数写入头文件以设置 cookie。

可以从Request结构中检索 cookie 值。具有名称参数的Cookie方法返回指向Cookie的指针,如果请求中存在 cookie。

要列出请求中的所有 cookie,可以调用Cookies方法。此方法返回Cookie结构指针的切片。

为了让客户端知道应该删除 cookie,可以检索具有给定名称的Cookie,并将MaxAge字段设置为负值。请注意,这不是 Go 的特性,而是客户端应该工作的方式。

优雅关闭 HTTP 服务器

在第一章中,与环境交互,介绍了实现优雅关闭的机制。在这个示例中,我们将描述如何关闭 HTTP 服务器并给予它处理现有客户端的时间。

操作步骤...

  1. 打开控制台并创建文件夹chapter09/recipe11

  2. 导航到目录。

  3. 创建名为gracefully.go的文件,内容如下:

        package main

        import (
          "context"
          "fmt"
          "log"
          "net/http"
          "os"
          "os/signal"
          "time"
        )

        func main() {

          mux := http.NewServeMux()
          mux.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
            fmt.Fprintln(w, "Hello world!")
          })

          srv := &http.Server{Addr: ":8080", Handler: mux}
          go func() {
            if err := srv.ListenAndServe(); err != nil {
              log.Printf("Server error: %s\n", err)
            }
          }()

          log.Println("Server listening on : " + srv.Addr)

          stopChan := make(chan os.Signal)
          signal.Notify(stopChan, os.Interrupt)

          <-stopChan // wait for SIGINT
          log.Println("Shutting down server...")

          ctx, cancel := context.WithTimeout(
            context.Background(),
            5*time.Second)
          srv.Shutdown(ctx)
          <-ctx.Done()
          cancel()
          log.Println("Server gracefully stopped")
        }
  1. 通过go run gracefully.go执行代码。

  2. 等待服务器开始监听:

  1. 使用浏览器连接到http://localhost:8080;这将导致浏览器等待 10 秒钟的响应。

  2. 在 10 秒的间隔内,按下Ctrl + C发送SIGINT信号。

  3. 尝试从另一个标签页重新连接(服务器应该拒绝其他连接)。

  4. 在终端中查看输出:

工作原理...

net/http包中的Server提供了优雅关闭连接的方法。前面的代码在一个单独的goroutine中启动 HTTP 服务器,并在一个变量中保留对Server结构的引用。

通过调用Shutdown方法,Server开始拒绝新连接并关闭打开的监听器和空闲连接。然后它无限期地等待已经挂起的连接,直到这些连接变为空闲。在所有连接关闭后,服务器关闭。请注意,Shutdown方法会消耗Context。如果提供的Context在关闭之前过期,则会返回来自Context的错误,并且Shutdown不再阻塞。

提供安全的 HTTP 内容

这个示例描述了创建 HTTP 服务器的最简单方式,它通过 TLS/SSL 层提供内容。

准备工作

准备私钥和自签名的 X-509 证书。为此,可以使用 OpenSSL 实用程序。通过执行命令openssl genrsa -out server.key 2048,使用 RSA 算法生成私钥到文件server.key。基于此私钥,可以通过调用openssl req -new -x509 -sha256 -key server.key -out server.crt -days 365生成 X-509 证书。创建server.crt文件。

操作步骤...

  1. 打开控制台并创建文件夹chapter09/recipe12

  2. 导航到目录。

  3. 将创建的server.keyserver.crt文件放入其中。

  4. 创建名为servetls.go的文件,内容如下:

        package main

        import (
          "fmt"
          "net/http"
        )

        type SimpleHTTP struct{}

          func (s SimpleHTTP) ServeHTTP(rw http.ResponseWriter,
                              r *http.Request) {
            fmt.Fprintln(rw, "Hello world")
          }

          func main() {
            fmt.Println("Starting HTTP server on port 8080")
            // Eventually you can use
            // http.ListenAndServe(":8080", SimpleHTTP{})
            s := &http.Server{Addr: ":8080", Handler: SimpleHTTP{}}
            if err := s.ListenAndServeTLS("server.crt", "server.key");
            err != nil {
              panic(err)
            }
          }
  1. 通过go run servetls.go执行服务器。

  2. 访问 URL https://localhost:8080(使用 HTTPS 协议)。如果使用curl实用程序,则必须使用--insecure标志,因为我们的证书是自签名的,不受信任:

工作原理...

除了net/http包中的ListenAndServe函数之外,还存在用于通过 SSL/TLS 提供 HTTP 服务的 TLS 变体。通过ServerListenAndServeTLS方法,可以提供安全的 HTTP 服务。ListenAndServeTLS需要私钥和 X-509 证书的路径。当然,也可以直接使用net/http包中的ListenAndServeTLS函数。

解析表单变量

HTTP 的POST表单是向服务器传递信息的一种常见方式,以结构化的方式。这个示例展示了如何在服务器端解析和访问这些信息。

如何做...

  1. 打开控制台,创建文件夹chapter09/recipe12

  2. 导航到目录。

  3. 创建名为form.go的文件,内容如下:

        package main

        import (
          "fmt"
          "net/http"
        )

        type StringServer string

        func (s StringServer) ServeHTTP(rw http.ResponseWriter,
                              req *http.Request) {
          fmt.Printf("Prior ParseForm: %v\n", req.Form)
          req.ParseForm()
          fmt.Printf("Post ParseForm: %v\n", req.Form)
          fmt.Println("Param1 is : " + req.Form.Get("param1"))
          rw.Write([]byte(string(s)))
        }

        func createServer(addr string) http.Server {
          return http.Server{
            Addr: addr,
            Handler: StringServer("Hello world"),
          }
        }

        func main() {
          s := createServer(":8080")
          fmt.Println("Server is starting...")
          if err := s.ListenAndServe(); err != nil {
            panic(err)
          }
        }
  1. 通过go run form.go执行代码。

  2. 打开第二个终端,使用curl执行POST

 curl -X POST -H "Content-Type: app
lication/x-www-form-urlencoded" -d "param1=data1&param2=data2" "localhost:8080?
param1=overriden&param3=data3"
  1. 在运行服务器的第一个终端中查看输出:

工作原理...

net/http包的Request结构包含Form字段,其中包含了POST表单变量和 URL 查询变量的合并。在前面的代码中,重要的一步是在Request指针上调用ParseForm方法。这个方法调用会将POST表单值和查询值解析为一个Form变量。请注意,如果在Form字段上使用Get方法,则会优先考虑参数的POST值。FormPostForm字段实际上都是url.Values类型。

如果只需要访问POST表单中的参数,可以使用RequestPostForm字段。这个字段只保留了POST主体中的参数。

第十章:并发乐趣

本章包含以下教程:

  • 使用 Mutex 同步对资源的访问

  • 为并发访问创建 map

  • 只运行一次代码块

  • 在多个 goroutines 之间池化资源

  • 使用 WaitGroup 同步 goroutines

  • 从多个来源获取最快的结果

  • 使用 errgroup 传播错误

介绍

并发行为的编程总是很困难的。Go 具有非常好的机制来管理并发,如通道。除了通道作为同步机制外,Go 标准库还提供了处理更传统核心方式的并发部分的包。本章描述了如何利用 sync 包来实现常见的同步任务。最后一个教程将展示如何简化一组 goroutines 的错误传播。

检查 Go 是否已正确安装。第一章检索 Golang 版本教程中的准备就绪部分将对你有所帮助。

确保端口80807070没有被其他应用程序使用。

使用 Mutex 同步对资源的访问

如果代码使用并发访问被认为对并发使用不安全的任何资源,就需要实现同步机制来保护访问。除了使用通道,还可以利用互斥锁来实现这一目的。这个教程将向你展示如何做到这一点。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe01

  2. 导航到目录。

  3. 创建文件mutex.go,内容如下:

        package main

        import (
          "fmt"
          "sync"
        )

        var names = []string{"Alan", "Joe", "Jack", "Ben",
                             "Ellen", "Lisa", "Carl", "Steve",
                             "Anton", "Yo"}

        type SyncList struct {
          m sync.Mutex
          slice []interface{}
        }

        func NewSyncList(cap int) *SyncList {
          return &SyncList{
            sync.Mutex{},
            make([]interface{}, cap),
          }
        }

        func (l *SyncList) Load(i int) interface{} {
          l.m.Lock()
          defer l.m.Unlock()
          return l.slice[i]
        }

        func (l *SyncList) Append(val interface{}) {
          l.m.Lock()
          defer l.m.Unlock()
          l.slice = append(l.slice, val)
        }

        func (l *SyncList) Store(i int, val interface{}) {
          l.m.Lock()
          defer l.m.Unlock()
          l.slice[i] = val
        }

        func main() {

          l := NewSyncList(0)
          wg := &sync.WaitGroup{}
          wg.Add(10)
          for i := 0; i < 10; i++ {
            go func(idx int) {
              l.Append(names[idx])
              wg.Done()
            }(i)
          }
          wg.Wait()

          for i := 0; i < 10; i++ {
            fmt.Printf("Val: %v stored at idx: %d\n", l.Load(i), i)
          }

        }
  1. 通过go run mutex.go执行代码。

  2. 查看输出:

它是如何工作的...

同步原语Mutexsync包提供。Mutex作为一个锁,用于保护部分或资源。一旦goroutineMutex上调用Lock并且Mutex处于未锁定状态,Mutex就会被锁定,goroutine就可以独占地访问临界区。如果Mutex处于锁定状态,goroutine调用Lock方法。这个goroutine会被阻塞,需要等待Mutex再次解锁。

请注意,在示例中,我们使用Mutex来同步对切片原语的访问,这被认为是不安全的并发使用。

重要的事实是Mutex在第一次使用后不能被复制。

为并发访问创建 map

在 Golang 中,map 原语应被视为不安全的并发访问。在上一个教程中,我们描述了如何使用 Mutex 同步对资源的访问,这也可以用于对 map 原语的访问。但是 Go 标准库还提供了专为并发访问设计的 map 结构。这个教程将说明如何使用它。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe02

  2. 导航到目录。

  3. 创建文件map.go,内容如下:

        package main

        import (
          "fmt"
          "sync"
        )

        var names = []string{"Alan", "Joe", "Jack", "Ben",
                             "Ellen", "Lisa", "Carl", "Steve",
                             "Anton", "Yo"}

        func main() {

          m := sync.Map{}
          wg := &sync.WaitGroup{}
          wg.Add(10)
          for i := 0; i < 10; i++ {
            go func(idx int) {
              m.Store(fmt.Sprintf("%d", idx), names[idx])
              wg.Done()
            }(i)
          }
          wg.Wait()

          v, ok := m.Load("1")
          if ok {
            fmt.Printf("For Load key: 1 got %v\n", v)
          }

          v, ok = m.LoadOrStore("11", "Tim")
          if !ok {
            fmt.Printf("Key 11 missing stored val: %v\n", v)
          }

          m.Range(func(k, v interface{}) bool {
            key, _ := k.(string)
            t, _ := v.(string)
            fmt.Printf("For index %v got %v\n", key, t)
            return true
          })

        }
  1. 通过go run map.go执行代码。

  2. 查看输出:

它是如何工作的...

sync包中包含了Map结构,该结构被设计用于从多个 Go 例程中并发使用。Map结构及其方法模仿了 map 原语的行为。Store方法相当于m[key] = val语句。Load方法相当于val, ok := m[key]Range方法提供了遍历 map 的能力。请注意,Range函数与Map的当前状态一起工作,因此如果在运行Range方法期间更改了值,则会反映这些更改,但前提是该键尚未被访问。Range函数只会访问其键一次。

只运行一次代码块

在多个 goroutine 运行相同代码的情况下,例如,有一个初始化共享资源的代码块,Go 标准库提供了解决方案,将在下文中描述。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe03

  2. 导航到目录。

  3. 创建文件once.go,内容如下:

        package main

        import (
          "fmt"
          "sync"
        )

        var names = []interface{}{"Alan", "Joe", "Jack", "Ben",
                                  "Ellen", "Lisa", "Carl", "Steve",
                                  "Anton", "Yo"}

        type Source struct {
          m *sync.Mutex
          o *sync.Once
          data []interface{}
        }

        func (s *Source) Pop() (interface{}, error) {
          s.m.Lock()
          defer s.m.Unlock()
          s.o.Do(func() {
            s.data = names
            fmt.Println("Data has been loaded.")
          })
          if len(s.data) > 0 {
            res := s.data[0]
            s.data = s.data[1:]
            return res, nil
          }
          return nil, fmt.Errorf("No data available")
        }

        func main() {

          s := &Source{&sync.Mutex{}, &sync.Once{}, nil}
          wg := &sync.WaitGroup{}
          wg.Add(10)
          for i := 0; i < 10; i++ {
            go func(idx int) {
              // This code block is done only once
              if val, err := s.Pop(); err == nil {
                fmt.Printf("Pop %d returned: %s\n", idx, val)
              }
              wg.Done()
            }(i)
          }
          wg.Wait()
        }
  1. 使用go run once.go执行代码。

  2. 查看输出:

工作原理...

示例代码说明了在访问容器结构时数据的延迟加载。由于数据只应加载一次,因此在Pop方法中使用了sync包中的Once结构。Once只实现了一个名为Do的方法,该方法消耗了一个无参数的func,并且该函数在每个Once实例的执行期间只执行一次。

Do方法调用会阻塞,直到第一次运行完成。这一事实与Once旨在用于初始化的事实相对应。

在多个 goroutine 之间池化资源

资源池是提高性能和节省资源的传统方式。通常,值得使用昂贵初始化的资源进行池化。Go 标准库提供了用于资源池的骨架结构,被认为对多个 goroutine 访问是安全的。本示例描述了如何使用它。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe04

  2. 导航到目录。

  3. 创建文件pool.go,内容如下:

        package main

        import "sync"
        import "fmt"
        import "time"

        type Worker struct {
          id string
        }

        func (w *Worker) String() string {
          return w.id
        }

        var globalCounter = 0

        var pool = sync.Pool{
          New: func() interface{} {
            res := &Worker{fmt.Sprintf("%d", globalCounter)}
            globalCounter++
            return res
          },
        }

        func main() {
          wg := &sync.WaitGroup{}
          wg.Add(10)
          for i := 0; i < 10; i++ {
            go func(idx int) {
              // This code block is done only once
              w := pool.Get().(*Worker)
              fmt.Println("Got worker ID: " + w.String())
              time.Sleep(time.Second)
              pool.Put(w)
              wg.Done()
            }(i)
          }
          wg.Wait()
        }
  1. 使用go run pool.go执行代码。

  2. 查看输出:

工作原理...

sync包包含了用于池化资源的结构。Pool结构具有GetPut方法,用于检索资源并将其放回池中。Pool结构被认为对并发访问是安全的。

在创建Pool结构时,需要设置New字段。New字段是一个无参数函数,应该返回指向池化项目的指针。如果需要初始化池中的新对象,则会调用此函数。

从前面示例的日志中可以看出,Worker在返回到池中时被重用。重要的事实是,不应该对Get检索的项目和Put方法返回的项目做任何假设(比如我刚刚把三个对象放到池中,所以至少会有三个可用)。这主要是因为Pool中的空闲项目可能随时被自动删除。

如果资源初始化很昂贵,资源池化通常是值得的。然而,资源的管理也带来了一些额外的成本。

使用 WaitGroup 同步 goroutine

在处理并发运行的代码分支时,程序在某个时刻需要等待并发运行的代码部分。本示例介绍了如何使用WaitGroup等待运行的 goroutine。

如何做...

  1. 打开控制台并创建文件夹chapter10/recipe05

  2. 导航到目录。

  3. 创建文件syncgroup.go,内容如下:

        package main

        import "sync"
        import "fmt"

        func main() {
          wg := &sync.WaitGroup{}
          for i := 0; i < 10; i++ {
            wg.Add(1)
            go func(idx int) {
              // Do some work
              defer wg.Done()
              fmt.Printf("Exiting %d\n", idx)
            }(i)
          }
          wg.Wait()
          fmt.Println("All done.")
        }
  1. 使用go run syncgroup.go执行代码。

  2. 查看输出:

工作原理...

通过sync包中的WaitGroup结构,程序可以等待有限数量的 goroutine 完成运行。WaitGroup结构实现了Add方法,用于添加要等待的 goroutine 数量。然后在 goroutine 完成后,应调用Done方法来减少要等待的 goroutine 数量。Wait方法被调用时会阻塞,直到完成给定数量的Done调用(通常在goroutine结束时)。WaitGroup应该与sync包中的所有同步原语一样使用。在创建对象后,结构不应被复制。

从多个来源获取最快的结果

在某些情况下,例如,在整合来自多个来源的信息检索时,您只需要第一个结果,最快的结果,其他结果在那之后就不相关了。现实世界中的一个例子可能是提取货币汇率以计算价格。您有多个第三方服务,因为您需要尽快显示价格,所以只需要从任何服务接收到的第一个汇率。本教程将展示如何实现这种行为的模式。

如何做...

  1. 打开控制台并创建文件夹 chapter10/recipe06

  2. 导航到目录。

  3. 创建文件 first.go,内容如下:

        package main

        import (
          "context"
          "fmt"
          "sync"
          "time"
        )

        type SearchSrc struct {
          ID string
          Delay int
        }

        func (s *SearchSrc) Search(ctx context.Context) <-chan string {
          out := make(chan string)
          go func() {
            time.Sleep(time.Duration(s.Delay) * time.Second)
            select {
              case out <- "Result " + s.ID:
              case <-ctx.Done():
              fmt.Println("Search received Done()")
            }
            close(out)
            fmt.Println("Search finished for ID: " + s.ID)
          }()
          return out
        }

        func main() {

          ctx, cancel := context.WithCancel(context.Background())

          src1 := &SearchSrc{"1", 2}
          src2 := &SearchSrc{"2", 6}

          r1 := src1.Search(ctx)
          r2 := src2.Search(ctx)

          out := merge(ctx, r1, r2)

          for firstResult := range out {
            cancel()
            fmt.Println("First result is: " + firstResult)
          }
        }

        func merge(ctx context.Context, results ...<-chan string)
                   <-chan string {
          wg := sync.WaitGroup{}
          out := make(chan string)

          output := func(c <-chan string) {
            defer wg.Done()
            select {
              case <-ctx.Done():
                fmt.Println("Received ctx.Done()")
              case res := <-c:
              out <- res
            }
          }

          wg.Add(len(results))
          for _, c := range results {
            go output(c)
          }

          go func() {
            wg.Wait()
            close(out)
          }()
          return out
        }
  1. 通过 go run first.go 执行代码。

  2. 查看输出:

它是如何工作的...

上述代码提出了执行多个任务并输出一些结果的解决方案,我们只需要最快的一个。解决方案使用 Context 和取消函数来在获得第一个结果后调用取消。SearchSrc 结构提供了 Search 方法,该方法会导致写入结果的通道。请注意,Search 方法使用 time.Sleep 函数模拟延迟。对于来自 Search 方法的每个通道,合并函数触发写入最终输出通道的 goroutine,该通道在 main 方法中读取。从 merge 函数产生的输出通道接收到第一个结果时,将调用存储在变量 cancel 中的 CancelFunc 来取消其余处理。

请注意,Search 方法仍然需要结束,即使其结果不会被处理;因此,需要处理以避免 goroutine 和通道泄漏。

使用 errgroup 传播错误

本教程将展示如何轻松使用 errgroup 扩展包来检测 goroutine 组中运行子任务的错误。

如何做...

  1. 打开控制台并创建文件夹 chapter10/recipe07

  2. 导航到目录。

  3. 创建文件 lines.go,内容如下:

        package main

        import (
          "bufio"
          "context"
          "fmt"
          "log"
          "strings"

          "golang.org/x/sync/errgroup"
        )

        const data = `line one
        line two with more words
        error: This is erroneous line`

        func main() {
          log.Printf("Application %s starting.", "Error Detection")
          scanner := bufio.NewScanner(strings.NewReader(data))
          scanner.Split(bufio.ScanLines)

          // For each line fire a goroutine
          g, _ := errgroup.WithContext(context.Background())
          for scanner.Scan() {
            row := scanner.Text()
            g.Go(func() error {
              return func(s string) error {
                if strings.Contains(s, "error:") {
                  return fmt.Errorf(s)
                }
                return nil
              }(row)
            })
          }

          // Wait until the goroutines finish
          if err := g.Wait(); err != nil {
            fmt.Println("Error while waiting: " + err.Error())
          }

        }
  1. 通过 go run lines.go 执行代码。

  2. 查看输出:

它是如何工作的...

golang.org/x/sync/errgroup 包有助于简化 goroutine 组的错误传播和上下文取消。Group 包含消耗无参数函数返回 error 的 Go 方法。此函数应包含应由执行的 goroutine 完成的任务。errgroupGroupWait 方法等待直到 Go 方法中执行的所有任务完成,如果其中任何一个返回 err,则返回第一个非空错误。这样,就可以简单地从运行的 goroutine 组中传播错误。

请注意,Group 也是使用上下文创建的。Context 用作取消其他任务的机制,如果发生错误。在 goroutine 函数返回 error 后,内部实现会取消上下文,因此正在运行的任务也可能会被取消。

第十一章:提示和技巧

本章将涵盖以下示例:

  • 日志定制

  • 测试代码

  • 对代码进行基准测试

  • 创建子测试

  • 测试 HTTP 处理程序

  • 通过反射访问标签

  • 对切片进行排序

  • 将 HTTP 处理程序分成组

  • 利用 HTTP/2 服务器推送

介绍

这最后一章添加了一些与测试、设计应用程序接口以及利用sortreflect包相关的附加示例。

检查 Go 是否已正确安装。第一章准备就绪部分的检索 Golang 版本示例,与环境交互将帮助您。

确保端口8080未被其他应用程序使用。

日志定制

除了使用log包中的默认记录器进行记录外,标准库还提供了一种根据应用程序或包的需求创建自定义记录器的方法。本示例将简要介绍如何创建自定义记录器。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe01

  2. 导航到目录。

  3. 创建名为logging.go的文件,其中包含以下内容:

        package main

        import (
          "log"
          "os"
        )

        func main() {
          custLogger := log.New(os.Stdout, "custom1: ",
                                log.Ldate|log.Ltime)
          custLogger.Println("Hello I'm customized")

          custLoggerEnh := log.New(os.Stdout, "custom2: ",
                                   log.Ldate|log.Lshortfile)
          custLoggerEnh.Println("Hello I'm customized logger 2")

        }
  1. 通过go run logging.go执行代码。

  2. 查看输出:

它是如何工作的...

log包提供了New函数,简化了自定义记录器的创建。New函数接受Writer作为参数,该参数可以是实现Writer接口的任何对象,以及以字符串形式的前缀和由标志组成的日志消息的形式。最后一个参数是最有趣的,因为通过它,您可以使用动态字段增强日志消息,例如日期和文件名。

请注意,前面的示例中,第一个记录器custLogger配置了在日志消息前显示日期和时间的标志。第二个记录器custLoggerEnh使用标志LdateLshortfile来显示文件名和日期。

测试代码

测试和基准测试自然属于软件开发。作为一种现代语言,Go 支持从头开始进行这些操作。在这个示例中,将描述测试的基础知识。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe02

  2. 导航到目录。

  3. 创建名为sample_test.go的文件,其中包含以下内容:

        package main

        import (
          "strconv"
          "testing"
        )

        func TestSampleOne(t *testing.T) {
          expected := "11"
          result := strconv.Itoa(10)
          compare(expected, result, t)
        }

        func TestSampleTwo(t *testing.T) {
          expected := "11"
          result := strconv.Itoa(10)
          compareWithHelper(expected, result, t)
        }

        func TestSampleThree(t *testing.T) {
          expected := "10"
          result := strconv.Itoa(10)
          compare(expected, result, t)
        }

        func compareWithHelper(expected, result string, t *testing.T) {
          t.Helper()
          if expected != result {
            t.Fatalf("Expected result %v does not match result %v",
                     expected, result)
          }
        }

        func compare(expected, result string, t *testing.T) {
          if expected != result {
            t.Fatalf("Fail: Expected result %v does not match result %v",
                     expected, result)
          }
          t.Logf("OK: Expected result %v = %v",
                 expected, result)
        }
  1. 通过go test -v执行测试。

  2. 在终端中查看输出:

它是如何工作的...

标准库的testing包提供了对代码测试需求的支持。test函数需要满足名称模式TestXXX。默认情况下,测试工具会查找名为xxx_test.go的文件。请注意,每个测试函数都需要接受T指针参数,该参数提供了用于测试控制的有用方法。通过T结构指针,可以设置测试的状态。例如,FailFailNow方法会导致测试失败。借助T结构指针的帮助,可以通过调用SkipSkipfSkipNow来跳过测试。

T指针的有趣方法是Helper方法。通过调用Helper方法,当前函数被标记为辅助函数,如果在该函数内调用FailNowFatal),则测试输出将指向测试中调用该函数的代码行,如前面示例代码中所示。

请注意,如果测试工具未以详细模式运行(使用-v标志),或者特定测试失败(仅适用于T测试),则Log方法(及其变体)将不可见。尝试在不使用-v标志的情况下运行此示例代码。

另请参阅

  • 以下示例涵盖了基准测试的基础知识

  • 有关测试包的更详细描述,请参阅golang.org/pkg/testing中测试包的丰富文档。

对代码进行基准测试

上一个示例介绍了测试包的测试部分,在本示例中将介绍基准测试的基础知识。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe03

  2. 导航到目录。

  3. 创建名为sample_test.go的文件,内容如下:

        package main

        import (
          "log"
          "testing"
        )

        func BenchmarkSampleOne(b *testing.B) {
          logger := log.New(devNull{}, "test", log.Llongfile)
          b.ResetTimer()
          b.StartTimer()
          for i := 0; i < b.N; i++ {
            logger.Println("This si awesome")
          }
          b.StopTimer()
        }

        type devNull struct{}

        func (d devNull) Write(b []byte) (int, error) {
          return 0, nil
        }
  1. 通过go test -bench=执行基准测试。

  2. 在终端中查看输出:

它是如何工作的...

除了纯测试支持外,测试包还提供了用于测量代码性能的机制。为此,使用B结构指针作为参数,并且测试文件中的基准测试函数命名为BenchmarkXXXX

基准测试函数的关键部分是操作定时器和使用循环迭代计数器N

如您所见,定时器通过Reset/Start/StopTimer方法进行操作。通过这些方法,基准测试的结果会受到影响。请注意,定时器在基准测试函数开始时开始运行,而ResetTimer函数只是重新启动它。

BN字段是测量循环中的迭代次数。N值设置为足够高的值,以可靠地测量基准测试的结果。基准测试日志中显示迭代次数和每次迭代的测量时间。

另请参阅

  • 下一个示例将展示如何在测试中创建子测试

  • 有关基准测试的更多选项和信息,请查看此处的包文档:golang.org/pkg/testing

创建子测试

在某些情况下,有用的是创建一组可能具有类似设置或清理代码的测试。这可以在没有为每个测试创建单独函数的情况下完成。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe04

  2. 导航到目录。

  3. 创建名为sample_test.go的文件,内容如下:

        package main

        import (
          "fmt"
          "strconv"
          "testing"
        )

        var testData = []int{10, 11, 017}

        func TestSampleOne(t *testing.T) {
          expected := "10"
          for _, val := range testData {
            tc := val
            t.Run(fmt.Sprintf("input = %d", tc), func(t *testing.T) {
              if expected != strconv.Itoa(tc) {
                t.Fail()
              }
            })
          }
        }
  1. 通过go test -v执行测试。

  2. 在终端中查看输出:

它是如何工作的...

testing包的T结构还提供了Run方法,可用于运行嵌套测试。Run方法需要子测试的名称和将要执行的测试函数。例如,使用表驱动测试时,这种方法可能很有益。代码示例只是使用int值的简单切片作为输入。

基准测试结构B也包含相同的方法Run,可以提供一种创建复杂基准测试后续步骤的方法。

另请参阅

在包文档中仍有很多内容要找出,golang.org/pkg/testing

测试 HTTP 处理程序

测试HTTP服务器可能会很复杂。Go 标准库通过一个方便的包net/http/httptest简化了这一点。本示例描述了如何利用此包来测试HTTP处理程序。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe05

  2. 导航到目录。

  3. 创建名为sample_test.go的文件,内容如下:

        package main

        import (
          "fmt"
          "io/ioutil"
          "net/http"
          "net/http/httptest"
          "testing"
          "time"
        )

        const cookieName = "X-Cookie"

        func HandlerUnderTest(w http.ResponseWriter, r *http.Request) {
          http.SetCookie(w, &http.Cookie{
            Domain: "localhost",
            Expires: time.Now().Add(3 * time.Hour),
            Name: cookieName,
          })
          r.ParseForm()
          username := r.FormValue("username")
          fmt.Fprintf(w, "Hello %s!", username)
        }

        func TestHttpRequest(t *testing.T) {

          req := httptest.NewRequest("GET",
                          "http://unknown.io?username=John", nil)
          w := httptest.NewRecorder()
          HandlerUnderTest(w, req)

          var res *http.Cookie
          for _, c := range w.Result().Cookies() {
            if c.Name == cookieName {
              res = c
            }
          }

          if res == nil {
            t.Fatal("Cannot find " + cookieName)
          }

          content, err := ioutil.ReadAll(w.Result().Body)
          if err != nil {
            t.Fatal("Cannot read response body")
          }

          if string(content) != "Hello John!" {
            t.Fatal("Content not matching expected value")
          }
        }
  1. 通过go test执行测试。

  2. 在终端中查看输出:

它是如何工作的...

对于HandlerHandlerFunc的测试,可以利用net/http/httptest。该包提供了ResponseRecorder结构,能够记录响应内容并将其提供回来以断言值。用于组装请求的是net/http包的NewRequest函数。

net/http/httptest包还包含了在本地主机上监听系统选择端口的 HTTP 服务器版本。此实现旨在用于端到端测试。

通过反射访问标签

Go 语言允许给结构化字段打标签,附加额外信息。这些信息通常用作编码器的附加信息,或者对结构体进行任何类型的额外处理。这个示例将向你展示如何访问这些信息。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe06

  2. 导航到目录。

  3. 创建文件structtags.go,内容如下:

        package main

        import (
          "fmt"
          "reflect"
        )

        type Person struct {
          Name string `json:"p_name" bson:"pName"`
          Age int `json:"p_age" bson:"pAge"`
        }

        func main() {
          f := &Person{"Tom", 30}
          describe(f)
        }

        func describe(f interface{}) {
          val := reflect.TypeOf(f).Elem()
          for i := 0; i < val.NumField(); i++ {
            typeF := val.Field(i)
            fieldName := typeF.Name
            jsonTag := typeF.Tag.Get("json")
            bsonTag := typeF.Tag.Get("bson")
            fmt.Printf("Field : %s jsonTag: %s bsonTag: %s\n",
                       fieldName, jsonTag, bsonTag)
          }
        }
  1. 通过go run structtags.go执行代码。

  2. 在终端中查看输出:

它是如何工作的...

可以使用reflect包提取struct标签。通过调用TypeOf,我们得到了Person的指针Type,随后通过调用Elem,我们得到了指针指向的值的Type

结果的Type让我们可以访问struct类型Person及其字段。通过遍历字段并调用Field方法检索字段,我们可以获得StructFieldStructField类型包含Tag字段,该字段提供对struct标签的访问。然后,StructTag字段上的Get方法返回特定的标签。

对切片进行排序

数据排序是一个非常常见的任务。Go 标准库通过 sort 包简化了排序。这个示例简要介绍了如何使用它。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe07

  2. 导航到目录。

  3. 创建文件sort.go,内容如下:

        package main

        import (
          "fmt"
          "sort"
        )

        type Gopher struct {
          Name string
          Age int
        }

        var data = []Gopher{
          {"Daniel", 25},
          {"Tom", 19},
          {"Murthy", 33},
        }

        type Gophers []Gopher

        func (g Gophers) Len() int {
          return len(g)
        }

        func (g Gophers) Less(i, j int) bool {
          return g[i].Age > g[j].Age
        }

        func (g Gophers) Swap(i, j int) {
          tmp := g[j]
          g[j] = g[i]
          g[i] = tmp
        }

        func main() {

          sort.Slice(data, func(i, j int) bool {
            return sort.StringsAreSorted([]string{data[i].Name, 
                                      data[j].Name})
          })

          fmt.Printf("Sorted by name: %v\n", data)

          gophers := Gophers(data)
          sort.Sort(gophers)

          fmt.Printf("Sorted by age: %v\n", data)

        }
  1. 通过go run sort.go执行代码。

  2. 在终端中查看输出:

它是如何工作的...

示例代码展示了如何舒适地使用sort包对切片进行排序的两种方式。第一种方法更加临时,它使用了sort包的Slice函数。Slice函数消耗要排序的切片和所谓的 less 函数,该函数定义了元素i是否应该在元素j之前排序。

第二种方法需要更多的代码和提前规划。它利用了sort包的Interface接口。该接口充当数据的代表,并要求其在排序数据上实现必要的方法:Len(定义数据的数量)、Less(less 函数)、Swap(调用以交换元素)。如果数据值实现了这个接口,那么可以使用sort包的Sort函数。

原始类型切片float64intstringsort包中有涵盖。因此,可以使用现有的实现。例如,要对字符串切片进行排序,可以调用Strings函数。

将 HTTP 处理程序分组

这个示例提供了关于如何将 HTTP 处理程序分离成模块的建议。

如何做...

  1. 打开控制台并创建文件夹chapter11/recipe08

  2. 导航到目录。

  3. 创建文件handlegroups.go,内容如下:

        package main

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

         func main() {

           log.Println("Staring server...")
           // Adding to mani Mux
           mainMux := http.NewServeMux()
           mainMux.Handle("/api/",
           http.StripPrefix("/api", restModule()))
           mainMux.Handle("/ui/",
           http.StripPrefix("/ui", uiModule()))

           if err := http.ListenAndServe(":8080", mainMux); err != nil {
             panic(err)
           }

         }

         func restModule() http.Handler {
           // Separate Mux for all REST
           restApi := http.NewServeMux()
           restApi.HandleFunc("/users", func(w http.ResponseWriter,
                              r *http.Request) {
             w.Header().Set("Content-Type", "application/json")
             fmt.Fprint(w, `[{"id":1,"name":"John"}]`)
           })
           return restApi
         }

         func uiModule() http.Handler {
           // Separate Mux for all UI
           ui := http.NewServeMux()
           ui.HandleFunc("/users", func(w http.ResponseWriter, 
                         r *http.Request) {
             w.Header().Set("Content-Type", "text/html")
             fmt.Fprint(w, `<html><body>Hello from UI!</body></html>`)
           })

           return ui
         }
  1. 通过go run handlegroups.go执行代码。

  2. 查看输出:

  1. 访问浏览器 URLhttp://localhost:8080/api/users,输出应该如下所示:

  1. 同样,您可以测试http://localhost:8080/ui/users

它是如何工作的...

为了将处理程序分离成模块,代码使用了ServeMux来为每个模块(restui)进行处理。给定模块的 URL 处理是相对定义的。这意味着如果Handler的最终 URL 应该是/api/users,那么模块内定义的路径将是/users。模块本身将设置为/api/ URL。

通过利用StripPrefix函数将模块插入到名为mainMux的主ServeMux指针中,模块被插入到主ServeMux中。例如,通过StripPrefix("/api",restModule())将由restModule函数创建的 REST 模块插入到主ServeMux中。然后模块内的处理 URL 将是/users,而不是/api/users

利用 HTTP/2 服务器推送

HTTP/2 规范为服务器提供了在被请求之前推送资源的能力。本示例演示了如何实现服务器推送。

准备工作

准备私钥和自签名 X-509 证书。为此,可以使用openssl实用程序。通过执行命令openssl genrsa -out server.key 2048,使用 RSA 算法生成私钥文件server.key。基于此私钥,可以通过调用openssl req -new -x509 -sha256 -key server.key -out server.crt -days 365生成 X-509 证书。创建了server.crt文件。

操作步骤...

  1. 打开控制台并创建文件夹chapter11/recipe09

  2. 导航到目录。

  3. 创建文件push.go,内容如下:

        package main

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

        func main() {

          log.Println("Staring server...")
          // Adding to mani Mux
          http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
            if p, ok := w.(http.Pusher); ok {
              if err := p.Push("/app.css", nil); err != nil {
                log.Printf("Push err : %v", err)
              }
            }
            io.WriteString(w,
              `<html>
                 <head>
                   <link rel="stylesheet" type="text/css" href="app.css">
                 </head>
                 <body>
                   <p>Hello</p>
                 </body>
               </html>`
             )
           })
           http.HandleFunc("/app.css", func(w http.ResponseWriter,
                           r *http.Request) {
             io.WriteString(w,
               `p {
                 text-align: center;
                 color: red;
               }`)
           })

           if err := http.ListenAndServeTLS(":8080", "server.crt",
                                            "server.key", nil);
           err != nil {
             panic(err)
           }

         }
  1. 通过go run push.go启动服务器。

  2. 打开浏览器,在 URL https://localhost:8080 中打开开发者工具(查看Push作为app.css的发起者):

工作原理...

首先,注意 HTTP/2 需要安全连接。服务器推送非常简单实现。自 Go 1.8 以来,HTTP 包提供了Pusher接口,可以在资源被请求之前用于Push资产。如果客户端(通常是浏览器)支持 HTTP/2 协议并且与服务器的握手成功,HandlerHandlerFunc中的ResponseWriter可以转换为PusherPusher只提供Push方法。Push方法消耗目标(可以是绝对路径或绝对 URL)到资源和PushOptions,可以提供额外选项(默认情况下可以使用 nil)。

在上面的示例中,查看浏览器中开发者工具的输出。推送的资源在 Initiator 列中具有值Push