Go 编程秘籍第二版(七)
原文:
zh.annas-archive.org/md5/6A3DCC49D461FA27A010AAE9FBA229E0译者:飞龙
第十三章:无服务器编程
本章将重点介绍无服务器架构以及如何在 Go 语言中使用它们。无服务器架构是指开发人员不管理后端服务器的架构。这包括 Amazon Lambda、Google App Engine 和 Firebase 等服务。这些服务允许您快速部署应用程序并在网络上存储数据。
本章中的所有示例都涉及到按使用计费的第三方服务;确保在使用完毕后进行清理。否则,可以将这些示例视为在这些平台上启动更大型应用程序的起步器。
在本章中,我们将涵盖以下内容:
-
使用 Apex 在 Lambda 上进行 Go 编程
-
Apex 无服务器日志和指标
-
使用 Go 的 Google App Engine
-
使用
firebase.google.com/go与 Firebase 一起工作
使用 Apex 在 Lambda 上进行 Go 编程
Apex 是一个用于构建、部署和管理 AWS Lambda 函数的工具。它曾经提供了一个用于在代码中管理 Lambda 函数的 Go shim,但现在可以使用原生的 AWS 库(github.com/aws/aws-lambda-go)来完成这个任务。本教程将探讨如何创建 Go Lambda 函数并使用 Apex 部署它们。
准备工作
根据以下步骤配置您的环境:
-
从
golang.org/doc/install下载并安装 Go 1.12.6 或更高版本到您的操作系统上。 -
从
apex.run/#installation安装 Apex。 -
打开终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/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
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/lambda的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/lambda
您应该看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/lambda
-
创建一个 Amazon 账户和一个可以编辑 Lambda 函数的 IAM 角色,可以从
aws.amazon.com/lambda/完成。 -
创建一个名为
~/.aws/credentials的文件,内容如下,将您在 Amazon 控制台中设置的凭据复制进去:
[default]
aws_access_key_id = xxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx
- 创建一个环境变量来保存您想要的区域:
export AWS_REGION=us-west-2
- 运行
apex init命令并按照屏幕上的说明进行操作:
$ apex init
Enter the name of your project. It should be machine-friendly, as this is used to prefix your functions in Lambda.
Project name: go-cookbook
Enter an optional description of your project.
Project description: Demonstrating Apex with the Go Cookbook
[+] creating IAM go-cookbook_lambda_function role
[+] creating IAM go-cookbook_lambda_logs policy
[+] attaching policy to lambda_function role.
[+] creating ./project.json
[+] creating ./functions
Setup complete, deploy those functions!
$ apex deploy
-
删除
lambda/functions/hello目录。 -
创建一个新的
lambda/functions/greeter1/main.go文件,内容如下:
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
)
// Message is the input to the function and
// includes a Name
type Message struct {
Name string `json:"name"`
}
// Response is sent back and contains a greeting
// string
type Response struct {
Greeting string `json:"greeting"`
}
// HandleRequest will be called when the lambda function is invoked
// it takes a Message and returns a Response that contains a greeting
func HandleRequest(ctx context.Context, m Message) (Response, error) {
return Response{Greeting: fmt.Sprintf("Hello, %s", m.Name)}, nil
}
func main() {
lambda.Start(HandleRequest)
}
- 创建一个新的
lambda/functions/greeter/main.go文件,内容如下:
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
)
// Message is the input to the function and
// includes a FirstName and LastName
type Message struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// Response is sent back and contains a greeting
// string
type Response struct {
Greeting string `json:"greeting"`
}
// HandleRequest will be called when the lambda function is invoked
// it takes a Message and returns a Response that contains a greeting
// this greeting contains the first and last name specified
func HandleRequest(ctx context.Context, m Message) (Response, error) {
return Response{Greeting: fmt.Sprintf("Hello, %s %s", m.FirstName, m.LastName)}, nil
}
func main() {
lambda.Start(HandleRequest)
}
- 部署它们:
$ apex deploy
• creating function env= function=greeter2
• creating function env= function=greeter1
• created alias current env= function=greeter2 version=4
• function created env= function=greeter2 name=go-cookbook_greeter2 version=1
• created alias current env= function=greeter1 version=5
• function created env= function=greeter1 name=go-cookbook_greeter1 version=1
- 调用新部署的函数:
$ echo '{"name": "Reader"}' | apex invoke greeter1 {"greeting":"Hello, Reader"}
$ echo '{"first_name": "Go", "last_name": "Coders"}' | apex invoke greeter2 {"greeting":"Hello, Go Coders"}
- 查看日志:
$ apex logs greeter2
apex logs greeter2
/aws/lambda/go-cookbook_greeter2 START RequestId: 7c0f9129-3830-11e7-8755-75aeb52a51b9 Version: 1
/aws/lambda/go-cookbook_greeter2 END RequestId: 7c0f9129-3830-11e7-8755-75aeb52a51b9
/aws/lambda/go-cookbook_greeter2 REPORT RequestId: 7c0f9129-3830-11e7-8755-75aeb52a51b9 Duration: 93.84 ms Billed Duration: 100 ms
Memory Size: 128 MB Max Memory Used: 19 MB
- 清理已部署的服务:
$ apex delete
The following will be deleted:
- greeter1 - greeter2
Are you sure? (yes/no) yes
• deleting env= function=greeter
• function deleted env= function=greeter
它是如何工作的...
AWS Lambda 使得无需维护服务器即可按需运行函数变得容易。Apex 提供了部署、版本控制和测试函数的功能,使您可以将它们发送到 Lambda。
Go 库(github.com/aws/aws-lambda-go)在 Lambda 中提供了原生的 Go 编译,并允许我们将 Go 代码部署为 Lambda 函数。这是通过定义一个处理程序、处理传入的请求有效负载并返回响应来实现的。目前,您定义的函数必须遵循这些规则:
-
处理程序必须是一个函数。
-
处理程序可能需要零到两个参数。
-
如果有两个参数,则第一个参数必须满足
context.Context接口。 -
处理程序可能返回零到两个参数。
-
如果有两个返回值,则第二个参数必须是一个错误。
-
如果只有一个返回值,它必须是一个错误。
在这个配方中,我们定义了两个问候函数,一个接受全名,另一个将名字分成名和姓。如果我们修改了一个函数greeter,而不是创建两个,Apex 将部署新版本,并在所有先前的示例中调用v2而不是v1。也可以使用apex rollback greeter进行回滚。
Apex 无服务器日志和指标
在使用 Lambda 等无服务器函数时,拥有可移植的结构化日志非常有价值。此外,您还可以将处理日志的早期配方与此配方结合使用。我们在第四章Go 中的错误处理中涵盖的配方同样相关。因为我们使用 Apex 来管理我们的 Lambda 函数,所以我们选择使用 Apex 记录器进行此配方。我们还将依赖 Apex 提供的指标,以及 AWS 控制台。早期的配方探讨了更复杂的日志记录和指标示例,这些仍然适用——Apex 记录器可以轻松配置为使用例如 Amazon Kinesis 或 Elasticsearch 来聚合日志。
准备工作
参考本章中Go 编程在 Apex 上的 Lambda配方的准备工作部分。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/logging的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/logging
您应该看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/logging
-
创建一个可以编辑 Lambda 函数的 Amazon 帐户和 IAM 角色,可以在
aws.amazon.com/lambda/上完成。 -
创建一个
~/.aws/credentials文件,其中包含以下内容,将您在 Amazon 控制台中设置的凭据复制过来:
[default]
aws_access_key_id = xxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxx
- 创建一个环境变量来保存您想要的区域:
export AWS_REGION=us-west-2
- 运行
apex init命令并按照屏幕上的说明进行操作:
$ apex init
Enter the name of your project. It should be machine-friendly, as this is used to prefix your functions in Lambda.
Project name: logging
Enter an optional description of your project.
Project description: An example of apex logging and metrics
[+] creating IAM logging_lambda_function role
[+] creating IAM logging_lambda_logs policy
[+] attaching policy to lambda_function role.
[+] creating ./project.json
[+] creating ./functions
Setup complete, deploy those functions!
$ apex deploy
-
删除
lambda/functions/hello目录。 -
创建一个新的
lambda/functions/secret/main.go文件,其中包含以下内容:
package main
import (
"context"
"os"
"github.com/apex/log"
"github.com/apex/log/handlers/text"
"github.com/aws/aws-lambda-go/lambda"
)
// Input takes in a secret
type Input struct {
Secret string `json:"secret"`
}
// HandleRequest will be called when the Lambda function is invoked
// it takes an input and checks if it matches our super secret value
func HandleRequest(ctx context.Context, input Input) (string, error) {
log.SetHandler(text.New(os.Stderr))
log.WithField("secret", input.Secret).Info("secret guessed")
if input.Secret == "klaatu barada nikto" {
return "secret guessed!", nil
}
return "try again", nil
}
func main() {
lambda.Start(HandleRequest)
}
- 将其部署到指定的区域:
$ apex deploy
• creating function env= function=secret
• created alias current env= function=secret version=1
• function created env= function=secret name=logging_secret version=1
- 要调用它,请运行以下命令:
$ echo '{"secret": "open sesame"}' | apex invoke secret
"try again"
$ echo '{"secret": "klaatu barada nikto"}' | apex invoke secret
"secret guessed!"
- 检查日志:
$ apex logs secret
/aws/lambda/logging_secret START RequestId: cfa6f655-3834-11e7-b99d-89998a7f39dd Version: 1
/aws/lambda/logging_secret INFO[0000] secret guessed secret=open sesame
/aws/lambda/logging_secret END RequestId: cfa6f655-3834-11e7-b99d-89998a7f39dd
/aws/lambda/logging_secret REPORT RequestId: cfa6f655-3834-11e7-b99d-89998a7f39dd Duration: 52.23 ms Billed Duration: 100 ms Memory Size: 128 MB Max Memory Used: 19 MB
/aws/lambda/logging_secret START RequestId: d74ea688-3834-11e7-aa4e-d592c1fbc35f Version: 1
/aws/lambda/logging_secret INFO[0012] secret guessed secret=klaatu barada nikto
/aws/lambda/logging_secret END RequestId: d74ea688-3834-11e7-aa4e-d592c1fbc35f
/aws/lambda/logging_secret REPORT RequestId: d74ea688-3834-11e7-aa4e-d592c1fbc35f Duration: 7.43 ms Billed Duration: 100 ms
Memory Size: 128 MB Max Memory Used: 19 MB
- 检查您的指标:
$ apex metrics secret
secret
total cost: $0.00
invocations: 0 ($0.00)
duration: 0s ($0.00)
throttles: 0
errors: 0
memory: 128
- 清理已部署的服务:
$ apex delete
Are you sure? (yes/no) yes
• deleting env= function=secret
• function deleted env= function=secret
它是如何工作的...
在这个配方中,我们创建了一个名为 secret 的新 Lambda 函数,它将根据您是否猜对了秘密短语来做出响应。该函数解析传入的 JSON 请求,使用Stderr进行一些日志记录,并返回一个响应。
使用函数几次后,我们可以看到我们的日志可以使用apex logs命令查看。此命令可以在单个 Lambda 函数或所有受管理的函数上运行。如果您正在链接 Apex 命令并希望观看许多服务的日志,这将非常有用。
此外,我们还向您展示了如何使用apex metrics命令收集有关应用程序的一般指标,包括成本和调用。您还可以在 Lambda 部分的 AWS 控制台中直接查看大量此信息。与其他配方一样,我们在最后尽力清理。
使用 Go 的 Google App Engine
App Engine 是谷歌的一个服务,可以快速部署 Web 应用程序。这些应用程序可以访问云存储和各种其他谷歌 API。总体思路是 App Engine 将根据负载轻松扩展,并简化与托管应用相关的任何操作管理。这个配方将展示如何创建并可选部署一个基本的 App Engine 应用程序。这个配方不会深入讨论设置谷歌云帐户、设置计费或清理实例的具体细节。作为最低要求,此配方需要访问 Google Cloud Datastore (cloud.google.com/datastore/docs/concepts/overview)。
准备工作
根据这些步骤配置您的环境:
-
从
golang.org/doc/install下载并安装 Go 1.11.1 或更高版本到您的操作系统。 -
从
cloud.google.com/appengine/docs/flexible/go/quickstart下载 Google Cloud SDK。 -
创建一个允许您执行数据存储访问并记录应用程序名称的应用程序。对于这个配方,我们将使用
go-cookbook。 -
安装
gcloud components install app-engine-goGo app engine 组件。 -
打开终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/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
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/appengine的新目录,并导航到该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/appengine
您应该看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/appengine
- 创建一个名为
app.yml的文件,其中包含以下内容,将go-cookbook替换为您在准备就绪部分创建的应用程序名称:
runtime: go112
manual_scaling:
instances: 1
#[START env_variables]
env_variables:
GCLOUD_DATASET_ID: go-cookbook
#[END env_variables]
- 创建一个名为
message.go的文件,其中包含以下内容:
package main
import (
"context"
"time"
"cloud.google.com/go/datastore"
)
// Message is the object we store
type Message struct {
Timestamp time.Time
Message string
}
func (c *Controller) storeMessage(ctx context.Context, message
string) error {
m := &Message{
Timestamp: time.Now(),
Message: message,
}
k := datastore.IncompleteKey("Message", nil)
_, err := c.store.Put(ctx, k, m)
return err
}
func (c *Controller) queryMessages(ctx context.Context, limit
int) ([]*Message, error) {
q := datastore.NewQuery("Message").
Order("-Timestamp").
Limit(limit)
messages := make([]*Message, 0)
_, err := c.store.GetAll(ctx, q, &messages)
return messages, err
}
- 创建一个名为
controller.go的文件,其中包含以下内容:
package main
import (
"context"
"fmt"
"log"
"net/http"
"cloud.google.com/go/datastore"
)
// Controller holds our storage and other
// state
type Controller struct {
store *datastore.Client
}
func (c *Controller) handle(w http.ResponseWriter, r
*http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "invalid method",
http.StatusMethodNotAllowed)
return
}
ctx := context.Background()
// store the new message
r.ParseForm()
if message := r.FormValue("message"); message != "" {
if err := c.storeMessage(ctx, message); err != nil {
log.Printf("could not store message: %v", err)
http.Error(w, "could not store
message",
http.StatusInternalServerError)
return
}
}
// get the current messages and display them
fmt.Fprintln(w, "Messages:")
messages, err := c.queryMessages(ctx, 10)
if err != nil {
log.Printf("could not get messages: %v", err)
http.Error(w, "could not get messages",
http.StatusInternalServerError)
return
}
for _, message := range messages {
fmt.Fprintln(w, message.Message)
}
}
- 创建一个名为
main.go的文件,其中包含以下内容:
package main
import (
"log"
"net/http"
"os"
"cloud.google.com/go/datastore"
"golang.org/x/net/context"
"google.golang.org/appengine"
)
func main() {
ctx := context.Background()
log.SetOutput(os.Stderr)
// Set this in app.yaml when running in production.
projectID := os.Getenv("GCLOUD_DATASET_ID")
datastoreClient, err := datastore.NewClient(ctx, projectID)
if err != nil {
log.Fatal(err)
}
c := Controller{datastoreClient}
http.HandleFunc("/", c.handle)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
log.Printf("Defaulting to port %s", port)
}
log.Printf("Listening on port %s", port)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
-
运行
gcloud config set project go-cookbook命令,其中go-cookbook是您在准备就绪部分创建的项目。 -
运行
gcloud auth application-default login命令,并按照说明操作。 -
运行
export PORT=8080命令。 -
运行
export GCLOUD_DATASET_ID=go-cookbook命令,其中go-cookbook是您在准备就绪部分创建的项目。 -
运行
go build命令。 -
运行
./appengine命令。 -
尝试几条消息(
?message=other)。 -
可选择使用
gcloud app deploy将应用程序部署到您的实例。 -
使用
gcloud app browse导航到部署的应用程序。 -
可选择清理您的
appengine实例和数据存储在以下 URL:
-
go.mod文件可能会更新,go.sum文件现在应该存在于顶级配方目录中。 -
如果您复制或编写了自己的测试,请运行
go test命令。确保所有测试都通过。
它是如何工作的...
一旦云 SDK 配置为指向您的应用程序并已经经过身份验证,GCloud 工具允许快速部署和配置,使本地应用程序能够访问 Google 服务。
在验证和设置端口之后,我们在localhost上运行应用程序,然后可以开始使用代码。该应用程序定义了一个可以从数据存储中存储和检索的消息对象。这演示了您可能如何隔离这种代码。您还可以使用存储/数据库接口,如前几章所示。
接下来,我们设置一个处理程序,尝试将消息插入数据存储,然后检索所有消息,在浏览器中显示它们。这创建了类似基本留言簿的东西。您可能会注意到消息并不总是立即出现。如果您在没有消息参数的情况下导航或发送另一条消息,它应该在重新加载时出现。
最后,请确保在不再使用它们时清理实例。
使用 firebase.google.com/go 使用 Firebase 进行工作
Firebase 是另一个谷歌云服务,它创建了一个可扩展、易于管理的数据库,可以支持身份验证,并且特别适用于移动应用程序。在这个示例中,我们将使用最新的 Firestore 作为我们的数据库后端。Firebase 服务提供的功能远远超出了本示例涵盖的范围,但我们只会关注存储和检索数据。我们还将研究如何为您的应用程序设置身份验证,并使用我们自己的自定义客户端封装 Firebase 客户端。
准备工作
根据以下步骤配置您的环境:
-
从
golang.org/doc/install下载并安装 Go 1.11.1 或更高版本到您的操作系统。 -
在
console.firebase.google.com/创建一个 Firebase 帐户、项目和数据库。
此示例以测试模式运行,默认情况下不安全。
-
通过访问
console.firebase.google.com/project/go-cookbook/settings/serviceaccounts/adminsdk生成服务管理员令牌。在这里,go-cookbook将替换为您的项目名称。 -
将下载的令牌移动到
/tmp/service_account.json。 -
打开终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/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
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter13/firebase的新目录,并进入该目录。 -
运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/firebase
您应该看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/firebase
- 创建一个名为
client.go的文件,内容如下:
package firebase
import (
"context"
"cloud.google.com/go/firestore"
"github.com/pkg/errors"
)
// Client Interface for mocking
type Client interface {
Get(ctx context.Context, key string) (interface{}, error)
Set(ctx context.Context, key string, value interface{}) error
Close() error
}
// firestore.Client implements Close()
// we create Get and Set
type firebaseClient struct {
*firestore.Client
collection string
}
func (f *firebaseClient) Get(ctx context.Context, key string) (interface{}, error) {
data, err := f.Collection(f.collection).Doc(key).Get(ctx)
if err != nil {
return nil, errors.Wrap(err, "get failed")
}
return data.Data(), nil
}
func (f *firebaseClient) Set(ctx context.Context, key string, value interface{}) error {
set := make(map[string]interface{})
set[key] = value
_, err := f.Collection(f.collection).Doc(key).Set(ctx, set)
return errors.Wrap(err, "set failed")
}
- 创建一个名为
auth.go的文件,内容如下:
package firebase
import (
"context"
firebase "firebase.google.com/go"
"github.com/pkg/errors"
"google.golang.org/api/option"
)
// Authenticate grabs oauth scopes using a generated
// service_account.json file from
// https://console.firebase.google.com/project/go-cookbook/settings/serviceaccounts/adminsdk
func Authenticate(ctx context.Context, collection string) (Client, error) {
opt := option.WithCredentialsFile("/tmp/service_account.json")
app, err := firebase.NewApp(ctx, nil, opt)
if err != nil {
return nil, errors.Wrap(err, "error initializing app")
}
client, err := app.Firestore(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to intialize filestore")
}
return &firebaseClient{Client: client, collection: collection}, nil
}
-
创建一个名为
example的新目录并进入该目录。 -
创建一个名为
main.go的文件,内容如下:
package main
import (
"context"
"fmt"
"log"
"github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter13/firebase"
)
func main() {
ctx := context.Background()
c, err := firebase.Authenticate(ctx, "collection")
if err != nil {
log.Fatalf("error initializing client: %v", err)
}
defer c.Close()
if err := c.Set(ctx, "key", []string{"val1", "val2"}); err != nil {
log.Fatalf(err.Error())
}
res, err := c.Get(ctx, "key")
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println(res)
if err := c.Set(ctx, "key2", []string{"val3", "val4"}); err != nil {
log.Fatalf(err.Error())
}
res, err = c.Get(ctx, "key2")
if err != nil {
log.Fatalf(err.Error())
}
fmt.Println(res)
}
-
运行
go run main.go。 -
您也可以运行
go build ./example。您应该会看到以下输出:
$ go run main.go
[val1 val2]
[val3 val4]
-
go.mod文件可能已更新,顶级示例目录中现在应该存在go.sum文件。 -
如果您复制或编写了自己的测试,返回上一级目录并运行
go test。确保所有测试都通过。
它是如何工作的...
Firebase 提供了方便的功能,让您可以使用凭据文件登录。登录后,我们可以存储任何类型的结构化、类似地图的对象。在这种情况下,我们存储map[string]interface{}。这些数据可以被多个客户端访问,包括 Web 和移动设备。
客户端代码将所有操作封装在一个接口中,以便进行测试。这是编写客户端代码时常见的模式,也用于其他示例中。在我们的情况下,我们创建了一个Get和Set函数,用于按键存储和检索值。我们还公开了Close(),以便使用客户端的代码可以延迟close()并在最后清理我们的连接。
第十四章:性能改进、技巧和诀窍
在本章中,我们将专注于优化应用程序和发现瓶颈。这些都是一些可立即被现有应用程序使用的技巧。如果您或您的组织需要完全可重现的构建,许多这些食谱是必需的。当您想要对应用程序的性能进行基准测试时,它们也是有用的。最后一个食谱侧重于提高 HTTP 的速度;然而,重要的是要记住网络世界变化迅速,重要的是要及时了解最佳实践。例如,如果您需要 HTTP/2,自 Go 1.6 版本以来,可以使用内置的 Go net/http包。
在这一章中,我们将涵盖以下食谱:
-
使用 pprof 工具
-
基准测试和发现瓶颈
-
内存分配和堆管理
-
使用 fasthttprouter 和 fasthttp
技术要求
为了继续本章中的所有食谱,请根据以下步骤配置您的环境:
-
在您的操作系统上从
golang.org/doc/install下载并安装 Go 1.12.6 或更高版本。 -
打开一个终端或控制台应用程序,并创建并导航到一个项目目录,例如
~/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
- 可选地,从
www.graphviz.org/Home.php安装 Graphviz。
使用 pprof 工具
pprof工具允许 Go 应用程序收集和导出运行时分析数据。它还提供了用于从 Web 界面访问工具的 Webhook。本食谱将创建一个基本应用程序,验证bcrypt哈希密码与明文密码,然后对应用程序进行分析。
您可能希望在第十一章 分布式系统中涵盖pprof工具,以及其他指标和监控食谱。但它实际上被放在了本章,因为它将用于分析和改进程序,就像基准测试可以使用一样。因此,本食谱将主要关注于使用pprof来分析和改进应用程序的内存使用情况。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/pprof的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/pprof
您应该看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/pprof
-
从
~/projects/go-programming-cookbook-original/chapter14/pprof复制测试,或者使用这个作为练习来编写一些您自己的代码! -
创建一个名为
crypto的目录并导航到该目录。 -
创建一个名为
handler.go的文件,内容如下:
package crypto
import (
"net/http"
"golang.org/x/crypto/bcrypt"
)
// GuessHandler checks if ?message=password
func GuessHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil{
// if we can't parse the form
// we'll assume it is malformed
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("error reading guess"))
return
}
msg := r.FormValue("message")
// "password"
real :=
[]byte("$2a$10$2ovnPWuIjMx2S0HvCxP/mutzdsGhyt8rq/
JqnJg/6OyC3B0APMGlK")
if err := bcrypt.CompareHashAndPassword(real, []byte(msg));
err != nil {
w.WriteHeader(http.StatusBadRequest)
w.Write([]byte("try again"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("you got it"))
return
}
-
导航到上一级目录。
-
创建一个名为
example的新目录并导航到该目录。 -
创建一个
main.go文件,内容如下:
package main
import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"github.com/PacktPublishing/
Go-Programming-Cookbook-Second-Edition/
chapter14/pprof/crypto"
)
func main() {
http.HandleFunc("/guess", crypto.GuessHandler)
fmt.Println("server started at localhost:8080")
log.Panic(http.ListenAndServe("localhost:8080", nil))
}
-
运行
go run main.go。 -
您还可以运行以下命令:
$ go build $ ./example
现在您应该看到以下输出:
$ go run main.go
server started at localhost:8080
- 在一个单独的终端中,运行以下命令:
$ go tool pprof http://localhost:8080/debug/pprof/profile
-
这将启动一个 30 秒的计时器。
-
在
pprof运行时运行几个curl命令:
$ curl "http://localhost:8080/guess?message=test"
try again
$curl "http://localhost:8080/guess?message=password"
you got it
.
.
.
.
$curl "http://localhost:8080/guess?message=password"
you got it
-
返回到
pprof命令并等待其完成。 -
从
pprof提示符中运行top10命令:
(pprof) top 10
930ms of 930ms total ( 100%)
Showing top 10 nodes out of 15 (cum >= 930ms)
flat flat% sum% cum cum%
870ms 93.55% 93.55% 870ms 93.55%
golang.org/x/crypto/blowfish.encryptBlock
30ms 3.23% 96.77% 900ms 96.77%
golang.org/x/crypto/blowfish.ExpandKey
30ms 3.23% 100% 30ms 3.23% runtime.memclrNoHeapPointers
0 0% 100% 930ms 100% github.com/agtorre/go-
cookbook/chapter13/pprof/crypto.GuessHandler
0 0% 100% 930ms 100%
golang.org/x/crypto/bcrypt.CompareHashAndPassword
0 0% 100% 30ms 3.23% golang.org/x/crypto/bcrypt.base64Encode
0 0% 100% 930ms 100% golang.org/x/crypto/bcrypt.bcrypt
0 0% 100% 900ms 96.77%
golang.org/x/crypto/bcrypt.expensiveBlowfishSetup
0 0% 100% 930ms 100% net/http.(*ServeMux).ServeHTTP
0 0% 100% 930ms 100% net/http.(*conn).serve
- 如果您安装了 Graphviz 或支持的浏览器,请从
pprof提示符中运行web命令。您应该会看到类似这样的东西,右侧有一长串红色框:
-
go.mod文件可能会更新,go.sum文件现在应该存在于顶级食谱目录中。 -
如果您已经复制或编写了自己的测试,请返回到上一级目录并运行
go test。确保所有测试都通过。
它是如何工作的...
pprof工具提供了关于应用程序的许多运行时信息。使用net/pprof包通常是最简单的配置方式,只需要在端口上进行监听并导入即可。
在我们的案例中,我们编写了一个处理程序,使用了一个非常计算密集的应用程序(bcrypt),以便演示在使用pprof进行分析时它们是如何出现的。这将快速地分离出在应用程序中创建瓶颈的代码块。
我们选择收集一个通用概要,导致pprof在 30 秒内轮询我们的应用程序端点。然后我们对端点生成流量,以帮助产生结果。当您尝试检查单个处理程序或代码分支时,这可能会有所帮助。
最后,我们查看了在 CPU 利用率方面排名前 10 的函数。还可以使用pprof http://localhost:8080/debug/pprof/heap命令查看内存/堆管理。pprof控制台中的web命令可用于查看 CPU/内存概要的可视化,并有助于突出更活跃的代码。
基准测试和查找瓶颈
使用基准测试来确定代码中的慢部分是另一种方法。基准测试可用于测试函数的平均性能,并且还可以并行运行基准测试。这在比较函数或对特定代码进行微优化时非常有用,特别是要查看在并发使用时函数实现的性能如何。在本示例中,我们将创建两个结构,两者都实现了原子计数器。第一个将使用sync包,另一个将使用sync/atomic。然后我们将对这两种解决方案进行基准测试。
如何操作...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/bench的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/bench
您应该会看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/bench
- 从
~/projects/go-programming-cookbook-original/chapter14/bench复制测试,或者将其作为练习编写一些自己的代码!
请注意,复制的测试还包括本示例中稍后编写的基准测试。
- 创建一个名为
lock.go的文件,内容如下:
package bench
import "sync"
// Counter uses a sync.RWMutex to safely
// modify a value
type Counter struct {
value int64
mu *sync.RWMutex
}
// Add increments the counter
func (c *Counter) Add(amount int64) {
c.mu.Lock()
c.value += amount
c.mu.Unlock()
}
// Read returns the current counter amount
func (c *Counter) Read() int64 {
c.mu.RLock()
defer c.mu.RUnlock()
return c.value
}
- 创建一个名为
atomic.go的文件,内容如下:
package bench
import "sync/atomic"
// AtomicCounter implements an atmoic lock
// using the atomic package
type AtomicCounter struct {
value int64
}
// Add increments the counter
func (c *AtomicCounter) Add(amount int64) {
atomic.AddInt64(&c.value, amount)
}
// Read returns the current counter amount
func (c *AtomicCounter) Read() int64 {
var result int64
result = atomic.LoadInt64(&c.value)
return result
}
- 创建一个名为
lock_test.go的文件,内容如下:
package bench
import "testing"
func BenchmarkCounterAdd(b *testing.B) {
c := Counter{0, &sync.RWMutex{}}
for n := 0; n < b.N; n++ {
c.Add(1)
}
}
func BenchmarkCounterRead(b *testing.B) {
c := Counter{0, &sync.RWMutex{}}
for n := 0; n < b.N; n++ {
c.Read()
}
}
func BenchmarkCounterAddRead(b *testing.B) {
c := Counter{0, &sync.RWMutex{}}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Add(1)
c.Read()
}
})
}
- 创建一个名为
atomic_test.go的文件,内容如下:
package bench
import "testing"
func BenchmarkAtomicCounterAdd(b *testing.B) {
c := AtomicCounter{0}
for n := 0; n < b.N; n++ {
c.Add(1)
}
}
func BenchmarkAtomicCounterRead(b *testing.B) {
c := AtomicCounter{0}
for n := 0; n < b.N; n++ {
c.Read()
}
}
func BenchmarkAtomicCounterAddRead(b *testing.B) {
c := AtomicCounter{0}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.Add(1)
c.Read()
}
})
}
- 运行
go test -bench .命令,您将看到以下输出:
$ go test -bench .
BenchmarkAtomicCounterAdd-4 200000000 8.38 ns/op
BenchmarkAtomicCounterRead-4 1000000000 2.09 ns/op
BenchmarkAtomicCounterAddRead-4 50000000 24.5 ns/op
BenchmarkCounterAdd-4 50000000 34.8 ns/op
BenchmarkCounterRead-4 20000000 66.0 ns/op
BenchmarkCounterAddRead-4 10000000 146 ns/op
PASS
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter14/bench 10.919s
- 如果您已经复制或编写了自己的测试,请返回上一级目录并运行
go test。确保所有测试都通过。
工作原理...
本示例是比较代码的关键路径的一个示例。例如,有时您的应用程序必须经常执行某些功能,也许是每次调用。在这种情况下,我们编写了一个原子计数器,可以从多个 go 例程中添加或读取值。
第一个解决方案使用RWMutex和Lock或RLock对象进行写入和读取。第二个使用atomic包,它提供了相同的功能。我们使函数的签名相同,以便可以在稍作修改的情况下重用基准测试,并且两者都可以满足相同的atomic整数接口。
最后,我们为添加值和读取值编写了标准基准测试。然后,我们编写了一个并行基准测试,调用添加和读取函数。并行基准测试将创建大量的锁争用,因此我们预计会出现减速。也许出乎意料的是,atomic包明显优于RWMutex。
内存分配和堆管理
一些应用程序可以从优化中受益很多。例如,考虑路由器,我们将在以后的示例中进行讨论。幸运的是,工具基准测试套件提供了收集许多内存分配以及内存分配大小的标志。调整某些关键代码路径以最小化这两个属性可能会有所帮助。
这个教程将展示编写一个将字符串用空格粘合在一起的函数的两种方法,类似于strings.Join("a", "b", "c")。一种方法将使用连接,而另一种方法将使用strings包。然后我们将比较这两种方法之间的性能和内存分配。
如何做...
这些步骤涵盖了编写和运行您的应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/tuning的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/tuning
应该看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/tuning
- 从
~/projects/go-programming-cookbook-original/chapter14/tuning复制测试,或者将其作为练习编写一些您自己的代码!
请注意,复制的测试还包括稍后在本教程中编写的基准测试。
- 创建一个名为
concat.go的文件,其中包含以下内容:
package tuning
func concat(vals ...string) string {
finalVal := ""
for i := 0; i < len(vals); i++ {
finalVal += vals[i]
if i != len(vals)-1 {
finalVal += " "
}
}
return finalVal
}
- 创建一个名为
join.go的文件,其中包含以下内容:
package tuning
import "strings"
func join(vals ...string) string {
c := strings.Join(vals, " ")
return c
}
- 创建一个名为
concat_test.go的文件,其中包含以下内容:
package tuning
import "testing"
func Benchmark_concat(b *testing.B) {
b.Run("one", func(b *testing.B) {
one := []string{"1"}
for i := 0; i < b.N; i++ {
concat(one...)
}
})
b.Run("five", func(b *testing.B) {
five := []string{"1", "2", "3", "4", "5"}
for i := 0; i < b.N; i++ {
concat(five...)
}
})
b.Run("ten", func(b *testing.B) {
ten := []string{"1", "2", "3", "4", "5",
"6", "7", "8", "9", "10"}
for i := 0; i < b.N; i++ {
concat(ten...)
}
})
}
- 创建一个名为
join_test.go的文件,其中包含以下内容:
package tuning
import "testing"
func Benchmark_join(b *testing.B) {
b.Run("one", func(b *testing.B) {
one := []string{"1"}
for i := 0; i < b.N; i++ {
join(one...)
}
})
b.Run("five", func(b *testing.B) {
five := []string{"1", "2", "3", "4", "5"}
for i := 0; i < b.N; i++ {
join(five...)
}
})
b.Run("ten", func(b *testing.B) {
ten := []string{"1", "2", "3", "4", "5",
"6", "7", "8", "9", "10"}
for i := 0; i < b.N; i++ {
join(ten...)
}
})
}
- 运行
GOMAXPROCS=1 go test -bench=. -benchmem -benchtime=1s命令,您将看到以下输出:
$ GOMAXPROCS=1 go test -bench=. -benchmem -benchtime=1s
Benchmark_concat/one 100000000 13.6 ns/op 0 B/op 0 allocs/op
Benchmark_concat/five 5000000 386 ns/op 48 B/op 8 allocs/op
Benchmark_concat/ten 2000000 992 ns/op 256 B/op 18 allocs/op
Benchmark_join/one 200000000 6.30 ns/op 0 B/op 0 allocs/op
Benchmark_join/five 10000000 124 ns/op 32 B/op 2 allocs/op
Benchmark_join/ten 10000000 183 ns/op 64 B/op 2 allocs/op
PASS
ok github.com/PacktPublishing/Go-Programming-Cookbook-Second-
Edition/chapter14/tuning 12.003s
- 如果您已经复制或编写了自己的测试,请运行
go test。确保所有测试都通过。
工作原理...
基准测试有助于调整应用程序并进行某些微优化,例如内存分配。在对带有输入的应用程序进行分配基准测试时,重要的是要尝试各种输入大小,以确定它是否会影响分配。我们编写了两个函数,concat和join。两者都将variadic字符串参数与空格连接在一起,因此参数(a,b,c)将返回字符串a b c。
concat方法仅通过字符串连接实现这一点。我们创建一个字符串,并在列表中和for循环中添加字符串和空格。我们在最后一个循环中省略添加空格。join函数使用内部的Strings.Join函数来更有效地完成这个任务。与您自己的函数相比,对标准库进行基准测试有助于更好地理解性能、简单性和功能性之间的权衡。
我们使用子基准测试来测试所有参数,这也与表驱动基准测试非常搭配。我们可以看到concat方法在单个长度输入的情况下比join方法产生了更多的分配。一个很好的练习是尝试使用可变长度的输入字符串以及一些参数来进行测试。
使用 fasthttprouter 和 fasthttp
尽管 Go 标准库提供了运行 HTTP 服务器所需的一切,但有时您需要进一步优化诸如路由和请求时间等内容。本教程将探讨一个加速请求处理的库,称为fasthttp(github.com/valyala/fasthttp),以及一个显著加速路由性能的路由器,称为fasthttprouter(github.com/buaazp/fasthttprouter)。尽管fasthttp很快,但重要的是要注意它不支持 HTTP/2(github.com/valyala/fasthttp/issues/45)。
如何做...
这些步骤涵盖了编写和运行应用程序:
-
从您的终端或控制台应用程序中,创建一个名为
~/projects/go-programming-cookbook/chapter14/fastweb的新目录,并导航到该目录。 -
运行此命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/fastweb
应该看到一个名为go.mod的文件,其中包含以下内容:
module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter14/fastweb
-
从
~/projects/go-programming-cookbook-original/chapter14/fastweb复制测试,或者将其作为练习编写一些您自己的代码! -
创建一个名为
items.go的文件,其中包含以下内容:
package main
import (
"sync"
)
var items []string
var mu *sync.RWMutex
func init() {
mu = &sync.RWMutex{}
}
// AddItem adds an item to our list
// in a thread-safe way
func AddItem(item string) {
mu.Lock()
items = append(items, item)
mu.Unlock()
}
// ReadItems returns our list of items
// in a thread-safe way
func ReadItems() []string {
mu.RLock()
defer mu.RUnlock()
return items
}
- 创建一个名为
handlers.go的文件,其中包含以下内容:
package main
import (
"encoding/json"
"github.com/valyala/fasthttp"
)
// GetItems will return our items object
func GetItems(ctx *fasthttp.RequestCtx) {
enc := json.NewEncoder(ctx)
items := ReadItems()
enc.Encode(&items)
ctx.SetStatusCode(fasthttp.StatusOK)
}
// AddItems modifies our array
func AddItems(ctx *fasthttp.RequestCtx) {
item, ok := ctx.UserValue("item").(string)
if !ok {
ctx.SetStatusCode(fasthttp.StatusBadRequest)
return
}
AddItem(item)
ctx.SetStatusCode(fasthttp.StatusOK)
}
- 创建一个名为
main.go的文件,其中包含以下内容:
package main
import (
"fmt"
"log"
"github.com/buaazp/fasthttprouter"
"github.com/valyala/fasthttp"
)
func main() {
router := fasthttprouter.New()
router.GET("/item", GetItems)
router.POST("/item/:item", AddItems)
fmt.Println("server starting on localhost:8080")
log.Fatal(fasthttp.ListenAndServe("localhost:8080",
router.Handler))
}
-
运行
go build命令。 -
运行
./fastweb命令:
$ ./fastweb
server starting on localhost:8080
- 从单独的终端,使用一些
curl命令进行测试:
$ curl "http://localhost:8080/item/hi" -X POST
$ curl "http://localhost:8080/item/how" -X POST
$ curl "http://localhost:8080/item/are" -X POST
$ curl "http://localhost:8080/item/you" -X POST
$ curl "http://localhost:8080/item" -X GET
["hi","how", "are", "you"]
-
go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。 -
如果您已经复制或编写了自己的测试,请运行
go test。确保所有测试都通过。
它是如何工作的...
fasthttp和fasthttprouter包可以大大加快 Web 请求的生命周期。这两个包在热代码路径上进行了大量优化,但不幸的是,需要重写处理程序以使用新的上下文对象,而不是传统的请求和响应写入器。
有许多框架采用了类似的路由方法,有些直接集成了fasthttp。这些项目在它们的README文件中保持最新的信息。
我们的配方实现了一个简单的list对象,我们可以通过一个端点进行附加,然后由另一个端点返回。这个配方的主要目的是演示如何处理参数,设置一个现在明确定义支持的方法的路由器,而不是通用的Handle和HandleFunc,并展示它们与标准处理程序有多么相似,但又有许多其他好处。