第十三章 性能改进

最后更新于:2022-04-02 06:49:12

';

第十二章 无服务器编程

最后更新于:2022-04-02 06:49:10

';

第十一章 响应式编程和数据流

最后更新于:2022-04-02 06:49:08

';

第十章 分布式系统

最后更新于:2022-04-02 06:49:06

';

第九章 并发和并行

最后更新于:2022-04-02 06:49:03

';

行为驱动测试

最后更新于:2022-04-02 06:49:01

## 行为驱动测试 ### 实践 1. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` ### 说明 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

模糊测试

最后更新于:2022-04-02 06:48:59

## 模糊测试 ### 实践 1. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` 2. 建立 mock.go: ``` ``` ### 说明 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

使用第三方测试工具

最后更新于:2022-04-02 06:48:57

## 使用第三方测试工具 Go测试有许多有用的工具。 这些工具可以让你更容易地了解每个功能级别的代码覆盖率,还可以使用断言工具来减少测试代码行。 本文将介绍 github.com/axw/gocov 和github.com/smartystreets/goconvey 软件包,以展示其中的一些功能。此外 github.com/smartystreets/goconvey 包支持断言和运行时测试。 ### 实践 1.获取第三方库: ``` go get github.com/axw/gocov go get github.com/smartystreets/goconvey/ ``` 2. 建立 funcs.go: ``` package tools import ( "fmt" ) func example() error { fmt.Println("in example") return nil } var example2 = func() int { fmt.Println("in example2") return 10 } ``` 3. 建立 structs.go: ``` package tools import ( "errors" "fmt" ) type c struct { Branch bool } func (c *c) example3() error { fmt.Println("in example3") if c.Branch { fmt.Println("branching code!") return errors.New("bad branch") } return nil } ``` 4. 建立 funcs_test.go: ``` package tools import ( "testing" . "github.com/smartystreets/goconvey/convey" ) func Test_example(t *testing.T) { tests := []struct { name string }{ {"base-case"}, } for _, tt := range tests { Convey(tt.name, t, func() { res := example() So(res, ShouldBeNil) }) } } func Test_example2(t *testing.T) { tests := []struct { name string }{ {"base-case"}, } for _, tt := range tests { Convey(tt.name, t, func() { res := example2() So(res, ShouldBeGreaterThanOrEqualTo, 1) }) } } ``` 5. 建立 structs_test.go: ``` package tools import ( "testing" . "github.com/smartystreets/goconvey/convey" ) func Test_c_example3(t *testing.T) { type fields struct { Branch bool } tests := []struct { name string fields fields wantErr bool }{ {"no branch", fields{false}, false}, {"branch", fields{true}, true}, } for _, tt := range tests { Convey(tt.name, t, func() { c := &c{ Branch: tt.fields.Branch, } So((c.example3() != nil), ShouldEqual, tt.wantErr) }) } } ``` 6. 运行: ``` $ gocov test | gocov report ok github.com/agtorre/go-cookbook/chapter8/tools 0.006s coverage: 100.0% of statements github.com/agtorre/go-cookbook/chapter8/tools/struct.go c.example3 100.00% (5/5) github.com/agtorre/go-cookbook/chapter8/tools/funcs.go example 100.00% (2/2) github.com/agtorre/go-cookbook/chapter8/tools/funcs.go @12:16 100.00% (2/2) github.com/agtorre/go-cookbook/chapter8/tools ---------- 100.00% (9/9) Total Coverage: 100.00% (9/9) ``` 7. 执行goconvey命令,会在浏览器中显示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/21b9c114362d1c98846b13801bd9e9e7_712x367.jpg) ### 说明 本节演示了如何使用goconvry。与前面的小节相比,Convey关键字基本替代了t.Run,并且可以生成在goconvey生成的UI中显示的标签,但与t,Run的表现有所不同。如果你有嵌套的这样测试块: ``` Convey("Outer loop", t, func(){ a := 1 Convey("Inner loop", t, func() { a = 2 }) Convey ("Inner loop2", t, func(){ fmt.Println(a) }) }) ``` 使用goconvey命令,将打印1。如果我们使用t.Run,将打印2。换句话说,t.Run按顺序运行测试,永远不会重复。此行为对于将变量设置在外部代码块中非常有用。如果混合使用,就必须记住这种区别。 在使用convey断言时,在UI中有成功的复选标记和其他统计信息。使用它还可以减少if检查单行的大小,甚至可以创建自定义断言。 如果保留goconvey Web界面并打开通知,则在保存代码时,将自动运行测试,并且将收到有关任何增加或减少覆盖范围以及构建失败时的通知。 以上功能都可以单独或一起使用。 在努力提高测试覆盖率时,gocov工具非常有用。它可以快速识别尚未测试的函数,并帮助了解覆盖率报告。此外,gocov可用于生成使用 github.com/matm/gocov-html 包随Go代码一起提供的备用HTML报告。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

使用表驱动测试

最后更新于:2022-04-02 06:48:54

