使用 API 和编写 web 客户端可能是一项棘手的工作。不同的 API 具有不同类型的授权、身份验证和协议。我们将探索http.Client
结构对象,与 OAuth2 客户机和长期令牌存储一起工作,最后使用 GRPC 和一个附加的 REST 接口。
在本章结束时,您应该了解如何与第三方或内部 API 接口,并了解一些常见操作的模式,例如对 API 的异步请求。
在本章中,我们将介绍以下配方:
- 初始化、存储和传递 http.Client 结构
- 为 RESTAPI 编写客户机
- 执行并行和异步客户端请求
- 利用 OAuth2 客户机
- 实现 OAuth2 令牌存储接口
- 在添加的功能和功能组合中包装客户端
- 了解 GRPC 客户
- 为 RPC 使用 twitchtv/twirp
为了继续本章中的所有配方,请按照以下步骤配置您的环境:
-
在您的操作系统上下载并安装 Go 1.12.6 或更高版本 https://golang.org/doc/install 。
-
打开终端或控制台应用,创建项目目录,如
~/projects/go-programming-cookbook
,并导航到此目录。所有代码将从此目录运行和修改。 -
将最新的代码克隆到
~/projects/go-programming-cookbook-original
中,并且可以选择从该目录工作,而不是手动键入示例,如下所示:
$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original
Gonet/http
包公开了一个灵活的http.Client
结构,用于处理 HTTP API。此结构具有单独的传输功能,使得短路请求、修改每个客户端操作的头以及处理任何 REST 操作相对简单。创建客户机是一项非常常见的操作,本食谱将从工作和创建http.Client
对象的基础知识开始。
这些步骤包括应用的编写和运行:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/client
的新目录,并导航到此目录。 - 运行以下命令:
$ 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
-
从
~/projects/go-programming-cookbook-original/chapter7/client
复制测试,或者将其作为练习来编写自己的代码! -
创建一个名为
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
}
- 创建一个名为
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
}
- 创建一个名为
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
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个名为
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)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
results of DefaultGetGolang: 200
results of DoOps: 200
results of client.DoOps 200
results of DefaultGetGolang: 418
- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
net/http
包公开了一个DefaultClient
包变量,用于以下内部操作:Do
、GET
、POST
等。我们的Setup()
函数返回一个客户端,并将默认客户端设置为同一个客户端。设置客户端时,您的大部分修改将在传输中进行,而传输只需要实现RoundTripper
接口。
该配方给出了一个始终返回 418 状态代码的无操作往返器示例。您可以想象这对测试有多有用。它还演示了如何将客户机作为函数参数传入、将其用作结构参数以及使用默认客户机处理请求。
为 RESTAPI 编写客户机不仅可以帮助您更好地理解所讨论的 API,还可以为将来使用该 API 的所有应用提供有用的工具。本食谱将探索构建客户机,并展示一些您可以立即利用的策略。
对于这个客户机,我们将假设身份验证由基本身份验证来处理,但也应该可以点击端点来检索令牌,依此类推。为了简单起见,我们假设我们的 API 公开了一个端点GetGoogle()
,它将从GET
请求返回的状态代码返回给https://www.google.com 。
这些步骤包括应用的编写和运行:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/rest
的新目录,并导航到此目录。 - 运行以下命令:
$ 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
- 从
~/projects/go-programming-cookbook-original/chapter7/rest
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
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
}
- 创建一个名为
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)
}
- 创建一个名为
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
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个名为
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)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ go build $ ./example
您现在应该看到以下输出:
$ go run main.go
Result of GetGoogle: 200
- 如果您复制或编写了自己的测试,请转到一个目录并运行
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
文件中完成。
这些步骤包括应用的编写和运行:
-
从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/async
的新目录,并导航到此目录。 -
运行以下命令:
$ 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
- 从
~/projects/go-programming-cookbook-original/chapter7/async
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
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
}
- 创建一个名为
exec.go
的文件,其内容如下:
package async
// FetchAll grabs a list of urls
func FetchAll(urls []string, c *Client) {
for _, url := range urls {
go c.AsyncGet(url)
}
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个名为
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)
}
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ 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
- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
这个方法创建了一个框架,用于使用单个客户端以扇出async
的方式处理请求。它将尝试尽可能快地检索您指定的尽可能多的 URL。在许多情况下,您会希望使用诸如工作池之类的工具进一步限制这一点。在客户端之外处理这些async
Go 例程,以及处理特定的存储或检索接口,也可能是有意义的。
本食谱还探讨了如何使用 case 语句打开多个通道。因为回迁是异步执行的,所以必须有某种机制等待它们完成。在这种情况下,只有当主函数读取的响应和错误数量与原始列表中的 URL 数量相同时,程序才会终止。在这种情况下,重要的是考虑你的应用是否应该超时,或者是否有其他方式提前取消它的操作。
OAuth2 是一个相对通用的与 API 对话的协议。golang.org/x/oauth2
包为使用 OAuth2 提供了一个非常灵活的客户端。它有子包,为各种提供商(如 Facebook、Google 和 GitHub)指定端点。
此配方将演示如何创建新的 GitHub OAuth2 客户端及其一些基本用法。
完成本章开头的技术要求一节中提到的初始设置步骤后,继续执行以下步骤:
- 在处配置 OAuth 客户端 https://github.com/settings/applications/new 。
- 使用客户端 ID 和密码设置环境变量:
export GITHUB_CLIENT="your_client"
export GITHUB_SECRET="your_secret"
- 在上复习 GitHub API 文档 https://developer.github.com/v3/ 。
这些步骤包括应用的编写和运行:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/oauthcli
的新目录,并导航到此目录。 - 运行以下命令:
$ 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
- 从
~/projects/go-programming-cookbook-original/chapter7/oauthcli
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
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)
}
- 创建一个名为
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
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
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)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ 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>}
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
标准 OAuth2 流基于重定向,并以服务器重定向到您指定的端点结束。然后,服务器负责获取代码并将其交换为令牌。该配方绕过了该要求,允许我们使用 URL,如https://localhost
或https://a-domain-you-own
,手动复制/粘贴代码,然后点击输入。交换令牌后,客户端将根据需要智能地刷新令牌。
需要注意的是,我们没有以任何方式存储令牌。如果程序崩溃,它必须重新交换令牌。还需要注意的是,除非刷新令牌过期、丢失或损坏,否则我们只需要显式检索令牌一次。一旦配置了客户端,只要在 OAuth2 流期间请求了适当的作用域,它就应该能够对 API 执行所有典型的 HTTP 操作。此配方需要"repo"
和"user"
范围,但可以根据需要添加更多或更少。
在前面的配方中,我们为客户机检索了一个令牌,并执行了 API 请求。这种方法的缺点是我们没有令牌的长期存储。例如,在 HTTP 服务器中,我们希望在请求之间具有一致的令牌存储。
此方法将探索如何修改 OAuth2 客户端,以便在请求之间存储令牌,并根据需要使用密钥检索令牌。为了简单起见,此密钥将是一个文件,但也可以是数据库、Redis 等。
请参阅使用 OAuth2 客户端配方中的准备部分。
这些步骤包括应用的编写和运行:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/oauthstore
的新目录,并导航到此目录。 - 运行以下命令:
$ 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
-
从
~/projects/go-programming-cookbook-original/chapter7/oauthstore
复制测试,或者将其作为练习来编写自己的代码! -
创建一个名为
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))
}
- 创建一个名为
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}
}
- 创建一个名为
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)
}
- 创建一个名为
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
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个名为
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)
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ 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>}
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
此配方负责在文件中存储和检索令牌的内容。如果是第一次运行,它必须执行整个代码交换,但后续运行将重用访问令牌,如果有可用的访问令牌,它将使用刷新令牌进行刷新。
这段代码中目前没有办法区分用户/令牌,但这可以通过 cookie 作为文件名或数据库中某一行的键来实现。让我们了解一下此代码的作用:
-
config.go
文件包装了标准的 OAuth2 配置。对于涉及检索令牌的每个方法,我们首先检查本地存储中是否有有效的令牌。如果没有,我们使用标准配置检索一个,然后存储它。 -
tokensource.go
文件实现了我们定制的TokenSource
接口,该接口与Config
配对。与Config
类似,我们总是首先尝试从文件中检索我们的令牌;如果失败,我们将其设置为新令牌。 -
storage.go
文件是Config
和TokenSource
使用的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
功能,可在需要时与多种中间件一起使用。
这些步骤包括应用的编写和运行:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/decorator
的新目录,并导航到此目录。 - 运行以下命令:
$ 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
- 从
~/projects/go-programming-cookbook-original/chapter7/decorator
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
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
}
- 创建一个名为
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
}
- 创建一个名为
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
})
}
}
- 创建一个名为
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
}
- 创建一个名为
example
的新目录并导航到它。 - 创建一个包含以下内容的
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)
}
}
- 运行
go run main.go
。 - 您还可以运行以下命令:
$ 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
- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
这个方法利用闭包作为一级公民和接口。实现这一点的主要技巧是让函数实现接口。这允许我们将由结构实现的接口包装为由函数实现的接口。
middleware.go
文件包含两个示例客户端中间件函数。这些可以扩展为包含额外的中间件,例如更复杂的身份验证和度量。此配方还可以与前面的配方组合,以生成一个 OAuth2 客户端,该客户端可以通过额外的中间件进行扩展。
Decorator
功能是一种方便的功能,可实现以下功能:
Decorate(RoundTripper, Middleware1, Middleware2, etc)
vs
var t RoundTripper
t = Middleware1(t)
t = Middleware2(t)
etc
与包装客户机相比,这种方法的优点是我们可以保持接口稀疏。如果你想要一个功能齐全的客户端,你还需要实现诸如GET
、POST
和PostForm
等方法。
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
这些步骤包括应用的编写和运行:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/grpc
的新目录,并导航到此目录。 - 运行以下命令:
$ 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
- 从
~/projects/go-programming-cookbook-original/chapter7/grpc
复制测试,或者将其作为练习来编写自己的代码! - 创建一个名为
greeter
的目录并导航到它。 - 创建一个名为
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;
}
- 将目录导航回
grpc
。 - 运行以下命令:
$ protoc --go_out=plugins=grpc:. greeter/greeter.proto
- 创建一个名为
server
的新目录并导航到它。 - 创建一个名为
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
}
- 创建一个名为
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)
}
- 将目录导航回
grpc
。 - 创建一个名为
client
的新目录并导航到它。 - 创建一个名为
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)
}
-
将目录导航回
grpc
。 -
运行
go run ./server
,您将看到以下输出:
$ go run ./server
Listening on port :4444
- 在单独的终端中,从
grpc
目录运行go run ./client
,您将看到以下输出:
$ go run ./client
response:"Hello Reader!"
response:"Goodbye Reader!"
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。- 如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
GRPC 服务器设置为侦听端口4444
。一旦客户端连接,它就可以发送请求并从服务器接收响应。请求、响应和支持的方法的结构由我们在步骤 4 中创建的.proto
文件决定。实际上,在与 GRPC 服务器集成时,它们应该提供.proto
文件,该文件可用于自动生成客户端。
除了客户端,protoc
命令还为服务器生成存根,只需填写实现细节即可。生成的 Go 代码也有 JSON 标记,相同的结构可以用于 JSON REST 服务。我们的代码设置了一个不安全的客户端。要安全地处理 GRPC,您需要使用 SSL 证书。
twitchtv/twirp
RPC 框架提供了 GRPC 的许多好处,包括使用协议缓冲区构建模型(https://developers.google.com/protocol-buffers ),并允许通过 HTTP 1.1 进行通信。它还可以使用 JSON 进行通信,因此可以使用curl
命令与twirp
RPC 服务进行通信。该配方将实施与之前 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
这些步骤包括应用的编写和运行:
- 从终端或控制台应用中,创建一个名为
~/projects/go-programming-cookbook/chapter7/twirp
的新目录,并导航到此目录。 - 运行以下命令:
$ 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
-
从
~/projects/go-programming-cookbook-original/chapter7/twirp
复制测试,或者将其作为练习来编写自己的代码! -
创建一个名为
rpc/greeter
的目录并导航到它。 -
创建一个名为
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;
}
- 将目录导航回
twirp
。 - 运行以下命令:
$ protoc --proto_path=$GOPATH/src:. --twirp_out=. --go_out=. ./rpc/greeter/greeter.proto
- 创建一个名为
server
的新目录并导航到它。 - 创建一个名为
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
}
- 创建一个名为
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)
}
- 将目录导航回
twirp
。 - 创建一个名为
client
的新目录并导航到它。 - 创建一个名为
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)
}
- 将目录导航回
twirp
。 - 运行
go run ./server
,您将看到以下输出:
$ go run ./server
Listening on port :4444
- 在单独的终端中,从
twirp
目录运行go run ./client
。您应该看到以下输出:
$ go run ./client
response:"Hello Reader."
response:"Goodbye Reader."
-
go.mod
文件可能会被更新,go.sum
文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请转到一个目录并运行
go test
。确保所有测试都通过。
我们将twitchtv/twirp
RPC 服务器设置为侦听端口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."}