Skip to content

Latest commit

 

History

History
1553 lines (1180 loc) · 50.7 KB

File metadata and controls

1553 lines (1180 loc) · 50.7 KB

七、Web 客户端和 API

使用 API 和编写 web 客户端可能是一项棘手的工作。不同的 API 具有不同类型的授权、身份验证和协议。我们将探索http.Client结构对象,与 OAuth2 客户机和长期令牌存储一起工作,最后使用 GRPC 和一个附加的 REST 接口。

在本章结束时,您应该了解如何与第三方或内部 API 接口,并了解一些常见操作的模式,例如对 API 的异步请求。

在本章中,我们将介绍以下配方:

  • 初始化、存储和传递 http.Client 结构
  • 为 RESTAPI 编写客户机
  • 执行并行和异步客户端请求
  • 利用 OAuth2 客户机
  • 实现 OAuth2 令牌存储接口
  • 在添加的功能和功能组合中包装客户端
  • 了解 GRPC 客户
  • 为 RPC 使用 twitchtv/twirp

技术要求

为了继续本章中的所有配方,请按照以下步骤配置您的环境:

  1. 在您的操作系统上下载并安装 Go 1.12.6 或更高版本 https://golang.org/doc/install

  2. 打开终端或控制台应用,创建项目目录,如~/projects/go-programming-cookbook,并导航到此目录。所有代码将从此目录运行和修改。

  3. 将最新的代码克隆到~/projects/go-programming-cookbook-original中,并且可以选择从该目录工作,而不是手动键入示例,如下所示:

$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original

初始化、存储和传递 http.Client 结构