## 使用表驱动测试 本节将演示如何编写表驱动测试,收集测试覆盖率并对其进行改进。还将使用 github.com/cweill/gotests 包来生成测试。 如果你一直在下载其他章节的测试代码,这些代码应该看起来非常熟悉。使用本节与前几节的测试组合,应该能够实现100%的测试覆盖率。 ### 实践 1. 获取第三方库: ``` go get github.com/cweill/gotests/ ``` 2. 建立 coverage.go: ``` package main import "errors" // Coverage 是一个具有一些分支条件的简单函数 func Coverage(condition bool) error { if condition { return errors.New("condition was set") } return nil } ``` 3. 建立 coverage_test.go: 运行 ``` gotests -all -w ``` 这会生成: ``` package main import "testing" func TestCoverage(t *testing.T) { type args struct { condition bool } tests := []struct { name string args args wantErr bool }{ //TODO } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := Coverage(tt.args.condition); (err != nil) != tt.wantErr { t.Errorf("Coverage() error = %v, wantErr %v", err, tt.wantErr) } }) } } ``` 4. 填充TODO部分: ``` {"no condition", args{true}, true}, ``` 5. 运行测试: ``` go test -cover PASS coverage: 66.7% of statements ok github.com/agtorre/go-cookbook/chapter8/coverage 0.007s ``` ``` go test -coverprofile=cover.out go tool cover -html=cover.out -o coverage.html ``` 打开coverage.html可以看到覆盖率报告。 ### 说明 go test -cover命令附带一个基本的Go安装。它可用于收集Go应用程序的测试覆盖率报告。此外,它还能够输出覆盖率指标和HTML覆盖率报告。此工具通常由其他工具包装,将在下一节中介绍。 https//github.com/golang/go/wiki/TableDrivenTests 涵盖了这些表驱动的测试样例,可以在不编写大量额外代码的情况下完成可以处理许多情况的干净测试。 首先自动生成测试代码,然后根据需要填写测试用例以帮助创建更多的覆盖范围。在调用非变量函数或方法时,或测试输入和输出的许多变化,可能很难达到100%的测试覆盖率,在这样的情况下,模糊测试会变得很有用。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

使用Mockgen包

最后更新于:2022-04-02 06:48:52

## 使用Mockgen包 前面的小节我们使用了自行模拟的方式。当你需要面对很多的接口时,这么干会变得极为麻烦且极易发生错误。这是自动化测试的意义所在。本节我们使用 github.com/golang/mock/gomock ,该库提供了一组模拟对象,可以与接口测试结合使用。 ### 实践 1.获取第三方库: ``` go get github.com/golang/mock/ ``` 2. 建立 interface.go: ``` package mockgen type GetSetter interface { Set(key, val string) error Get(key string) (string, error) } ``` 3. 运行命令行建立 mocks.go: ``` mockgen -destination internal/mocks.go -package internal github.com/agtorre/go-cookbook/chapter8/mockgen GetSetter ``` ``` // Automatically generated by MockGen. DO NOT EDIT! // Source: github.com/agtorre/go-cookbook/chapter8/mockgen (interfaces: GetSetter) package internal import ( gomock "github.com/golang/mock/gomock" ) // Mock of GetSetter interface type MockGetSetter struct { ctrl *gomock.Controller recorder *_MockGetSetterRecorder } // Recorder for MockGetSetter (not exported) type _MockGetSetterRecorder struct { mock *MockGetSetter } func NewMockGetSetter(ctrl *gomock.Controller) *MockGetSetter { mock := &MockGetSetter{ctrl: ctrl} mock.recorder = &_MockGetSetterRecorder{mock} return mock } func (_m *MockGetSetter) EXPECT() *_MockGetSetterRecorder { return _m.recorder } func (_m *MockGetSetter) Get(_param0 string) (string, error) { ret := _m.ctrl.Call(_m, "Get", _param0) ret0, _ := ret[0].(string) ret1, _ := ret[1].(error) return ret0, ret1 } func (_mr *_MockGetSetterRecorder) Get(arg0 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Get", arg0) } func (_m *MockGetSetter) Set(_param0 string, _param1 string) error { ret := _m.ctrl.Call(_m, "Set", _param0, _param1) ret0, _ := ret[0].(error) return ret0 } func (_mr *_MockGetSetterRecorder) Set(arg0, arg1 interface{}) *gomock.Call { return _mr.mock.ctrl.RecordCall(_mr.mock, "Set", arg0, arg1) } ``` 4. 建立 exec.go: ``` package mockgen // Controller 这个结构体演示了一种初始化接口的方式 type Controller struct { GetSetter } // GetThenSet 检查值是否已设置。如果没有设置就将其设置 func (c *Controller) GetThenSet(key, value string) error { val, err := c.Get(key) if err != nil { return err } if val != value { return c.Set(key, value) } return nil } ``` 5. 建立 interface_test.go: ``` package mockgen import ( "errors" "testing" "github.com/agtorre/go-cookbook/chapter8/mockgen/internal" "github.com/golang/mock/gomock" ) func TestExample(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockGetSetter := internal.NewMockGetSetter(ctrl) var k string mockGetSetter.EXPECT().Get("we can put anything here!").Do(func(key string) { k = key }).Return("", nil) customError := errors.New("failed this time") mockGetSetter.EXPECT().Get(gomock.Any()).Return("", customError) if _, err := mockGetSetter.Get("we can put anything here!"); err != nil { t.Errorf("got %#v; want %#v", err, nil) } if k != "we can put anything here!" { t.Errorf("bad key") } if _, err := mockGetSetter.Get("key"); err == nil { t.Errorf("got %#v; want %#v", err, customError) } } ``` 6. 建立 exec_test.go: ``` package mockgen import ( "errors" "testing" "github.com/agtorre/go-cookbook/chapter8/mockgen/internal" "github.com/golang/mock/gomock" ) func TestController_Set(t *testing.T) { tests := []struct { name string getReturnVal string getReturnErr error setReturnErr error wantErr bool }{ {"get error", "value", errors.New("failed"), nil, true}, {"value match", "value", nil, nil, false}, {"no errors", "not set", nil, nil, false}, {"set error", "not set", nil, errors.New("failed"), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() mockGetSetter := internal.NewMockGetSetter(ctrl) mockGetSetter.EXPECT().Get("key").AnyTimes().Return(tt.getReturnVal, tt.getReturnErr) mockGetSetter.EXPECT().Set("key", gomock.Any()).AnyTimes().Return(tt.setReturnErr) c := &Controller{ GetSetter: mockGetSetter, } if err := c.GetThenSet("key", "value"); (err != nil) != tt.wantErr { t.Errorf("Controller.Set() error = %v, wantErr %v", err, tt.wantErr) } }) } } ``` ### 说明 生成的模拟对象允许测试预定的参数,调用函数的次数以及返回的内容,并且允许我们设置其他工作流程。interface_test.go文件展示了在线调用它们时使用模拟对象的一些示例。 通常,测试看起来更像exec_test.go,我们希望拦截由实际代码执行的接口函数调用,并在测试时更改它们的行为。 exec_test.go文件还展示了如何在表驱动的测试环境中使用模拟对象。Any()函数意味着模拟函数可以被调用零次或多次,这对于代码提前终止的情况非常有用。 示例演示的最后一个技巧是将模拟对象粘贴到内部包中。当需要模拟在自己之外的包中声明的函数时,这非常有用。 这允许在非_test.go文件中定义这些方法,但不允许将它们导出到库的情况。通常,使用与当前编写的测试相同的包名称将模拟对象粘贴到_test.go文件中更容易。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

使用标准库进行模拟

最后更新于:2022-04-02 06:48:50

## 使用标准库进行模拟 在Go中,模拟通常意味着实现具有测试版本的接口,该测试版本允许从测试中控制运行时行为。它也可以指模拟函数和方法,我们将探索如何实现它。示例中使用的Patch和Restore函数可以在https://play.golang.org/p/oLF1XnRX3C 找到。 包含大量分支条件或深度嵌套逻辑的代码可能很难测试,最后测试往往更加效果很差。这是因为开发人员需要在其测试中跟踪很多模拟对象,返回值和状态。 ### 实践 1. 建立 mock.go: ``` package mocking // DoStuffer 是一个简单的接口 type DoStuffer interface { DoStuff(input string) error } ``` 2. 建立 patch.go: ``` package mocking import "reflect" // Restorer是一个可用于恢复先前状态的函数 type Restorer func() // Restore存储了之前的状态 func (r Restorer) Restore() { r() } // Patch将给定目标指向的值设置为给定值,并返回一个函数以将其恢复为原始值。 该值必须可分配给目标的元素类型。 func Patch(dest, value interface{}) Restorer { destv := reflect.ValueOf(dest).Elem() oldv := reflect.New(destv.Type()).Elem() oldv.Set(destv) valuev := reflect.ValueOf(value) if !valuev.IsValid() { // 对于目标类型不可用的情况,这种解决方式并不优雅 valuev = reflect.Zero(destv.Type()) } destv.Set(valuev) return func() { destv.Set(oldv) } } ``` 3. 建立 exec.go: ``` package mocking import "errors" var ThrowError = func() error { return errors.New("always fails") } func DoSomeStuff(d DoStuffer) error { if err := d.DoStuff("test"); err != nil { return err } if err := ThrowError(); err != nil { return err } return nil } ``` 4. 建立 mock_test.go: ``` package mocking type MockDoStuffer struct { // 使用闭包模拟 MockDoStuff func(input string) error } func (m *MockDoStuffer) DoStuff(input string) error { if m.MockDoStuff != nil { return m.MockDoStuff(input) } // 如果我们不模拟输入,就返回一个常见的情况 return nil } ``` 5. 建立 exec_test.go: ``` package mocking import ( "errors" "testing" ) func TestThrowError(t *testing.T) { tests := []struct { name string wantErr bool }{ {"base-case", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := ThrowError(); (err != nil) != tt.wantErr { t.Errorf("DoSomeStuff() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestDoSomeStuff(t *testing.T) { tests := []struct { name string DoStuff error ThrowError error wantErr bool }{ {"base-case", nil, nil, false}, {"DoStuff error", errors.New("failed"), nil, true}, {"ThrowError error", nil, errors.New("failed"), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 使用模拟结构来模拟接口 d := MockDoStuffer{} d.MockDoStuff = func(string) error { return tt.DoStuff } // 模拟声明为变量的函数对func A()不起作用,必须是var A = func() defer Patch(&ThrowError, func() error { return tt.ThrowError }).Restore() if err := DoSomeStuff(&d); (err != nil) != tt.wantErr { t.Errorf("DoSomeStuff() error = %v, wantErr %v", err, tt.wantErr) } }) } } ``` 6. 运行go test: ``` PASS ok github.com/agtorre/go-cookbook/chapter8/mocking 0.006s ``` ### 说明 无论是使用errors.New,fmt.Errorf还是自定义错误,最重要的是不应该在代码中不处理错误。这些定义错误的不同方法提供了很大的灵活性。例如,你可以在结构中添加额外的函数,以进一步检查错误并将接口转换为调用函数中的错误类型,以获得一些额外的功能。 接口本身非常简单,唯一的要求是返回一个有效的字符串。(在测试中明显将其复杂化了)这样的测试保证对某些要求严格的应用程序同样可用。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