Gonet/http包公开了一个灵活的http.Client结构,用于处理 HTTP API。此结构具有单独的传输功能,使得短路请求、修改每个客户端操作的头以及处理任何 REST 操作相对简单。创建客户机是一项非常常见的操作,本食谱将从工作和创建http.Client对象的基础知识开始。

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/client的新目录,并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/client 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/client 
  1. ~/projects/go-programming-cookbook-original/chapter7/client复制测试,或者将其作为练习来编写自己的代码!

  2. 创建一个名为client.go的文件,其内容如下:

        package client

        import (
            "crypto/tls"
            "net/http"
        )

        // Setup configures our client and redefines
        // the global DefaultClient
        func Setup(isSecure, nop bool) *http.Client {
            c := http.DefaultClient

            // Sometimes for testing, we want to
            // turn off SSL verification
            if !isSecure {
                c.Transport = &http.Transport{
                TLSClientConfig: &tls.Config{
                    InsecureSkipVerify: false,
                },
            }
        }
        if nop {
            c.Transport = &NopTransport{}
        }
        http.DefaultClient = c
        return c
        }

        // NopTransport is a No-Op Transport
        type NopTransport struct {
        }

        // RoundTrip Implements RoundTripper interface
        func (n *NopTransport) RoundTrip(*http.Request) 
        (*http.Response, error) {
            // note this is an unitialized Response
            // if you're looking at headers etc
            return &http.Response{StatusCode: http.StatusTeapot}, nil
        }
  1. 创建一个名为exec.go的文件,其内容如下:
        package client

        import (
            "fmt"
            "net/http"
        )

        // DoOps takes a client, then fetches
        // google.com
        func DoOps(c *http.Client) error {
            resp, err := c.Get("http://www.google.com")
            if err != nil {
                return err
            }
            fmt.Println("results of DoOps:", resp.StatusCode)

            return nil
        }

        // DefaultGetGolang uses the default client
        // to get golang.org
        func DefaultGetGolang() error {
            resp, err := http.Get("https://www.golang.org")
            if err != nil {
                return err
            }
            fmt.Println("results of DefaultGetGolang:", 
            resp.StatusCode)
            return nil
        }
  1. 创建一个名为store.go的文件,其内容如下:
        package client

        import (
            "fmt"
            "net/http"
        )

        // Controller embeds an http.Client
        // and uses it internally
        type Controller struct {
            *http.Client
        }

        // DoOps with a controller object
        func (c *Controller) DoOps() error {
            resp, err := c.Client.Get("http://www.google.com")
            if err != nil {
                return err
            }
            fmt.Println("results of client.DoOps", resp.StatusCode)
            return nil
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import "github.com/PacktPublishing/
                Go-Programming-Cookbook-Second-Edition/
                chapter7/client"

        func main() {
            // secure and op!
            cli := client.Setup(true, false)

            if err := client.DefaultGetGolang(); err != nil {
                panic(err)
            }

            if err := client.DoOps(cli); err != nil {
                panic(err)
            }

            c := client.Controller{Client: cli}
            if err := c.DoOps(); err != nil {
                panic(err)
            }

            // secure and noop
            // also modifies default
            client.Setup(true, true)

            if err := client.DefaultGetGolang(); err != nil {
                panic(err)
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
results of DefaultGetGolang: 200
results of DoOps: 200
results of client.DoOps 200
results of DefaultGetGolang: 418
  1. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

net/http包公开了一个DefaultClient包变量,用于以下内部操作:DoGETPOST等。我们的Setup()函数返回一个客户端,并将默认客户端设置为同一个客户端。设置客户端时,您的大部分修改将在传输中进行,而传输只需要实现RoundTripper接口。

该配方给出了一个始终返回 418 状态代码的无操作往返器示例。您可以想象这对测试有多有用。它还演示了如何将客户机作为函数参数传入、将其用作结构参数以及使用默认客户机处理请求。

为 RESTAPI 编写客户机

为 RESTAPI 编写客户机不仅可以帮助您更好地理解所讨论的 API,还可以为将来使用该 API 的所有应用提供有用的工具。本食谱将探索构建客户机,并展示一些您可以立即利用的策略。

对于这个客户机,我们将假设身份验证由基本身份验证来处理,但也应该可以点击端点来检索令牌,依此类推。为了简单起见,我们假设我们的 API 公开了一个端点GetGoogle(),它将从GET请求返回的状态代码返回给https://www.google.com

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/rest的新目录,并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/rest 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/rest 
  1. ~/projects/go-programming-cookbook-original/chapter7/rest复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为client.go的文件,其内容如下:
        package rest

        import "net/http"

        // APIClient is our custom client
        type APIClient struct {
            *http.Client
        }

        // NewAPIClient constructor initializes the client with our
        // custom Transport
        func NewAPIClient(username, password string) *APIClient {
            t := http.Transport{}
            return &APIClient{
                Client: &http.Client{
                    Transport: &APITransport{
                        Transport: &t,
                        username: username,
                        password: password,
                    },
                },
            }
        }

        // GetGoogle is an API Call - we abstract away
        // the REST aspects
        func (c *APIClient) GetGoogle() (int, error) {
            resp, err := c.Get("http://www.google.com")
            if err != nil {
                return 0, err
            }
            return resp.StatusCode, nil
        }
  1. 创建一个名为transport.go的文件,其内容如下:
        package rest

        import "net/http"

        // APITransport does a SetBasicAuth
        // for every request
        type APITransport struct {
            *http.Transport
            username, password string
        }

        // RoundTrip does the basic auth before deferring to the
        // default transport
        func (t *APITransport) RoundTrip(req *http.Request) 
        (*http.Response, error) {
            req.SetBasicAuth(t.username, t.password)
            return t.Transport.RoundTrip(req)
        }
  1. 创建一个名为exec.go的文件,其内容如下:
        package rest

        import "fmt"

        // Exec creates an API Client and uses its
        // GetGoogle method, then prints the result
        func Exec() error {
            c := NewAPIClient("username", "password")

            StatusCode, err := c.GetGoogle()
            if err != nil {
                return err
            }
            fmt.Println("Result of GetGoogle:", StatusCode)
            return nil
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import "github.com/PacktPublishing/
                Go-Programming-Cookbook-Second-Edition/
                chapter7/rest"

        func main() {
            if err := rest.Exec(); err != nil {
                panic(err)
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
Result of GetGoogle: 200
  1. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

这段代码演示了如何隐藏逻辑,例如身份验证,以及如何使用Transport接口执行令牌刷新。它还演示了如何通过方法公开 API 调用。如果我们是针对诸如用户 API 之类的东西实现的,我们会期望使用以下方法:

type API interface{
  GetUsers() (Users, error)
  CreateUser(User) error
  UpdateUser(User) error
  DeleteUser(User)
}

如果您已经阅读了第 5 章所有 ab**输出数据库和存储,这可能类似于题为执行数据库事务接口的配方。这种通过接口的组合,特别是像RoundTripper接口这样的普通接口,为编写 API 提供了很大的灵活性。此外,像我们前面所做的那样编写一个顶级接口,并将接口传递给客户机而不是直接传递给客户机可能会很有用。在编写 OAuth2 客户机时,我们将在下一步中更详细地探讨这一点。

执行并行和异步客户端请求

在 Go 中,并行执行客户端请求相对简单。在下面的方法中,我们将使用一个客户端使用 Go 缓冲通道检索多个 URL。响应和错误都将转到一个单独的通道,任何有权访问客户端的人都可以轻松访问该通道。

在这个配方中,创建客户端、读取通道、处理响应和错误都将在main.go文件中完成。

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/async的新目录,并导航到此目录。

  2. 运行以下命令:

$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/async 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/async 
  1. ~/projects/go-programming-cookbook-original/chapter7/async复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为config.go的文件,其内容如下:
        package async

        import "net/http"

        // NewClient creates a new client and 
        // sets its appropriate channels
        func NewClient(client *http.Client, bufferSize int) *Client {
            respch := make(chan *http.Response, bufferSize)
            errch := make(chan error, bufferSize)
            return &Client{
                Client: client,
                Resp: respch,
                Err: errch,
            }
        }

        // Client stores a client and has two channels to aggregate
        // responses and errors
        type Client struct {
            *http.Client
            Resp chan *http.Response
            Err chan error
        }

        // AsyncGet performs a Get then returns
        // the resp/error to the appropriate channel
        func (c *Client) AsyncGet(url string) {
            resp, err := c.Get(url)
            if err != nil {
                c.Err <- err
                return
            }
            c.Resp <- resp
        }
  1. 创建一个名为exec.go的文件,其内容如下:
        package async

        // FetchAll grabs a list of urls
        func FetchAll(urls []string, c *Client) {
            for _, url := range urls {
                go c.AsyncGet(url)
            }
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import (
            "fmt"
            "net/http"

            "github.com/PacktPublishing/
             Go-Programming-Cookbook-Second-Edition/chapter7/async"
        )

        func main() {
            urls := []string{
                "https://www.google.com",
                "https://golang.org",
                "https://www.github.com",
            }
            c := async.NewClient(http.DefaultClient, len(urls))
            async.FetchAll(urls, c)

            for i := 0; i < len(urls); i++ {
                select {
                    case resp := <-c.Resp:
                    fmt.Printf("Status received for %s: %d\n", 
                    resp.Request.URL, resp.StatusCode)
                    case err := <-c.Err:
                   fmt.Printf("Error received: %s\n", err)
                }
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
Status received for https://www.google.com: 200
Status received for https://golang.org: 200
Status received for https://github.com/: 200
  1. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

这个方法创建了一个框架,用于使用单个客户端以扇出async的方式处理请求。它将尝试尽可能快地检索您指定的尽可能多的 URL。在许多情况下,您会希望使用诸如工作池之类的工具进一步限制这一点。在客户端之外处理这些asyncGo 例程,以及处理特定的存储或检索接口,也可能是有意义的。

本食谱还探讨了如何使用 case 语句打开多个通道。因为回迁是异步执行的,所以必须有某种机制等待它们完成。在这种情况下,只有当主函数读取的响应和错误数量与原始列表中的 URL 数量相同时,程序才会终止。在这种情况下,重要的是考虑你的应用是否应该超时,或者是否有其他方式提前取消它的操作。

利用 OAuth2 客户机

OAuth2 是一个相对通用的与 API 对话的协议。golang.org/x/oauth2包为使用 OAuth2 提供了一个非常灵活的客户端。它有子包,为各种提供商(如 Facebook、Google 和 GitHub)指定端点。

此配方将演示如何创建新的 GitHub OAuth2 客户端及其一些基本用法。

准备

完成本章开头的技术要求一节中提到的初始设置步骤后,继续执行以下步骤:

  1. 处配置 OAuth 客户端 https://github.com/settings/applications/new
  2. 使用客户端 ID 和密码设置环境变量:
    • export GITHUB_CLIENT="your_client"
    • export GITHUB_SECRET="your_secret"
  3. 上复习 GitHub API 文档 https://developer.github.com/v3/

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/oauthcli的新目录,并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthcli 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthcli 
  1. ~/projects/go-programming-cookbook-original/chapter7/oauthcli复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为config.go的文件,其内容如下:
        package oauthcli

        import (
            "context"
            "fmt"
            "os"

            "golang.org/x/oauth2"
            "golang.org/x/oauth2/github"
        )

        // Setup return an oauth2Config configured to talk
        // to github, you need environment variables set
        // for your id and secret
        func Setup() *oauth2.Config {
            return &oauth2.Config{
                ClientID: os.Getenv("GITHUB_CLIENT"),
                ClientSecret: os.Getenv("GITHUB_SECRET"),
                Scopes: []string{"repo", "user"},
                Endpoint: github.Endpoint,
            }
        }

        // GetToken retrieves a github oauth2 token
        func GetToken(ctx context.Context, conf *oauth2.Config) 
        (*oauth2.Token, error) {
            url := conf.AuthCodeURL("state")
            fmt.Printf("Type the following url into your browser and 
            follow the directions on screen: %v\n", url)
            fmt.Println("Paste the code returned in the redirect URL 
            and hit Enter:")

            var code string
            if _, err := fmt.Scan(&code); err != nil {
                return nil, err
            }
            return conf.Exchange(ctx, code)
        }
  1. 创建一个名为exec.go的文件,其内容如下:
        package oauthcli

        import (
            "fmt"
            "net/http"
        )

        // GetUsers uses an initialized oauth2 client to get
        // information about a user
        func GetUser(client *http.Client) error {
            url := fmt.Sprintf("https://api.github.com/user")

            resp, err := client.Get(url)
            if err != nil {
                return err
            }
            defer resp.Body.Close()
            fmt.Println("Status Code from", url, ":", resp.StatusCode)
            io.Copy(os.Stdout, resp.Body)
            return nil
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的main.go文件:
        package main

        import (
            "context"

            "github.com/PacktPublishing/
             Go-Programming-Cookbook-Second-Edition/
             chapter7/oauthcli"
        )

        func main() {
            ctx := context.Background()
            conf := oauthcli.Setup()

            tok, err := oauthcli.GetToken(ctx, conf)
            if err != nil {
                panic(err)
            }
            client := conf.Client(ctx, tok)

            if err := oauthcli.GetUser(client); err != nil {
                panic(err)
            }

        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
Visit the URL for the auth dialog: 
https://github.com/login/oauth/authorize?
access_type=offline&client_id=
<your_id>&response_type=code&scope=repo+user&state=state
Paste the code returned in the redirect URL and hit Enter:
<your_code>
Status Code from https://api.github.com/user: 200
{<json_payload>}
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

标准 OAuth2 流基于重定向,并以服务器重定向到您指定的端点结束。然后,服务器负责获取代码并将其交换为令牌。该配方绕过了该要求,允许我们使用 URL,如https://localhosthttps://a-domain-you-own,手动复制/粘贴代码,然后点击输入。交换令牌后,客户端将根据需要智能地刷新令牌。

需要注意的是,我们没有以任何方式存储令牌。如果程序崩溃,它必须重新交换令牌。还需要注意的是,除非刷新令牌过期、丢失或损坏,否则我们只需要显式检索令牌一次。一旦配置了客户端,只要在 OAuth2 流期间请求了适当的作用域,它就应该能够对 API 执行所有典型的 HTTP 操作。此配方需要"repo""user"范围,但可以根据需要添加更多或更少。

实现 OAuth2 令牌存储接口

在前面的配方中,我们为客户机检索了一个令牌,并执行了 API 请求。这种方法的缺点是我们没有令牌的长期存储。例如,在 HTTP 服务器中,我们希望在请求之间具有一致的令牌存储。

此方法将探索如何修改 OAuth2 客户端,以便在请求之间存储令牌,并根据需要使用密钥检索令牌。为了简单起见,此密钥将是一个文件,但也可以是数据库、Redis 等。

准备

请参阅使用 OAuth2 客户端配方中的准备部分。

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/oauthstore的新目录,并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthstore 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/oauthstore 
  1. ~/projects/go-programming-cookbook-original/chapter7/oauthstore复制测试,或者将其作为练习来编写自己的代码!

  2. 创建一个名为config.go的文件,其内容如下:

        package oauthstore

        import (
            "context"
            "net/http"

            "golang.org/x/oauth2"
        )

        // Config wraps the default oauth2.Config
        // and adds our storage
        type Config struct {
            *oauth2.Config
            Storage
        }

        // Exchange stores a token after retrieval
        func (c *Config) Exchange(ctx context.Context, code string)     
        (*oauth2.Token, error) {
            token, err := c.Config.Exchange(ctx, code)
            if err != nil {
                return nil, err
            }
            if err := c.Storage.SetToken(token); err != nil {
                return nil, err
            }
            return token, nil
        }

        // TokenSource can be passed a token which
        // is stored, or when a new one is retrieved,
        // that's stored
        func (c *Config) TokenSource(ctx context.Context, t 
        *oauth2.Token) oauth2.TokenSource {
            return StorageTokenSource(ctx, c, t)
        }

        // Client is attached to our TokenSource
        func (c *Config) Client(ctx context.Context, t *oauth2.Token) 
        *http.Client {
            return oauth2.NewClient(ctx, c.TokenSource(ctx, t))
        }
  1. 创建一个名为tokensource.go的文件,其内容如下:
        package oauthstore

        import (
            "context"

            "golang.org/x/oauth2"
        )

        type storageTokenSource struct {
            *Config
            oauth2.TokenSource
        }

        // Token satisfies the TokenSource interface
        func (s *storageTokenSource) Token() (*oauth2.Token, error) {
            if token, err := s.Config.Storage.GetToken(); err == nil && 
            token.Valid() {
                return token, err
            }
            token, err := s.TokenSource.Token()
            if err != nil {
                return token, err
            }
            if err := s.Config.Storage.SetToken(token); err != nil {
                return nil, err
            }
            return token, nil
        }

        // StorageTokenSource will be used by out configs TokenSource
        // function
        func StorageTokenSource(ctx context.Context, c *Config, t 
        *oauth2.Token) oauth2.TokenSource {
            if t == nil || !t.Valid() {
                if tok, err := c.Storage.GetToken(); err == nil {
                   t = tok
                }
            }
            ts := c.Config.TokenSource(ctx, t)
            return &storageTokenSource{c, ts}
        }
  1. 创建一个名为storage.go的文件,其内容如下:
        package oauthstore

        import (
            "context"
            "fmt"

            "golang.org/x/oauth2"
        )

        // Storage is our generic storage interface
        type Storage interface {
            GetToken() (*oauth2.Token, error)
            SetToken(*oauth2.Token) error
        }

        // GetToken retrieves a github oauth2 token
        func GetToken(ctx context.Context, conf Config) (*oauth2.Token, 
        error) {
            token, err := conf.Storage.GetToken()
            if err == nil && token.Valid() {
                return token, err
            }
            url := conf.AuthCodeURL("state")
            fmt.Printf("Type the following url into your browser and 
            follow the directions on screen: %v\n", url)
            fmt.Println("Paste the code returned in the redirect URL 
            and hit Enter:")

            var code string
            if _, err := fmt.Scan(&code); err != nil {
                return nil, err
            }
            return conf.Exchange(ctx, code)
        }
  1. 创建一个名为filestorage.go的文件,其内容如下:
        package oauthstore

        import (
            "encoding/json"
            "errors"
            "os"
            "sync"

            "golang.org/x/oauth2"
        )

        // FileStorage satisfies our storage interface
        type FileStorage struct {
            Path string
            mu sync.RWMutex
        }

        // GetToken retrieves a token from a file
        func (f *FileStorage) GetToken() (*oauth2.Token, error) {
            f.mu.RLock()
            defer f.mu.RUnlock()
            in, err := os.Open(f.Path)
            if err != nil {
                return nil, err
            }
            defer in.Close()
            var t *oauth2.Token
            data := json.NewDecoder(in)
            return t, data.Decode(&t)
        }

        // SetToken creates, truncates, then stores a token
        // in a file
        func (f *FileStorage) SetToken(t *oauth2.Token) error {
            if t == nil || !t.Valid() {
                return errors.New("bad token")
            }

            f.mu.Lock()
            defer f.mu.Unlock()
            out, err := os.OpenFile(f.Path, 
            os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755)
            if err != nil {
                return err
            }
            defer out.Close()
            data, err := json.Marshal(&t)
            if err != nil {
                return err
            }

            _, err = out.Write(data)
            return err
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个名为main.go的文件,其内容如下:
        package main

        import (
            "context"
            "io"
            "os"

            "github.com/PacktPublishing/
             Go-Programming-Cookbook-Second-Edition/
             chapter7/oauthstore"

            "golang.org/x/oauth2"
            "golang.org/x/oauth2/github"
        )

        func main() {
            conf := oauthstore.Config{
                Config: &oauth2.Config{
                    ClientID: os.Getenv("GITHUB_CLIENT"),
                    ClientSecret: os.Getenv("GITHUB_SECRET"),
                    Scopes: []string{"repo", "user"},
                    Endpoint: github.Endpoint,
                },
                Storage: &oauthstore.FileStorage{Path: "token.txt"},
            }
            ctx := context.Background()
            token, err := oauthstore.GetToken(ctx, conf)
            if err != nil {
                panic(err)
            }

            cli := conf.Client(ctx, token)
            resp, err := cli.Get("https://api.github.com/user")
            if err != nil {
                panic(err)
            }
            defer resp.Body.Close()
            io.Copy(os.Stdout, resp.Body)
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
Visit the URL for the auth dialog: 
https://github.com/login/oauth/authorize?
access_type=offline&client_id=
<your_id>&response_type=code&scope=repo+user&state=state
Paste the code returned in the redirect URL and hit Enter:
<your_code>
{<json_payload>}

$ go run main.go
{<json_payload>}
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

此配方负责在文件中存储和检索令牌的内容。如果是第一次运行,它必须执行整个代码交换,但后续运行将重用访问令牌,如果有可用的访问令牌,它将使用刷新令牌进行刷新。

这段代码中目前没有办法区分用户/令牌,但这可以通过 cookie 作为文件名或数据库中某一行的键来实现。让我们了解一下此代码的作用:

  • config.go文件包装了标准的 OAuth2 配置。对于涉及检索令牌的每个方法,我们首先检查本地存储中是否有有效的令牌。如果没有,我们使用标准配置检索一个,然后存储它。

  • tokensource.go文件实现了我们定制的TokenSource接口,该接口与Config配对。与Config类似,我们总是首先尝试从文件中检索我们的令牌;如果失败,我们将其设置为新令牌。

  • storage.go文件是ConfigTokenSource使用的storage接口。它只定义了两个方法,我们还包括一个 helper 函数来引导基于 OAuth2 代码的流,类似于我们在前面的方法中所做的,但是如果已经存在一个具有有效令牌的文件,则将使用它。

  • filestorage.go文件实现storage接口。当我们存储一个新的令牌时,我们首先截断文件并编写一个token结构的 JSON 表示。否则,我们解码文件并返回token

在添加的功能和功能组合中包装客户端

2015 年,Tomás Senart 就用接口包装http.Client结构做了一次精彩的演讲,让您能够利用中间件和功能组合。您可以在上了解更多信息 https://github.com/gophercon/2015-talks 。这个配方借鉴了他的想法,并演示了一个在http.Client结构的Transport接口上执行相同操作的示例,其方式与我们前面的配方类似,即为 REST API 编写客户端。

下面的方法将实现标准http.Client结构的日志记录和基本身份验证中间件。它还包括一个decorate功能,可在需要时与多种中间件一起使用。

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/decorator的新目录,并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/decorator 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/decorator 
  1. ~/projects/go-programming-cookbook-original/chapter7/decorator复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为config.go的文件,其内容如下:
        package decorator

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

        // Setup initializes our ClientInterface
        func Setup() *http.Client {
            c := http.Client{}

            t := Decorate(&http.Transport{},
                Logger(log.New(os.Stdout, "", 0)),
                BasicAuth("username", "password"),
            )
            c.Transport = t
            return &c
        }
  1. 创建一个名为decorator.go的文件,其内容如下:
        package decorator

        import "net/http"

        // TransportFunc implements the RountTripper interface
        type TransportFunc func(*http.Request) (*http.Response, error)

        // RoundTrip just calls the original function
        func (tf TransportFunc) RoundTrip(r *http.Request) 
        (*http.Response, error) {
            return tf(r)
        }

        // Decorator is a convenience function to represent our
        // middleware inner function
        type Decorator func(http.RoundTripper) http.RoundTripper

        // Decorate is a helper to wrap all the middleware
        func Decorate(t http.RoundTripper, rts ...Decorator) 
        http.RoundTripper {
            decorated := t
            for _, rt := range rts {
                decorated = rt(decorated)
            }
            return decorated
        }
  1. 创建一个名为middleware.go的文件,其内容如下:
        package decorator

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

        // Logger is one of our 'middleware' decorators
        func Logger(l *log.Logger) Decorator {
            return func(c http.RoundTripper) http.RoundTripper {
                return TransportFunc(func(r *http.Request) 
                (*http.Response, error) {
                   start := time.Now()
                   l.Printf("started request to %s at %s", r.URL,     
                   start.Format("2006-01-02 15:04:05"))
                   resp, err := c.RoundTrip(r)
                   l.Printf("completed request to %s in %s", r.URL, 
                   time.Since(start))
                   return resp, err
                })
            }
        }

        // BasicAuth is another of our 'middleware' decorators
        func BasicAuth(username, password string) Decorator {
            return func(c http.RoundTripper) http.RoundTripper {
                return TransportFunc(func(r *http.Request) 
                (*http.Response, error) {
                    r.SetBasicAuth(username, password)
                    resp, err := c.RoundTrip(r)
                    return resp, err
                })
            }
        }
  1. 创建一个名为exec.go的文件,其内容如下:
        package decorator

        import "fmt"

        // Exec creates a client, calls google.com
        // then prints the response
        func Exec() error {
            c := Setup()

            resp, err := c.Get("https://www.google.com")
            if err != nil {
                return err
            }
            fmt.Println("Response code:", resp.StatusCode)
            return nil
        }
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的main.go文件:
        package main

        import "github.com/PacktPublishing/
                Go-Programming-Cookbook-Second-Edition/
                chapter7/decorator"

        func main() {
            if err := decorator.Exec(); err != nil {
                panic(err)
            }
        }
  1. 运行go run main.go
  2. 您还可以运行以下命令:
$ go build $ ./example

您现在应该看到以下输出:

$ go run main.go
started request to https://www.google.com at 2017-01-01 13:38:42
completed request to https://www.google.com in 194.013054ms
Response code: 200
  1. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

这个方法利用闭包作为一级公民和接口。实现这一点的主要技巧是让函数实现接口。这允许我们将由结构实现的接口包装为由函数实现的接口。

middleware.go文件包含两个示例客户端中间件函数。这些可以扩展为包含额外的中间件,例如更复杂的身份验证和度量。此配方还可以与前面的配方组合,以生成一个 OAuth2 客户端,该客户端可以通过额外的中间件进行扩展。

Decorator功能是一种方便的功能,可实现以下功能:

Decorate(RoundTripper, Middleware1, Middleware2, etc)

vs

var t RoundTripper
t = Middleware1(t)
t = Middleware2(t)
etc

与包装客户机相比,这种方法的优点是我们可以保持接口稀疏。如果你想要一个功能齐全的客户端,你还需要实现诸如GETPOSTPostForm等方法。

了解 GRPC 客户

GRPC 是使用协议缓冲区(构建的高性能 RPC 框架 https://developers.google.com/protocol-buffers 和 HTTP/2(https://http2.github.io 。在 Go 中创建 GRPC 客户端涉及许多与使用 Go HTTP 客户端相同的复杂问题。为了演示客户端的基本用法,最简单的方法是实现一个服务器。这个菜谱将创建一个greeter服务,它接受一个问候语和一个名字,并返回句子<greeting> <name>!。此外,服务器还可以指定是否惊叹!``.(句号)。

有一些关于 GRPC 的细节,比如流媒体,这是本食谱不会探讨的;然而,它有望成为创建非常基本的服务器和客户机的介绍。

准备

完成本章开头的技术要求一节中提到的初始设置步骤后,安装 GRPC(https://grpc.io/docs/quickstart/go/ )并运行以下命令:

  • go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
  • go get -u google.golang.org/grpc

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/grpc的新目录,并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/grpc 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/grpc 
  1. ~/projects/go-programming-cookbook-original/chapter7/grpc复制测试,或者将其作为练习来编写自己的代码!
  2. 创建一个名为greeter的目录并导航到它。
  3. 创建一个名为greeter.proto的文件,其内容如下:
        syntax = "proto3";

        package greeter;

        service GreeterService{
            rpc Greet(GreetRequest) returns (GreetResponse) {}
        }

        message GreetRequest {
            string greeting = 1;
            string name = 2;
        }

        message GreetResponse{
            string response = 1;
        }
  1. 将目录导航回grpc
  2. 运行以下命令:
$ protoc --go_out=plugins=grpc:. greeter/greeter.proto
  1. 创建一个名为server的新目录并导航到它。
  2. 创建一个名为greeter.go的文件,包含以下内容。确保修改greeter导入以使用步骤 3 中设置的路径:
        package main

        import (
            "fmt"

            "github.com/PacktPublishing/
             Go-Programming-Cookbook-Second-Edition/
             chapter7/grpc/greeter"
            "golang.org/x/net/context"
        )

        // Greeter implements the interface
        // generated by protoc
        type Greeter struct {
            Exclaim bool
        }

        // Greet implements grpc Greet
        func (g *Greeter) Greet(ctx context.Context, r 
        *greeter.GreetRequest) (*greeter.GreetResponse, error) {
            msg := fmt.Sprintf("%s %s", r.GetGreeting(), r.GetName())
            if g.Exclaim {
                msg += "!"
            } else {
                msg += "."
            }
            return &greeter.GreetResponse{Response: msg}, nil
        }
  1. 创建一个名为server.go的文件,包含以下内容。确保修改greeter导入以使用步骤 3 中设置的路径:
        package main

        import (
            "fmt"
            "net"

            "github.com/PacktPublishing/
             Go-Programming-Cookbook-Second-Edition/
             chapter7/grpc/greeter"
            "google.golang.org/grpc"
        )

        func main() {
            grpcServer := grpc.NewServer()
            greeter.RegisterGreeterServiceServer(grpcServer, 
            &Greeter{Exclaim: true})
            lis, err := net.Listen("tcp", ":4444")
            if err != nil {
                panic(err)
            }
            fmt.Println("Listening on port :4444")
            grpcServer.Serve(lis)
        }
  1. 将目录导航回grpc
  2. 创建一个名为client的新目录并导航到它。
  3. 创建一个名为client.go的文件,包含以下内容。确保修改greeter导入以使用步骤 3 中设置的路径:
        package main

        import (
            "context"
            "fmt"

            "github.com/PacktPublishing/
             Go-Programming-Cookbook-Second-Edition/
             chapter7/grpc/greeter"
            "google.golang.org/grpc"
        )

        func main() {
            conn, err := grpc.Dial(":4444", grpc.WithInsecure())
            if err != nil {
                panic(err)
            }
            defer conn.Close()

            client := greeter.NewGreeterServiceClient(conn)

            ctx := context.Background()
            req := greeter.GreetRequest{Greeting: "Hello", Name: 
            "Reader"}
            resp, err := client.Greet(ctx, &req)
            if err != nil {
                panic(err)
            }
            fmt.Println(resp)

            req.Greeting = "Goodbye"
            resp, err = client.Greet(ctx, &req)
            if err != nil {
                panic(err)
            }
            fmt.Println(resp)
        }
  1. 将目录导航回grpc

  2. 运行go run ./server,您将看到以下输出:

$ go run ./server
Listening on port :4444
  1. 在单独的终端中,从grpc目录运行go run ./client,您将看到以下输出:
$ go run ./client
response:"Hello Reader!" 
response:"Goodbye Reader!"
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

GRPC 服务器设置为侦听端口4444。一旦客户端连接,它就可以发送请求并从服务器接收响应。请求、响应和支持的方法的结构由我们在步骤 4 中创建的.proto文件决定。实际上,在与 GRPC 服务器集成时,它们应该提供.proto文件,该文件可用于自动生成客户端。

除了客户端,protoc命令还为服务器生成存根,只需填写实现细节即可。生成的 Go 代码也有 JSON 标记,相同的结构可以用于 JSON REST 服务。我们的代码设置了一个不安全的客户端。要安全地处理 GRPC,您需要使用 SSL 证书。

为 RPC 使用 twitchtv/twirp

twitchtv/twirpRPC 框架提供了 GRPC 的许多好处,包括使用协议缓冲区构建模型(https://developers.google.com/protocol-buffers ),并允许通过 HTTP 1.1 进行通信。它还可以使用 JSON 进行通信,因此可以使用curl命令与twirpRPC 服务进行通信。该配方将实施与之前 GRPC 部分相同的greeter。此服务接受问候语和姓名,并返回句子<greeting> <name>!。此外,服务器可以指定是否感叹!或不感叹.

本食谱将不探讨twitchtv/twirp的其他功能,主要关注基本的客户机-服务器通信。有关支持内容的更多信息,请访问他们的 GitHub 页面(https://github.com/twitchtv/twirp

准备

完成本章开头的技术要求一节中提到的初始设置步骤后,安装 twirphttps://twitchtv.github.io/twirp/docs/install.html 并运行以下命令:

  • go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
  • go get github.com/twitchtv/twirp/protoc-gen-twirp

怎么做。。。

这些步骤包括应用的编写和运行:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter7/twirp的新目录,并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp 

您应该会看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp 
  1. ~/projects/go-programming-cookbook-original/chapter7/twirp复制测试,或者将其作为练习来编写自己的代码!

  2. 创建一个名为rpc/greeter的目录并导航到它。

  3. 创建一个名为greeter.proto的文件,其内容如下:

        syntax = "proto3";

        package greeter;

        service GreeterService{
            rpc Greet(GreetRequest) returns (GreetResponse) {}
        }

        message GreetRequest {
            string greeting = 1;
            string name = 2;
        }

        message GreetResponse{
            string response = 1;
        }
  1. 将目录导航回twirp
  2. 运行以下命令:
$ protoc --proto_path=$GOPATH/src:. --twirp_out=. --go_out=. ./rpc/greeter/greeter.proto
  1. 创建一个名为server的新目录并导航到它。
  2. 创建一个名为greeter.go的文件,包含以下内容。确保修改greeter导入以使用步骤 3 中设置的路径:
package main

import (
  "context"
  "fmt"

  "github.com/PacktPublishing/
   Go-Programming-Cookbook-Second-Edition/
   chapter7/twirp/rpc/greeter"
)

// Greeter implements the interface
// generated by protoc
type Greeter struct {
  Exclaim bool
}

// Greet implements twirp Greet
func (g *Greeter) Greet(ctx context.Context, r *greeter.GreetRequest) (*greeter.GreetResponse, error) {
  msg := fmt.Sprintf("%s %s", r.GetGreeting(), r.GetName())
  if g.Exclaim {
    msg += "!"
  } else {
    msg += "."
  }
  return &greeter.GreetResponse{Response: msg}, nil
}
  1. 创建一个名为server.go的文件,其中包含以下内容。请确保您修改了greeter导入,以使用您在步骤 3 中设置的路径:
package main

import (
  "fmt"
  "net/http"

  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp/rpc/greeter"
)

func main() {
  server := &Greeter{}
  twirpHandler := greeter.NewGreeterServiceServer(server, nil)

  fmt.Println("Listening on port :4444")
  http.ListenAndServe(":4444", twirpHandler)
}
  1. 将目录导航回twirp
  2. 创建一个名为client的新目录并导航到它。
  3. 创建一个名为client.go的文件,包含以下内容。确保修改greeter导入以使用步骤 3 中设置的路径:
package main

import (
  "context"
  "fmt"
  "net/http"

  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter7/twirp/rpc/greeter"
)

func main() {
  // you can put in a custom client for tighter controls on timeouts etc.
  client := greeter.NewGreeterServiceProtobufClient("http://localhost:4444", &http.Client{})

  ctx := context.Background()
  req := greeter.GreetRequest{Greeting: "Hello", Name: "Reader"}
  resp, err := client.Greet(ctx, &req)
  if err != nil {
    panic(err)
  }
  fmt.Println(resp)

  req.Greeting = "Goodbye"
  resp, err = client.Greet(ctx, &req)
  if err != nil {
    panic(err)
  }
  fmt.Println(resp)
}
  1. 将目录导航回twirp
  2. 运行go run ./server,您将看到以下输出:
$ go run ./server
Listening on port :4444
  1. 在单独的终端中,从twirp目录运行go run ./client。您应该看到以下输出:
$ go run ./client
response:"Hello Reader." 
response:"Goodbye Reader."
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。

  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

我们将twitchtv/twirpRPC 服务器设置为侦听端口4444。与 GRPC 类似,protoc可用于生成多种语言的客户端,例如,生成 Swagger(https://swagger.io/ 文件。

与 GRPC 一样,我们首先将模型定义为.proto文件,生成 Go 绑定,最后实现生成的接口。由于使用了.proto文件,只要您不依赖任何一个框架的更高级功能,GRPC 和twitchtv/twirp之间的代码相对来说是可移植的。

另外,由于twitchtv/twirp服务器支持 HTTP 1.1,我们可以curl进行如下操作:

$ curl --request "POST" \ 
 --location "http://localhost:4444/twirp/greeter.GreeterService/Greet" \
 --header "Content-Type:application/json" \
 --data '{"greeting": "Greetings to", "name":"you"}' 

{"response":"Greetings to you."}