第八章 测试

最后更新于:2022-04-02 06:48:48

本章会覆盖以下内容: * 使用标准库进行模拟 * 使用Mockgen包 * 使用表驱动测试 * 使用第三方测试工具 * 模糊测试 * 行为驱动测试 ### 介绍 本章与前面的章节有所不同,我们的目光将转向编写测试。Go语言提供了开箱即用的测试支持,使得monkey patching和模拟相对简单。 Go测试鼓励代码的结构化,特别是测试和模拟接口非常简单且对此给予了很好的支持。某些类型的代码可能更难以测试,例如,测试使用包级别全局变量的代码,未被抽象到接口的函数以及具有非导出变量或方法的结构可能很困难。本章将分享一些测试Go代码的思路。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

将GRPC导出为JSON API

最后更新于:2022-04-02 06:48:45

## 将GRPC导出为JSON API 在第六章的“理解GRPC的使用”一节中,我们实现了一个基础的GRPC服务器和客户端。本节将通过将常见的RPC函数放在一个包中并将它们包装在GRPC服务器和标准Web处理程序中来进行扩展。当你的API希望支持两种类型的客户端,但不希望复制代码以实现常见功能时,这非常有用。 ### 实践 1. 安装GRPC: https://github.com/grpc/grpc/blob/master/INSTALL.md. ``` go get github.com/golang/protobuf/proto go get github.com/golang/protobuf/protoc-gen-go ``` 2. 建立greeter.proto: ``` syntax = "proto3"; package keyvalue; service KeyValue{ rpc Set(SetKeyValueRequest) returns (KeyValueResponse){} rpc Get(GetKeyValueRequest) returns (KeyValueResponse){} } message SetKeyValueRequest { string key = 1; string value = 2; } message GetKeyValueRequest{ string key = 1; } message KeyValueResponse{ string success = 1; string value = 2; } ``` 运行 ``` protoc --go_out=plugins=grpc:. greeter.proto ``` 3. 建立 keyvalue.go: ``` package internal import ( "golang.org/x/net/context" "sync" "github.com/agtorre/go-cookbook/chapter7/grpcjson/keyvalue" "google.golang.org/grpc" "google.golang.org/grpc/codes" ) type KeyValue struct { mutex sync.RWMutex m map[string]string } // NewKeyValue 初始化了KeyValue中的map func NewKeyValue() *KeyValue { return &KeyValue{ m: make(map[string]string), } } // Set 为键设置一个值,然后返回该值 func (k *KeyValue) Set(ctx context.Context, r *keyvalue.SetKeyValueRequest) (*keyvalue.KeyValueResponse, error) { k.mutex.Lock() k.m[r.GetKey()] = r.GetValue() k.mutex.Unlock() return &keyvalue.KeyValueResponse{Value: r.GetValue()}, nil } // Get 得到一个给定键的值,或者如果它不存在报告查找失败 func (k *KeyValue) Get(ctx context.Context, r *keyvalue.GetKeyValueRequest) (*keyvalue.KeyValueResponse, error) { k.mutex.RLock() defer k.mutex.RUnlock() val, ok := k.m[r.GetKey()] if !ok { return nil, grpc.Errorf(codes.NotFound, "key not set") } return &keyvalue.KeyValueResponse{Value: val}, nil } ``` 4. 建立 grpc/main.go: ``` package main import ( "fmt" "net" "github.com/agtorre/go-cookbook/chapter7/grpcjson/internal" "github.com/agtorre/go-cookbook/chapter7/grpcjson/keyvalue" "google.golang.org/grpc" ) func main() { grpcServer := grpc.NewServer() keyvalue.RegisterKeyValueServer(grpcServer, internal.NewKeyValue()) lis, err := net.Listen("tcp", ":4444") if err != nil { panic(err) } fmt.Println("Listening on port :4444") grpcServer.Serve(lis) } ``` 5. 建立 set.go: ``` package main import ( "encoding/json" "net/http" "github.com/agtorre/go-cookbook/chapter7/grpcjson/internal" "github.com/agtorre/go-cookbook/chapter7/grpcjson/keyvalue" "github.com/apex/log" ) // Controller 保存一个内部的KeyValueObject type Controller struct { *internal.KeyValue } // SetHandler 封装了RPC的Set调用 func (c *Controller) SetHandler(w http.ResponseWriter, r *http.Request) { var kv keyvalue.SetKeyValueRequest decoder := json.NewDecoder(r.Body) if err := decoder.Decode(&kv); err != nil { log.Errorf("failed to decode: %s", err.Error()) w.WriteHeader(http.StatusBadRequest) return } gresp, err := c.Set(r.Context(), &kv) if err != nil { log.Errorf("failed to set: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } resp, err := json.Marshal(gresp) if err != nil { log.Errorf("failed to marshal: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) w.Write(resp) } ``` 6. 建立 get.go: ``` package main import ( "encoding/json" "net/http" "google.golang.org/grpc" "google.golang.org/grpc/codes" "github.com/agtorre/go-cookbook/chapter7/grpcjson/keyvalue" "github.com/apex/log" ) // GetHandler 封装了RPC的Get调用 func (c *Controller) GetHandler(w http.ResponseWriter, r *http.Request) { key := r.URL.Query().Get("key") kv := keyvalue.GetKeyValueRequest{Key: key} gresp, err := c.Get(r.Context(), &kv) if err != nil { if grpc.Code(err) == codes.NotFound { w.WriteHeader(http.StatusNotFound) return } log.Errorf("failed to get: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.WriteHeader(http.StatusOK) resp, err := json.Marshal(gresp) if err != nil { log.Errorf("failed to marshal: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.Write(resp) } ``` 7. 建立 main.go: ``` package main import ( "fmt" "net/http" "github.com/agtorre/go-cookbook/chapter7/grpcjson/internal" ) func main() { c := Controller{KeyValue: internal.NewKeyValue()} http.HandleFunc("/set", c.SetHandler) http.HandleFunc("/get", c.GetHandler) fmt.Println("Listening on port :3333") err := http.ListenAndServe(":3333", nil) panic(err) } ``` 8. 运行: ``` $ go run http/*.go Listening on port :3333 $curl "http://localhost:3333/set" -d '{"key":"test", "value":"123"}' -v {"value":"123"} $curl "http://localhost:3333/get?key=badtest" -v 'name=Reader;greeting=Goodbye' $curl "http://localhost:3333/get?key=test" -v 'name=Reader;greeting=Goodbye' {"value":"123"} ``` ### 说明 虽然示例中省略了客户端,但你可以复制第6章GRPC章节中的步骤,这样应该看到与示例中相同的结果。http和grpc使用了相同的内部包。我们必须返回适当的GRPC错误代码,并将这些错误代码映射到HTTP响应。在这种情况下,我们使用codes.NotFound,它映射到http.StatusNotFound。如果必须处理多种错误,则switch语句可能比if ... else语句更有意义。 你可能注意到GRPC签名通常非常一致。他们接受请求并返回可选响应和错误。如果你的GRPC调用重复性很强并且看起来很适合代码生成,那么可以创建一个通用的处理程序来填充它,像 github.com/goadesign/goa 这样的库就是这么干的。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

构建反向代理

最后更新于:2022-04-02 06:48:43

## 构建反向代理 本节我们将建立一个反向代理应用。我们的想法是,通过请求http:// localhost:3333,所有流量都将转发到可配置的主机,响应将转发到你的浏览器。 这可以与端口转发和ssh隧道结合使用,以便通过中间服务器安全地访问网站。本节将从头开始构建反向代理,但标准库的net/http/httputil包也提供此功能。使用此包,可以通过Director func(*http.Request)修改传入请求,并且可以通过 ModifyResponse func(*http.Response) error错误修改传出响应。 此外,还支持缓冲响应。 ### 实践 1. 建立 proxy.go: ``` package proxy import ( "log" "net/http" ) // Proxy 保存了客户端配置和需要代理的BaseURL地址 type Proxy struct { Client *http.Client BaseURL string } // ServeHTTP 表示代理部署了Handler接口它操纵请求,将其转发给BaseURL,然后返回响应 func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err := p.ProcessRequest(r); err != nil { log.Printf("error occurred during process request: %s", err.Error()) w.WriteHeader(http.StatusBadRequest) return } resp, err := p.Client.Do(r) if err != nil { log.Printf("error occurred during client operation: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } defer resp.Body.Close() CopyResponse(w, resp) } ``` 2. 建立 process.go: ``` package proxy import ( "bytes" "net/http" "net/url" ) // ProcessRequest 根据Proxy设置修改请求 func (p *Proxy) ProcessRequest(r *http.Request) error { proxyURLRaw := p.BaseURL + r.URL.String() proxyURL, err := url.Parse(proxyURLRaw) if err != nil { return err } r.URL = proxyURL r.Host = proxyURL.Host r.RequestURI = "" return nil } // CopyResponse获取客户端响应并将所有内容写入原始处理程序中的ResponseWriter func CopyResponse(w http.ResponseWriter, resp *http.Response) { var out bytes.Buffer out.ReadFrom(resp.Body) for key, values := range resp.Header { for _, value := range values { w.Header().Add(key, value) } } w.WriteHeader(resp.StatusCode) w.Write(out.Bytes()) } ``` 3. 建立 main.go: ``` package main import ( "fmt" "net/http" "github.com/agtorre/go-cookbook/chapter7/proxy" ) func main() { p := &proxy.Proxy{ Client: http.DefaultClient, BaseURL: "https://www.golang.org", } http.Handle("/", p) fmt.Println("Listening on port :3333") err := http.ListenAndServe(":3333", nil) panic(err) } ``` 4. 这会输出: ``` $ go run main.go Listening on port :3333 ``` 在浏览器地址栏输入localhost:3333/,你会看到跳转到了https://golang.org/。 ### 说明 Go请求和响应对象在很大程度上可以在客户端和处理程序之间共享。示例代码接受由满足Handler接口的Proxy结构获取的请求。一旦请求可用,它就被修改为在请求之前添加Proxy.BaseURL。最后,响应被复制回ResponseWriter接口。 我们还可以添加一些其他功能,例如请求的基本身份验证,令牌管理等。这对于代理管理JavaScript或其他客户端应用程序的会话令牌管理非常有用。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

使用中间件

最后更新于:2022-04-02 06:48:41

## 使用使用中间件 Go程序的中间件是一个被广泛探索的领域。有许多用于处理中间件的包。 本节将从头开始创建中间件,并实现一个ApplyMiddleware函数将一堆中间件链接在一起。此外还会在请求上下文对象中设置值,并使用中间件检索它们。以演示如何将中间件逻辑与处理程序分离。 ### 实践 1. 建立 middleware.go: ``` package middleware import ( "log" "net/http" "time" ) // Middleware是所有的中间件函数都会返回的 type Middleware func(http.HandlerFunc) http.HandlerFunc // ApplyMiddleware 将应用所有中间件,最后一个参数将是用于上下文传递目的的外部包装 func ApplyMiddleware(h http.HandlerFunc, middleware ...Middleware) http.HandlerFunc { applied := h for _, m := range middleware { applied = m(applied) } return applied } // Logger 记录请求日志 这会通过SetID()传递id func Logger(l *log.Logger) Middleware { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { start := time.Now() l.Printf("started request to %s with id %s", r.URL, GetID(r.Context())) next(w, r) l.Printf("completed request to %s with id %s in %s", r.URL, GetID(r.Context()), time.Since(start)) } } } ``` 2. 建立 context.go: ``` package middleware import ( "context" "net/http" "strconv" ) // ContextID 是自定义类型 用于检索context type ContextID int // ID是我们定义的唯一ID const ID ContextID = 0 // SetID 使用自增唯一id更新context func SetID(start int64) Middleware { return func(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { ctx := context.WithValue(r.Context(), ID, strconv.FormatInt(start, 10)) start++ r = r.WithContext(ctx) next(w, r) } } } // GetID 如果设置,则从上下文中获取ID,否则返回空字符串 func GetID(ctx context.Context) string { if val, ok := ctx.Value(ID).(string); ok { return val } return "" } ``` 3. 建立 handler.go: ``` package middleware import ( "net/http" ) func Handler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("success")) } ``` 4. 建立 main.go: ``` package main import ( "fmt" "log" "net/http" "os" "github.com/agtorre/go-cookbook/chapter7/middleware" ) func main() { // We apply from bottom up h := middleware.ApplyMiddleware( middleware.Handler, middleware.Logger(log.New(os.Stdout, "", 0)), middleware.SetID(100), ) http.HandleFunc("/", h) fmt.Println("Listening on port :3333") err := http.ListenAndServe(":3333", nil) panic(err) } ``` 5. 运行: ``` $ go run main.go Listening on port :3333 $curl "http://localhost:3333 success $curl "http://localhost:3333 success $curl "http://localhost:3333 success ``` 此外在运行main.go的命令行你还会看到: ``` Listening on port :3333 started request to / with id 100 completed request to / with id 100 in 52.284µs started request to / with id 101 completed request to / with id 101 in 40.273µs started request to / with id 102 ``` ### 说明 中间件可用于执行简单操作,例如日志记录,度量标准收集和分析。它还可用于在每个请求上动态填充变量。例如,可以用于从请求中收集X-Header以设置ID或生成ID,就像我们在示例中所做的那样。另一个ID策略可能是为每个请求生成一个UUID,这样我们可以轻松地将日志消息关联在一起,并在构建响应时跟踪请求。 使用上下文值时,考虑中间件的顺序很重要。通常,最好不要让中间件相互依赖。 例如,最好在日志记录中间件本身中生成UUID。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

内容渲染

最后更新于:2022-04-02 06:48:39

## 内容渲染 Web处理程序可以返回各种内容类型,例如,JSON,纯文本,图像等。 通常在与API通信时,可以指定并接收内容类型,以阐明将传入数据的格式以及要接收的数据。 本节将通过第三方库对数据格式进行切换。 ### 实践 1. 获取第三方库: ``` go get github.com/unrolled/render ``` 2. 建立 negotiate.go: ``` package negotiate import ( "net/http" "github.com/unrolled/render" ) // Negotiator 封装render并对ContentType进行一些切换 type Negotiator struct { ContentType string *render.Render } // GetNegotiator 接收http请求 并从Content-Type标头中找出ContentType func GetNegotiator(r *http.Request) *Negotiator { contentType := r.Header.Get("Content-Type") return &Negotiator{ ContentType: contentType, Render: render.New(), } } ``` 3. 建立 respond.go: ``` package negotiate import "io" import "github.com/unrolled/render" // Respond 根据 Content Type 判断应该返回什么样类型的数据 func (n *Negotiator) Respond(w io.Writer, status int, v interface{}) { switch n.ContentType { case render.ContentJSON: n.Render.JSON(w, status, v) case render.ContentXML: n.Render.XML(w, status, v) default: n.Render.JSON(w, status, v) } } ``` 4. 建立 handler.go: ``` package negotiate import ( "encoding/xml" "net/http" ) // Payload 甚至数据模板 type Payload struct { XMLName xml.Name `xml:"payload" json:"-"` Status string `xml:"status" json:"status"` } // Handler 调用GetNegotiator处理返回格式 func Handler(w http.ResponseWriter, r *http.Request) { n := GetNegotiator(r) n.Respond(w, http.StatusOK, &Payload{Status: "Successful!"}) } ``` 5. 建立 main.go: ``` package main import ( "fmt" "net/http" "github.com/agtorre/go-cookbook/chapter7/negotiate" ) func main() { http.HandleFunc("/", negotiate.Handler) fmt.Println("Listening on port :3333") err := http.ListenAndServe(":3333", nil) panic(err) } ``` 6. 运行: ``` $ go run main.go Listening on port :3333 $curl "http://localhost:3333 -H "Content-Type: text/xml" Successful! $curl "http://localhost:3333 -H "Content-Type: application/json" {"status":"Successful!"} ``` ### 说明 github.com/unrolled/render 包可以帮助你处理各种类型的请求头。请求头通常包含多个值,您的代码必须考虑到这一点。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

请求参数验证

最后更新于:2022-04-02 06:48:36

## 请求参数验证 对Web进行验证是一个难题。本节将探讨使用闭包来支持验证函数,并允许在初始化控制器结构时执行验证类型以增强灵活性。 我们将在结构上执行验证,但不会讨论如何填充结构。我们可以假设通过解析JSON,从表单输入或其他方法显式填充数据。 ### 实践 1. 建立 controller.go: ``` package validation // Controller 保存了验证方法 type Controller struct { ValidatePayload func(p *Payload) error } // New 使用我们的本地验证初始化controller 它可以被覆盖 func New() *Controller { return &Controller{ ValidatePayload: ValidatePayload, } } ``` 2. 建立 validate.go: ``` package validation import "errors" // Verror是在验证期间发生的错误,我们可以将其返回给用户 type Verror struct { error } // Payload 是我们处理的内容 type Payload struct { Name string `json:"name"` Age int `json:"age"` } // ValidatePayload是我们控制器中闭包的1个实现 func ValidatePayload(p *Payload) error { if p.Name == "" { return Verror{errors.New("name is required")} } if p.Age <= 0 || p.Age >= 120 { return Verror{errors.New("age is required and must be a value greater than 0 and less than 120")} } return nil } ``` 3. 建立 process.go: ``` package validation import ( "encoding/json" "fmt" "net/http" ) // Process是一个验证post传入的数据 func (c *Controller) Process(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } decoder := json.NewDecoder(r.Body) defer r.Body.Close() var p Payload if err := decoder.Decode(&p); err != nil { fmt.Println(err) w.WriteHeader(http.StatusBadRequest) return } if err := c.ValidatePayload(&p); err != nil { switch err.(type) { case Verror: w.WriteHeader(http.StatusBadRequest) // pass the Verror along w.Write([]byte(err.Error())) return default: w.WriteHeader(http.StatusInternalServerError) return } } } ``` 4. 建立 main.go: ``` package main import ( "fmt" "net/http" "github.com/agtorre/go-cookbook/chapter7/validation" ) func main() { c := validation.New() http.HandleFunc("/", c.Process) fmt.Println("Listening on port :3333") err := http.ListenAndServe(":3333", nil) panic(err) } ``` 5. 运行: ``` go run main.go ``` 这会输出 ``` Listening on port :3333 ``` 进行请求测试: ``` $curl "http://localhost:3333/-X POST -d '{}' name is required $curl "http://localhost:3333/-X POST -d '{"name":"test"}' age is required and must be a value greater than 0 and less than 120 $curl "http://localhost:3333/-X POST -d '{"name":"test", "age": 5}' -v ``` ### 说明 我们通过将闭包传递给控制器结构来处理验证。这种方法的优点是我们可以在运行时模拟和替换验证函数,因此测试变得更加简单。 另外,我们没有绑定到单个函数签名,因此可以将诸如数据库连接之类的东西传递给验证函数。 另外需要关注的是返回了一个名为Verror的类型错误。此类型包含用于向用户显示的验证错误消息。这种方法的一个缺点是它不能同时处理多个验证消息。这可以通过修改Verror类型以允许更多状态(例如,通过包含映射)来实现,以便在从ValidatePayload函数返回之前容纳更多验证错误。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

使用闭包进行状态处理

最后更新于:2022-04-02 06:48:34

## 使用闭包进行状态处理 将状态传递给处理程序通常很棘手。有两种方法:通过闭包传递状态,这有助于提高单个处理程序的灵活性,或使用结构体进行传递。 我们将使用结构控制器来存储接口,并使用由外部函数修改的单个处理程序创建两个路由。 ### 实践 1. 建立 controller.go: ``` package controllers // Controller 传递状态给处理函数 type Controller struct { storage Storage } func New(storage Storage) *Controller { return &Controller{ storage: storage, } } type Payload struct { Value string `json:"value"` } ``` 2. 建立 storage.go: ``` package controllers // Storage 接口支持存取单个值 type Storage interface { Get() string Put(string) } // MemStorage 实现了 Storage接口 type MemStorage struct { value string } func (m *MemStorage) Get() string { return m.value } func (m *MemStorage) Put(s string) { m.value = s } ``` 3. 建立 post.go: ``` package controllers import ( "encoding/json" "net/http" ) // SetValue 修改Controller的存储内容 func (c *Controller) SetValue(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) return } if err := r.ParseForm(); err != nil { w.WriteHeader(http.StatusInternalServerError) return } value := r.FormValue("value") c.storage.Put(value) w.WriteHeader(http.StatusOK) p := Payload{Value: value} if payload, err := json.Marshal(p); err == nil { w.Write(payload) } } ``` 4. 建立 get.go: ``` package controllers import ( "encoding/json" "net/http" ) // GetValue是一个封装HandlerFunc的闭包,如果UseDefault为true,则值始终为“default”,否则它将是存储在storage中的任何内容 func (c *Controller) GetValue(UseDefault bool) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method != "GET" { w.WriteHeader(http.StatusMethodNotAllowed) return } value := "default" if !UseDefault { value = c.storage.Get() } p := Payload{Value: value} w.WriteHeader(http.StatusOK) if payload, err := json.Marshal(p); err == nil { w.Write(payload) } } } ``` 5. 建立 main.go: ``` package main import ( "fmt" "net/http" "github.com/agtorre/go-cookbook/chapter7/controllers" ) func main() { storage := controllers.MemStorage{} c := controllers.New(&storage) http.HandleFunc("/get", c.GetValue(false)) http.HandleFunc("/get/default", c.GetValue(true)) http.HandleFunc("/set", c.SetValue) fmt.Println("Listening on port :3333") err := http.ListenAndServe(":3333", nil) panic(err) } ``` 6. 运行: ``` go run main.go ``` 这会输出 ``` Listening on port :3333 ``` 进行请求测试: ``` $curl "http://localhost:3333/set -X POST -d "value=value" {"value":"value"} $curl "http://localhost:3333/get -X GET {"value":"value"} $curl "http://localhost:3333/get/default -X GET {"value":"default"} ``` ### 说明 这种策略有效,因为Go允许函数传递。我们可以用类似的方法传入数据库连接、日志记录等。在示例中,我们插入了一个storage接口,所有请求的处理方法都可以使用其方法和属性。 GetValue方法没有传递http.HandlerFunc签名,而是直接返回它,我们通过这种方式来注入状态。在main.go中,我们定义了两个路由,其中UseDefault设置为false,另一个路由设置为true。这可以在定义跨越多个路由的函数时使用,也可以在使用处理程序感觉过于繁琐的结构时使用。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

处理Web请求

最后更新于:2022-04-02 06:48:32

## 处理Web请求 Go定义了HandlerFuncs和一个Handler接口: ``` // HandlerFunc 实现了Handler接口 type HandlerFunc func(http.ResponseWriter, *http.Request) type Handler interface { ServeHTTP(http.ResponseWriter, *http.Request) } ``` net/http包对这种操作方式广泛使用。例如路由可以附加到Handler或HandlerFunc接口。本节将探讨如何在处理http.Request之后创建Handler接口,侦听本地端口以及在http.ResponseWriter接口上执行某些操作。 这是建立Go Web应用程序和RESTFul API的基础。 ### 实践 1. 建立 get.go: ``` package handlers import ( "fmt" "net/http" ) // HelloHandler 接收GET请求中的参数"name" // 在responds中返回 Hello ! 文本数据 func HelloHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain") if r.Method != http.MethodGet { w.WriteHeader(http.StatusMethodNotAllowed) return } name := r.URL.Query().Get("name") w.WriteHeader(http.StatusOK) w.Write([]byte(fmt.Sprintf("Hello %s!", name))) } ``` 2. 建立 post.go: ``` package handlers import ( "encoding/json" "net/http" ) // GreetingResponse 用于序列化GreetingHandler返回的JSON数据 type GreetingResponse struct { Payload struct { Greeting string `json:"greeting,omitempty"` Name string `json:"name,omitempty"` Error string `json:"error,omitempty"` } `json:"payload"` Successful bool `json:"successful"` } // GreetingHandler 返回GreetingResponse格式的数据 func GreetingHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } var gr GreetingResponse if err := r.ParseForm(); err != nil { gr.Payload.Error = "bad request" if payload, err := json.Marshal(gr); err == nil { w.Write(payload) } } name := r.FormValue("name") greeting := r.FormValue("greeting") w.WriteHeader(http.StatusOK) gr.Successful = true gr.Payload.Name = name gr.Payload.Greeting = greeting if payload, err := json.Marshal(gr); err == nil { w.Write(payload) } } ``` 3. 建立 main.go: ``` package main import ( "fmt" "net/http" "github.com/agtorre/go-cookbook/chapter7/handlers" ) func main() { http.HandleFunc("/name", handlers.HelloHandler) http.HandleFunc("/greeting", handlers.GreetingHandler) fmt.Println("Listening on port :3333") err := http.ListenAndServe(":3333", nil) panic(err) } ``` 4. 执行go run main.go 会显示 ``` Listening on port :3333 ``` 在命令行中测试: ``` curl "http://localhost:3333/name?name=Reader" -X GET Hello Reader! curl "http://localhost:3333/greeting" -X POST -d 'name=Reader;greeting=Goodbye' {"payload": {"greeting":"Goodbye","name":"Reader"},"successful":true} ``` ### 说明 示例中我们对GET请求和POST请求分别进行了处理。注意POST的响应是如何返回JOSN格式数据的。 这里只是简单的演示,更丰富的路由解析、限制、处理关闭等复杂操作,可以挑一些第三方库来看看他们是如何思考的。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';

第七章 网络服务

最后更新于:2022-04-02 06:48:29

本章会覆盖以下内容: * 处理Web请求 * 使用闭包进行状态处理 * 请求参数验证 * 内容渲染 * 使用中间件 * 构建反向代理 * 将GRPC导出为JSON API ### 介绍 开箱即用的特性使得Go是编写Web应用程序的绝佳选择。标准库中的net/http和html/template包对全功能现代Web提供了极大的便利。虽然标准库功能齐全,但仍然有各种各样的第三方Web包可用于从路由到全栈框架的所有内容,包括: * https://github.com/urfave/negroni * https://github.com/gin-gonic/gin * https://github.com/labstack/echo * http://www.gorillatoolkit.org/ * https://github.com/julienschmidt/httprouter 本章将重点介绍在处理请求,路由和请求对象以及处理中间件等概念时可能遇到的问题。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)
';