1 - 单元测试

1.1 - Golang单元测试-wire依赖注入

Golang依赖注入框架wire全攻略

在前一阵介绍单元测试的系列文章中,曾经简单介绍过wire依赖注入框架。但当时的wire还处于alpha阶段,不过最近wire已经发布了首个beta版,API发生了一些变化,同时也承诺除非万不得已,将不会破坏API的兼容性。在前文中,介绍了一些wire的基本概况,本篇就不再重复,感兴趣的小伙伴们可以回看一下: 搞定Go单元测试(四)—— 依赖注入框架(wire)。本篇将具体介绍wire的使用方法和一些最佳实践。

本篇中的代码的完整示例可以在这里找到:wire-examples

Installing

go get github.com/google/wire/cmd/wire

Quick Start

我们先通过一个简单的例子,让小伙伴们对wire有一个直观的认识。下面的例子展示了一个简易wire依赖注入示例:

$ ls
main.go  wire.go

main.go

package main

import "fmt"

type Message struct {
    msg string
}
type Greeter struct {
    Message Message
}
type Event struct {
    Greeter Greeter
}
// NewMessage Message的构造函数
func NewMessage(msg string) Message {
    return Message{
        msg:msg,
    }
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}
func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}
func (g Greeter) Greet() Message {
    return g.Message
}

// 使用wire前
func main() {
    message := NewMessage("hello world")
    greeter := NewGreeter(message)
    event := NewEvent(greeter)

    event.Start()
}
/*
// 使用wire后
func main() {
    event := InitializeEvent("hello_world")

    event.Start()
}*/

wire.go

// +build wireinject
// The build tag makes sure the stub is not built in the final build.

package main

import "github.com/google/wire"

// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
    wire.Build(NewEvent, NewGreeter, NewMessage)
    return Event{}  //返回值没有实际意义,只需符合函数签名即可
}

调用wire命令生成依赖文件:

$ wire
wire: github.com/DrmagicE/wire-examples/quickstart: wrote XXXX\github.com\DrmagicE\wire-examples\quickstart\wire_gen.go
$ ls
main.go  wire.go  wire_gen.go

wire_gen.go wire生成的文件

// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from wire.go:

func InitializeEvent(msg string) Event {
    message := NewMessage(msg)
    greeter := NewGreeter(message)
    event := NewEvent(greeter)
    return event
}

使用前 V.S 使用后

...
/*
// 使用wire前
func main() {
    message := NewMessage("hello world")
    greeter := NewGreeter(message)
    event := NewEvent(greeter)

    event.Start()
}*/

// 使用wire后
func main() {
    event := InitializeEvent("hello_world")

    event.Start()
}
...

使用wire后,只需调一个初始化方法既可得到Event了,对比使用前,不仅减少了三行代码,并且无需再关心依赖之间的初始化顺序。

示例传送门: quickstart

Provider & Injector

providerinjectorwire的两个核心概念。

provider: a function that can produce a value. These functions are ordinary Go code. injector: a function that calls providers in dependency order. With Wire, you write the injector’s signature, then Wire generates the function’s body. github.com/google/wire…

通过提供provider函数,让wire知道如何产生这些依赖对象。wire根据我们定义的injector函数签名,生成完整的injector函数,injector函数是最终我们需要的函数,它将按依赖顺序调用provider

在quickstart的例子中,NewMessage,NewGreeter,NewEvent都是providerwire_gen.go中的InitializeEvent函数是injector,可以看到injector通过按依赖顺序调用provider来生成我们需要的对象Event

上述示例在wire.go中定义了injector的函数签名,注意要在文件第一行加上

// +build wireinject
...

用于告诉编译器无需编译该文件。在injector的签名定义函数中,通过调用wire.Build方法,指定用于生成依赖的provider:

// InitializeEvent 声明injector的函数签名
func InitializeEvent(msg string) Event{
    wire.Build(NewEvent, NewGreeter, NewMessage) // <--- 传入provider函数
    return Event{}  //返回值没有实际意义,只需符合函数签名即可
}

该方法的返回值没有实际意义,只需要符合函数签名的要求即可。

高级特性

quickstart示例展示了wire的基础功能,本节将介绍一些高级特性。

接口绑定

根据依赖倒置原则(Dependence Inversion Principle),对象应当依赖于接口,而不是直接依赖于具体实现。

抽象成接口依赖更有助于单元测试哦! 搞定Go单元测试(一)——基础原理 搞定Go单元测试(二)—— mock框架(gomock)

在quickstart的例子中的依赖均是具体实现,现在我们来看看在wire中如何处理接口依赖:

// UserService
type UserService struct {
    userRepo UserRepository // <-- UserService依赖UserRepository接口
}

// UserRepository 存放User对象的数据仓库接口,比如可以是mysql,restful api ....
type UserRepository interface {
    // GetUserByID 根据ID获取User, 如果找不到User返回对应错误信息
    GetUserByID(id int) (*User, error)
}
// NewUserService *UserService构造函数
func NewUserService(userRepo UserRepository) *UserService {
    return &UserService{
        userRepo:userRepo,
    }
}

// mockUserRepo 模拟一个UserRepository实现
type mockUserRepo struct {
    foo string
    bar int
}
// GetUserByID UserRepository接口实现
func (u *mockUserRepo) GetUserByID(id int) (*User,error){
    return &User{}, nil
}
// NewMockUserRepo *mockUserRepo构造函数
func NewMockUserRepo(foo string,bar int) *mockUserRepo {
    return &mockUserRepo{
        foo:foo,
        bar:bar,
    }
}
// MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))

在这个例子中,UserService依赖UserRepository接口,其中mockUserRepoUserRepository的一个实现,由于在Go的最佳实践中,更推荐返回具体实现而不是接口。所以mockUserRepoprovider函数返回的是*mockUserRepo这一具体类型。wire无法自动将具体实现与接口进行关联,我们需要显示声明它们之间的关联关系。通过wire.NewSetwire.Bind*mockUserRepoUserRepository进行绑定:

// MockUserRepoSet 将 *mockUserRepo与UserRepository绑定
var MockUserRepoSet = wire.NewSet(NewMockUserRepo,wire.Bind(new(UserRepository), new(*mockUserRepo)))

定义injector函数签名:

...
func InitializeUserService(foo string, bar int) *UserService{
    wire.Build(NewUserService,MockUserRepoSet) // 使用MockUserRepoSet
    return nil
}
...

示例传送门: binding-interfaces

返回错误

在前面的例子中,我们的provider函数均只有一个返回值,但在某些情况下,provider函数可能会对入参做校验,如果参数错误,则需要返回errorwire也考虑了这种情况,provider函数可以将返回值的第二个参数设置成error:

// Config 配置
type Config struct {
    // RemoteAddr 连接的远程地址
    RemoteAddr string

}
// APIClient API客户端
type APIClient struct {
    c Config
}
// NewAPIClient  APIClient构造函数,如果入参校验失败,返回错误原因
func NewAPIClient(c Config) (*APIClient,error) { // <-- 第二个参数设置成error
    if c.RemoteAddr == "" {
        return nil, errors.New("没有设置远程地址")
    }
    return &APIClient{
        c:c,
    },nil
}
// Service
type Service struct {
    client *APIClient
}
// NewService Service构造函数
func NewService(client *APIClient) *Service{
    return &Service{
        client:client,
    }
}

类似的,injector函数定义的时候也需要将第二个返回值设置成error

...
func InitializeClient(config Config) (*Service, error) { // <-- 第二个参数设置成error
    wire.Build(NewService,NewAPIClient)
    return nil,nil
}
...

观察一下wire生成的injector

func InitializeClient(config Config) (*Service, error) {
    apiClient, err := NewAPIClient(config)
    if err != nil { // <-- 在构造依赖的顺序中如果发生错误,则会返回对应的"零值"和相应错误
        return nil, err
    }
    service := NewService(apiClient)
    return service, nil
}

在构造依赖的顺序中如果发生错误,则会返回对应的"零值"和相应错误。

示例传送门: return-error

Cleanup functions

provider生成的对象需要一些cleanup处理,比如关闭文件,关闭数据库连接等操作时,依然可以通过设置provider的返回值来达到这样的效果:

// FileReader
type FileReader struct {
    f *os.File
}
// NewFileReader *FileReader 构造函数,第二个参数是cleanup function
func NewFileReader(filePath string) (*FileReader, func(), error){
    f, err := os.Open(filePath)
    if err != nil {
        return nil,nil,err
    }
    fr := &FileReader{
        f:f,
    }
    fn := func() {
        log.Println("cleanup")
        fr.f.Close()
    }
    return fr,fn,nil
}

跟返回错误类似,将provider的第二个返回参数设置成func()用于返回cleanup function,上述例子中在第三个参数中返回了error,但这是可选的:

wire对provider的返回值个数和顺序有所规定:

  1. 第一个参数是需要生成的依赖对象
  2. 如果返回2个返回值,第二个参数必须是func()或者error
  3. 如果返回3个返回值,第二个参数必须是func(),第三个参数则必须是error

示例传送门: cleanup-functions

Provider set

当一些provider通常是一起使用的时候,可以使用provider set将它们组织起来,以quickstart示例为模板稍作修改:

// NewMessage Message的构造函数
func NewMessage(msg string) Message {
    return Message{
        msg:msg,
    }
}
// NewGreeter Greeter构造函数
func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}
// NewEvent Event构造函数
func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}
func (e Event) Start() {
    msg := e.Greeter.Greet()
    fmt.Println(msg)
}
// EventSet Event通常是一起使用的一个集合,使用wire.NewSet进行组合
var EventSet  = wire.NewSet(NewEvent, NewMessage, NewGreeter) // <--

上述例子中将Event和它的依赖通过wire.NewSet组合起来,作为一个整体在injector函数签名定义中使用:

func InitializeEvent(msg string) Event{
    //wire.Build(NewEvent, NewGreeter, NewMessage)
    wire.Build(EventSet)
    return Event{}
}

这时只需将EventSet传入wire.Build即可。

示例传送门: provider-set

结构体provider

除了函数外,结构体也可以充当provider的角色,类似于setter注入:

type Foo int
type Bar int

func ProvideFoo() Foo {
    return 1
}
func ProvideBar() Bar {
    return 2
}
type FooBar struct {
    MyFoo Foo
    MyBar Bar
}
var Set = wire.NewSet(
    ProvideFoo,
    ProvideBar,
    wire.Struct(new(FooBar), "MyFoo", "MyBar"))

通过wire.Struct来指定那些字段要被注入到结构体中,如果是全部字段,也可以简写成:

var Set = wire.NewSet(
    ProvideFoo,
    ProvideBar,
    wire.Struct(new(FooBar), "*")) // * 表示注入全部字段

生成的injector函数:

func InitializeFooBar() FooBar {
    foo := ProvideFoo()
    bar := ProvideBar()
    fooBar := FooBar{
        MyFoo: foo,
        MyBar: bar,
    }
    return fooBar
}

示例传送门: struct-provider

Best Practices

区分类型

由于injector的函数中,不允许出现重复的参数类型,否则wire将无法区分这些相同的参数类型,比如:

type FooBar struct {
    foo string
    bar string
}

func NewFooBar(foo string, bar string) FooBar {
    return FooBar{
        foo: foo,
        bar: bar,
    }
}

injector函数签名定义:

// wire无法得知入参a,b跟FooBar.foo,FooBar.bar的对应关系
func InitializeFooBar(a string, b string) FooBar {
    wire.Build(NewFooBar)
    return FooBar{}
}

如果使用上面的provider来生成injector,wire会报如下错误:

provider has multiple parameters of type string

因为入参均是字符串类型,wire无法得知入参a,b跟FooBar.foo,FooBar.bar的对应关系。 所以我们使用不同的类型来避免冲突:

type Foo string
type Bar string
type FooBar struct {
    foo Foo
    bar Bar
}

func NewFooBar(foo Foo, bar Bar) FooBar {
    return FooBar{
        foo: foo,
        bar: bar,
    }
}

injector函数签名定义:

func InitializeFooBar(a Foo, b Bar) FooBar {
    wire.Build(NewFooBar)
    return FooBar{}
}

其中基础类型和通用接口类型是最容易发生冲突的类型,如果它们在provider函数中出现,最好统一新建一个别名来代替它(尽管还未发生冲突),例如:

type MySQLConnectionString string
type FileReader io.Reader

示例传送门 distinguishing-types

Options Structs

如果一个provider方法包含了许多依赖,可以将这些依赖放在一个options结构体中,从而避免构造函数的参数太多:

type Message string

// Options
type Options struct {
    Messages []Message
    Writer   io.Writer
    Reader   io.Reader
}
type Greeter struct {
}

// NewGreeter Greeter的provider方法使用Options以避免构造函数过长
func NewGreeter(ctx context.Context, opts *Options) (*Greeter, error) {
    return nil, nil
}
// GreeterSet 使用wire.Struct设置Options为provider
var GreeterSet = wire.NewSet(wire.Struct(new(Options), "*"), NewGreeter)

injector函数签名:

func InitializeGreeter(ctx context.Context, msg []Message, w io.Writer, r io.Reader) (*Greeter, error) {
    wire.Build(GreeterSet)
    return nil, nil
}

示例传送门 options-structs

一些缺点和限制

额外的类型定义

由于wire自身的限制,injector中的变量类型不能重复,需要定义许多额外的基础类型别名。

mock支持暂时不够友好

目前wire命令还不能识别_test.go结尾文件中的provider函数,这样就意味着如果需要在测试中也使用wire来注入我们的mock对象,我们需要在常规代码中嵌入mock对象的provider,这对常规代码有侵入性,不过官方似乎也已经注意到了这个问题,感兴趣的小伙伴可以关注一下这条issue:github.com/google/wire…

更多参考

官方README.md
官方guide.md
官方best-practices.md

1.2 - Golang单元测试-简单示例

one.go

package unittest

func AddOne(t int32) int32 {
    return t + 1
}

func MinusOne(t int32) int32 {
    return t - 1
}

func MultiAddOne(t int32) int32 {
    t = MinusOne(t)
    t = AddOne(t)
    t = AddOne(t)
    return t
}

one_test.go

package unittest

import (
    "testing"

    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
)

func TestMultiAddOne(t *testing.T) {
    Convey("TestApplyFunc", t, func() {
        Convey("input and output param", func() {
            patches := ApplyFunc(AddOne, func(t1 int32) int32 {
                return 5
            }) //对函数AddOne打桩
            defer patches.Reset()
            patches.ApplyFunc(MinusOne, func(t1 int32) int32 {
                return -2
            }) //对函数MinusOne打桩
            result := MultiAddOne(2) //看好了我调用的是MultiAddOne函数,而MultiAddOne函数内部调用了AddOne和MinusOne。
            So(result, ShouldEqual, 3)
        })
    })
}

1.3 - Golang单元测试01

搞定Go单元测试(一)——基础原理

单元测试是代码质量的保证。本系列文章将一步步由浅入深展示如何在Go中做单元测试。

Go对单元测试的支持相当友好,标准包中就支持单元测试,在开始本系阅读之前,需要对标准测试包的基本用法有所了解。

现在,我们从单元测试的基本思想和原理入手,一起来看看如何基于Go提供的标准测试包来进行单元测试。

单元测试的难点

1.掌握单元测试粒度

单元测试粒度是让人十分头疼的问题,特别是对于初尝单元测试的程序员。测试粒度做的太细,会耗费大量的开发以及维护时间,每改一个方法,都要改动其对应的测试方法。当发生代码重构的时候那简直就是噩梦(因为你所有的单元测试又都要写一遍了…)。 如单元测试粒度太粗,一个测试方法测试了n多方法,那么单元测试将显的非常臃肿,脱离了单元测试的本意,容易把单元测试写成集成测试

2. 破除外部依赖(mock,stub 技术)

单元测试一般不允许有任何外部依赖(文件依赖,网络依赖,数据库依赖等),我们不会在测试代码中去连接数据库,调用api等。这些外部依赖在执行测试的时候需要被模拟(mock/stub)。在测试的时候,我们使用模拟的对象来模拟真实依赖下的各种行为。如何运用mock/stub来模拟系统真实行为算是单元测试道路上的一只拦路虎。别着急,本文会通过示例来展示如何在Go中使用mock/stub来完成单元测试。

有的时候模拟是有效的方便的。但我们要提防过度的mock/stub,因为其会导致单元测试主要在测模拟对象而不是实际的系统。

Costs and Benefits

在受益于单元测试的好处的同时,也必然增加了代码量以及维护成本(单元测试代码也是要维护的)。下面这张成本/价值象限图很清晰的阐述了在不同性质的系统中单元测试成本价值之间的关系。

1.依赖很少的简单的代码(左下)

对于外部依赖少,代码又简单的代码。自然其成本和价值都是比较低的。举Go官方库里errors包为例,整个包就两个方法 New()Error(),没有任何外部依赖,代码也很简单,所以其单元测试起来也是相当方便。

2. 依赖较多但是很简单的代码(右下)

依赖一多,mock和stub就必然增多,单元测试的成本也就随之增加。但代码又如此简单(比如上述errors包的例子),这个时候写单元测试的成本已经大于其价值,还不如不写单元测试

3. 依赖很少的复杂代码 (左上)

像这一类代码,是最有价值写单元测试的。比如一些独立的复杂算法(银行利息计算,保险费率计算,TCP协议解析等),像这一类代码外部依赖很少,但却很容易出错,如果没有单元测试,几乎不能保证代码质量。

4.依赖很多又很复杂(右上)

这种代码显然是单元测试的噩梦。写单元测试吧,代价高昂;不写单元测试吧,风险太高。像这种代码我们尽量在设计上将其分为两部分:1.处理复杂的逻辑部分 2.处理依赖部分 然后1部分进行单元测试

原文参考:blog.stevensanderson.com/2009/11/04/…

迈出单元测试第一步

1. 识别依赖,抽象成接口

识别系统中的外部依赖,普遍来说,我们遇到最常见的依赖无非下面几种:

  1. 网络依赖——函数执行依赖于网络请求,比如第三方http-api,rpc服务,消息队列等等
  2. 数据库依赖
  3. I/O依赖(文件)

当然,还有可能是依赖还未开发完成的功能模块。但是处理方法都是大同小异的——抽象成接口,通过mock和stub进行模拟测试。

2. 明确需要测什么

当我们开始敲产品代码的时候,我们必然已经过初步的设计,已经了解系统中的外部依赖以及业务复杂的部分,这些部分是要优先考虑写单元测试的。在写每一个方法/结构体的时候同时思考这个方法/结构体需不需要测试?如何测试?对于什么样的方法/结构体需要测试,什么样的可以不做,除了可以从上面的成本/价值象限图中获得答案外,还可以参考以下关于单元测试粒度要做多细问题的回答:

老板为我的代码付报酬,而不是测试,所以,我对此的价值观是——测试越少越好,少到你对你的代码质量达到了某种自信(我觉得这种的自信标准应该要高于业内的标准,当然,这种自信也可能是种自大)。如果我的编码生涯中不会犯这种典型的错误(如:在构造函数中设了个错误的值),那我就不会测试它。我倾向于去对那些有意义的错误做测试,所以,我对一些比较复杂的条件逻辑会异常地小心。当在一个团队中,我会非常小心的测试那些会让团队容易出错的代码coolshell.cn/articles/82…

Mock和Stub怎么做

Mock(模拟)和Stub(桩)是在测试过程中,模拟外部依赖行为的两种常用的技术手段。 通过Mock和Stub我们不仅可以让测试环境没有外部依赖,而且还可以模拟一些异常行为,如数据库服务不可用,没有文件的访问权限等等。

Mock和Stub的区别

在Go语言中,可以这样描述Mock和Stub:

  • Mock:在测试包中创建一个结构体,满足某个外部依赖的接口 interface{}
  • Stub:在测试包中创建一个模拟方法,用于替换生成代码中的方法

还是有点抽象,下面举例说明。

Mock示例

Mock:在测试包中创建一个结构体,满足某个外部依赖的接口 interface{}

生产代码:

//auth.go
//假设我们有一个依赖http请求的鉴权接口
type AuthService interface{
    Login(username string,password string) (token string,e error)
    Logout(token string) error
}

mock代码:

//auth_test.go
type authService struct {}
func (auth *authService) Login (username string,password string) (string,error){
    return "token", nil
}
func (auth *authService) Logout(token string) error{
    return nil
}

在这里我们用 authService实现了 AuthService接口,这样测试 Login,Logout就不再需需要依赖网络请求了。而且我们也可以模拟一些错误的情况进行测试:

//auth_test.go
//模拟登录失败
type authLoginErr struct {
    auth AuthService  //可以使用组合的特性,Logout方法我们不关心,只用“覆盖”Login方法即可
}
func (auth *authLoginErr) Login (username string,password string) (string,error) {
    return "", errors.New("用户名密码错误")
}

//模拟api服务器宕机
type authUnavailableErr struct {
}
func (auth *authUnavailableErr) Login (username string,password string) (string,error) {
    return "", errors.New("api服务不可用")
}
func (auth *authUnavailableErr) Logout(token string) error{
    return errors.New("api服务不可用")
}

Stub示例

Stub:在测试包中创建一个模拟方法,用于替换生成代码中的方法。 这是《Go语言圣经》(11.2.3)当中的一个例子: 生产代码:

//storage.go
//发送邮件
var notifyUser = func(username, msg string) { //<--将发送邮件的方法变成一个全局变量
    auth := smtp.PlainAuth("", sender, password, hostname)
    err := smtp.SendMail(hostname+":587", auth, sender,
        []string{username}, []byte(msg))
    if err != nil {
        log.Printf("smtp.SendEmail(%s) failed: %s", username, err)
    }
}
//检查quota,quota不足将发邮件
func CheckQuota(username string) {
    used := bytesInUse(username)
    const quota = 1000000000 // 1GB
    percent := 100 * used / quota
    if percent < 90 {
        return // OK
    }
    msg := fmt.Sprintf(template, used, percent)
    notifyUser(username, msg) //<---发邮件
}

显然,在跑单元测试的过程中,我们肯定不会真的给用户发邮件。在书中采用了stub的方式来进行测试:

//storage_test.go
func TestCheckQuotaNotifiesUser(t *testing.T) {
    var notifiedUser, notifiedMsg string
    notifyUser = func(user, msg string) {  //<-看这里就够了,在测试中,覆盖了发送邮件的全局变量
        notifiedUser, notifiedMsg = user, msg
    }

    // ...simulate a 980MB-used condition...

    const user = "joe@example.org"
    CheckQuota(user)
    if notifiedUser == "" && notifiedMsg == "" {
        t.Fatalf("notifyUser not called")
    }
    if notifiedUser != user {
        t.Errorf("wrong user (%s) notified, want %s",
            notifiedUser, user)
    }
    const wantSubstring = "98% of your quota"
    if !strings.Contains(notifiedMsg, wantSubstring) {
        t.Errorf("unexpected notification message <<%s>>, "+
            "want substring %q", notifiedMsg, wantSubstring)
    }
}

可以看到,在Go中,如果要用stub,那将是侵入式的,必须将生产代码设计成可以用stub方法替换的形式。上述例子体现出来的结果就是:为了测试,专门用一个全局变量 notifyUser来保存了具有外部依赖的方法。然而在不提倡使用全局变量的Go语言当中,这显然是不合适的。所以,并不提倡这种Stub方式。

Mock与Stub相结合

既然不提倡Stub方式,那是不是在Go测试当中就可以抛弃Stub了呢?原本我是这么认为的,但直到我读了这篇译文Golang 标准包布局,虽然这篇译文讲的是包的布局,但里面的测试示例很值得学习。

//生产代码 myapp.go
package myapp

type User struct {
    ID      int
    Name    string
    Address Address
}
//User的一些增删改查
type UserService interface {
    User(id int) (*User, error)
    Users() ([]*User, error)
    CreateUser(u *User) error
    DeleteUser(id int) error
}

常规Mock方式:

//测试代码 myapp_test.go
type userService struct{
}
func (u* userService) User(id int) (*User,error) {
    return &User{Id:1,Name:"name",Address:"address"},nil
}
//..省略其他实现方法

//模拟user不存在
type userNotFound struct {
    u UserService
}
func (u* userNotFound) User(id int) (*User,error) {
    return nil,errors.New("not found")
}

//其他...

一般来说,mock结构体内部很少会放变量,针对每一个要模拟的场景(比如上面的user不存在),最政治正确的方法应该是新建一个mock结构体。这样有两个好处:

  1. mock出来的结构体十分简单,不需要进行额外的设置,不容易出错。
  2. mock出来的结构体职责单一,测试代码自说明能力更强,可读性更高。

但在刚才提到的文章中,他是这么做的:

//测试代码
// UserService 代表一个myapp.UserService.的 mock实现
type UserService struct {
    UserFn      func(id int) (*myapp.User, error)
    UserInvoked bool

    UsersFn     func() ([]*myapp.User, error)
    UsersInvoked bool
    // 其他接口方法补全..
}

// User调用mock实现, 并标记这个方法为已调用
func (s *UserService) User(id int) (*myapp.User, error) {
    s.UserInvoked = true
    return s.UserFn(id)
}

这里不仅实现了接口,还通过在结构体内放置与接口方法函数签名一致的方法( UserFnUsersFn...),以及 XxxInvoked是否调用标识符来追踪方法的调用情况。这种做法其实将mock与stub相结合了起来:在mock对象的内部放置了可以被测试函数替换的函数变量UserFn UsersFn…)。我们可以在我们的测试函数中,根据测试的需要,手动更换函数实现。

//mock与stub结合的方式
func TestUserNotFound(t *testing.T) {
    userNotFound := &UserService{}
    userNotFound.UserFn = func(id int) (*myapp.User, error) { //<--- 设置UserFn的期望返回结果
        return nil,errors.New("not found")
    }
    //后续业务测试代码...

    if !userNotFound.UserInvoked {
        t.Fatal("没有调用User()方法")
    }
}

// 常规mock方式
func TestUserNotFound(t *testing.T) {
    userNotFound := &userNotFound{} //<---结构体方法已经决定了返回值
    //后续业务测试代码
}

通过将mock与stub结合,不仅能在测试方法中动态的更改实现,还追踪方法的调用情况,上述例子中只是追踪了方法是否被调用,实际中,如果有需要,我们也可以追踪方法的调用次数,甚至是方法的调用顺序:

type UserService struct {
    UserFn      func(id int) (*myapp.User, error)
    UserInvoked bool
    UserInvokedTime int //<--追踪调用次数

    UsersFn     func() ([]*myapp.User, error)
    UsersInvoked bool

    // 其他接口方法补全..
    FnCallStack []string //<---函数名slice,追踪调用顺序
}

// User调用mock实现, 并标记这个方法为已调用
func (s *UserService) User(id int) (*myapp.User, error) {
    s.UserInvoked = true
    s.UserInvokedTime++ //<--调用发次数
    s.FnCallStack = append(s.FnCallStack,"User") //调用顺序
    return s.UserFn(id)
}

但同时,我们也会发现我们的mock结构体更复杂了,维护成本也随之增加了。两种mock风格各有各的好处,反正要记得软件工程没有银弹,合适的场景选用合适的方法就行了。 但总体而言,mock与stub相结合的这种方式的确是一种不错的测试思路,尤其是当我们需要追踪函数是否调用,调用次数,调用顺序等信息时,mock+stub将是我们的不二选择。举个例子:

//缓存依赖
type Cache interface{
    Get(id int) interface{} //获取某id的缓存
    Put(id int,obj interface{}) //放入缓存
}

//数据库依赖
type UserRepository interface{
    //....
}
//User结构体
type User struct {
    //...
}
//userservice
type UserService interface{
    cache Cache
    repository UserRepository
}

func (u *UserService) Get(id int) *User {
    //先从缓存找,缓存找不到在去repository里面找
}

func main() {
    userService := NewUserService(xxx) //注入一些外部依赖
    user := userService.Get(2) //获取id = 2的user
}

现在要测试 userService.Get(id)方法的行为:

  1. Cache命中之后是否还查数据库?(不应该再查了)
  2. Cache未命中的情况下是否会查库?
  3. ….

这种测试通过mock+stub结合做起来将会非常方便,作为小练习,可以尝试自己实现一下。

使用依赖注入传递接口

接口需要以依赖注入的方式注入到结构体中,这样才能为测试提供替换接口实现的可能。Why?我们先看一个反面例子,形如下面的写法是无法测试的:

type A interface {
    Fun1()
}
func (f *Foo) Bar() {
    a := NewInstanceOfA(...参数若干) //生成A接口的某个实现
    a.Fun1()  //调用接口方法
}

当你辛辛苦苦的将A接口mock出来后,却发现你根本没有办法在Bar()方法中将mock对象替换进去。下面来看看正确的写法:

type A interface {
    Fun1()
}
type Foo struct {
    a A // A接口
}
func (f *Foo) Bar() {
    f.a.Fun1() //调用接口方法
}
// NewFoo, 通过构造函数的方式,将A接口注入
func NewFoo(a A) *Foo {
    return &Foo{a: A}
}

在例子中我们使用了构造函数传参的方法来做依赖注入(当然你也可以用setter的方式做)。在测试的时候,就可以通过NewFoo()方法将我们的mock对象传递给*Foo了。

通常我们会在main.go中进行依赖注入

总结一下

长篇大论了一大堆,稍微总结一下单元测试的几个关键步骤:

  1. 识别依赖(网络,文件,未完成的功能等等)
  2. 将依赖抽象成接口
  3. main.go中使用依赖注入方式将接口注入

现在,我们已经对单元测试有了一个基本的认识,如果你能完成文中的小练习,那么恭喜你,你已经理解应当如何做单元测试,并成功迈出第一步了。在下一篇文章中,将介绍gomock测试框架,提高我们的测试效率。

1.4 - Golang单元测试02

搞定Go单元测试(二)—— mock框架(gomock)

通过阅读上一篇文章,相信你对怎么做单元测试已经有了初步的概念,可以着手对现有的项目进行改造并开展测试了。学会了走路,我们尝试跑起来,本篇主要介绍gomock测试框架,让我们的单元测试更加有效率。

表格驱动测试方法(Table Driven Tests)

当针对某方法进行单元测试的时候,通常不止写一个测试用例,我们需要测试该方法在多种入参条件下是否都能正常工作,特别是要针对边界值进行测试。通常这个时候表格驱动测试就派上用场了——当你发现你在写测试方法的时候用上了复制粘贴,这就说明你需要考虑使用表格驱动测试来构建你的测试方法了。我们依旧来举个例子:

func TestTime(t *testing.T) {
    testCases := []struct {  // 设计我们的测试用例
        gmt  string
        loc  string
        want string
    }{
        {"12:31", "Europe/Zuri", "13:31"},     // incorrect location name
        {"12:31", "America/New_York", "7:31"}, // should be 07:31
        {"08:08", "Australia/Sydney", "18:08"},
    }
    for _, tc := range testCases {  // 循环执行测试用例
        loc, err := time.LoadLocation(tc.loc)
        if err != nil {
            t.Fatalf("could not load location %q", tc.loc)
        }
        gmt, _ := time.Parse("15:04", tc.gmt)
        if got := gmt.In(loc).Format("15:04"); got != tc.want {
            t.Errorf("In(%s, %s) = %s; want %s", tc.gmt, tc.loc, got, tc.want)
        }
    }
}

表格驱动测试方法让我们的测试方法更加清晰和简练,减少了复制粘贴,并大大提高的测试代码的可读性。

还记得上文说单元测试也是需要维护的吗?单元测试也是代码的一部分,也应当被认真对待。记得要用表格驱动测试的方法来组织你的测试用例,同时别忘了像正式代码那样,写上相应的注释。 更多参考: github.com/golang/go/w… blog.golang.org/subtests

使用测试框架——gomock

What is gomock?

gomock是Google开源的golang测试框架。或者引用官方的话来说:“GoMock is a mocking framework for the Go programming language”。

github.com/golang/mock

Why gomock?

上篇文章末尾介绍了mock和stub相结合的测试方法,可以感受到mock与stub结合起来功能固然强大——调用顺序检测,调用次数检测,动态控制函数的返回值等等,但同时,其带来的维护成本和复杂度缺是不可忽视的,手动维护这样一套测试代码那将是一场灾难。我们期望能用一套框架或者工具,在提供强大的测试功能的同时帮我们维护复杂的mock代码。

How does it work?

gomock通过mockgen命令生成包含mock对象的.go文件,其生成的mock对象具备mock+stub的强大功能,并将我们从写mock对象中解放了出来:

mockgen -destination foo_mock.go -source foo.go -package foo //mock foo.go里面所有的接口,将mock结果保存到foo_mock.go

gomock让我们既能使用mock与stub结合的强大功能,又不需要手动维护这些mock对象,岂不美哉?

举个栗子

在这里我们对gomock的基本功能做一个简单演示: 假设我们的接口定义在 user.go

// user.go
package user

// User 表示一个用户
type User struct {
   Name string
}
// UserRepository 用户仓库
type UserRepository interface {
   // 根据用户id查询得到一个用户或是错误信息
   FindOne(id int) (*User,error)
}

通过mockgen在同目录下生成mock文件user_mock.go

mockgen -source user.go -destination user_mock.go -package user

然后在该目录下新建user_test.go来写我们的测试函数,上述步骤完成之后,我们的目录结构如下:

└── user
    ├── user.go
    ├── user_mock.go
    └── user_test.go

设置函数的返回值

// 静态设置返回值
func TestReturn(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    repo := NewMockUserRepository(ctrl)
    // 期望FindOne(1)返回张三用户
    repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
    // 期望FindOne(2)返回李四用户
    repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
    // 期望给FindOne(3)返回找不到用户的错误
    repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
    // 验证一下结果
    log.Println(repo.FindOne(1)) // 这是张三
    log.Println(repo.FindOne(2)) // 这是李四
    log.Println(repo.FindOne(3)) // user not found
    log.Println(repo.FindOne(4)) //没有设置4的返回值,却执行了调用,测试不通过
}
// 动态设置返回值
func TestReturnDynamic(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    repo := NewMockUserRepository(ctrl)
    // 常用方法之一:DoAndReturn(),动态设置返回值
    repo.EXPECT().FindOne(gomock.Any()).DoAndReturn(func(i int) (*User,error) {
        if i == 0 {
            return nil, errors.New("user not found")
        }
        if i < 100 {
            return &User{
                Name:"小于100",
            }, nil
        } else {
            return &User{
                Name:"大于等于100",
            }, nil
        }
    })
    log.Println(repo.FindOne(120))
    //log.Println(repo.FindOne(66))
    //log.Println(repo.FindOne(0))
}

调用次数检测

func TestTimes(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    repo := NewMockUserRepository(ctrl)
    // 默认期望调用一次
    repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
    // 期望调用2次
    repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil).Times(2)
    // 调用多少次可以,包括0次
    repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found")).AnyTimes()

    // 验证一下结果
    log.Println(repo.FindOne(1)) // 这是张三
    log.Println(repo.FindOne(2)) // 这是李四
    log.Println(repo.FindOne(2)) // FindOne(2) 需调用两次,注释本行代码将导致测试不通过
    log.Println(repo.FindOne(3)) // user not found, 不限调用次数,注释掉本行也能通过测试
}

调用顺序检测

func TestOrder(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    repo := NewMockUserRepository(ctrl)
    o1 := repo.EXPECT().FindOne(1).Return(&User{Name: "张三"}, nil)
    o2 := repo.EXPECT().FindOne(2).Return(&User{Name: "李四"}, nil)
    o3 := repo.EXPECT().FindOne(3).Return(nil, errors.New("user not found"))
    gomock.InOrder(o1, o2, o3) //设置调用顺序
    // 按顺序调用,验证一下结果
    log.Println(repo.FindOne(1)) // 这是张三
    log.Println(repo.FindOne(2)) // 这是李四
    log.Println(repo.FindOne(3)) // user not found

    // 如果我们调整了调用顺序,将导致测试不通过:
    // log.Println(repo.FindOne(2)) // 这是李四
    // log.Println(repo.FindOne(1)) // 这是张三
    // log.Println(repo.FindOne(3)) // user not found
}

上面的示例只展现了gomock功能的冰山一角,在本篇中不再深入讨论,更多用法请参考文档。

更多官方示例:github.com/golang/mock…

如果你完成了上一章的小练习,尝试动手使用gomock改造一下吧!

总结一下

本篇介绍了表格驱动测试与gomock测试框架。运用表格驱动测试方法不仅能使测试代码更精简易读,还能提高我们测试用例的编写能力,无形中提升了单元测试的质量。gomock的功能十分丰富,想掌握各种骚操作还是要细心阅读一下官方示例,但通常20%的常规功能也足够覆盖80%的测试场景了。 表格驱动单元测试和gomock将我们的单元测试效率与质量提升了一个档次。在下一篇文章中,将介绍 testify断言库,继续优化我们的单元测试。

1.5 - Golang单元测试03

搞定Go单元测试(三)—— 断言(testify)

在上一篇,介绍了表格驱动测试方法和gomock测试框架,大大提升了测试效率与质量。本篇将介绍在测试中引入断言(assertion),进一步提升测试效率与质量。

为什么需要断言库

我们先来看看Go标准包中为什么没有断言,官方在FAQ里面回答了这个问题。

golang.org/doc/faq#ass…

总体概括一下大意就是:“Go不提供断言,我们知道这会带来一定的不便,其主要目的是为了防止你们这些程序员在错误处理上偷懒。我们知道这是一个争论点,但是我们觉得这样很coooool~~。”所以,我们引入断言库的原因也很明显了:偷懒,引入断言能为我们提供便利——提高测试效率,增强代码可读性。

testify

在断言库的选择上,我们似乎没有过多的选择,从start数和活跃度来看,基本上是testify一枝独秀。

github.com/stretchr/te…

没有对比就没有伤害,先来看看使用testify之前的测试方法:

func TestSomeFun(t *testing.T){
...
    if v != want {
        t.Fatalf("v值错误,期望值:%s,实际值:%s", want, v)
    }
    if err != nil {
        t.Fatalf("非预期的错误:%s", err)
    }
    if objectA != objectB {
        if objectA.field1 !=  objectB.field1 {
            // t.Fatalf() field1值错误...bla bla bla
        }
         if objectA.field2 !=  objectB.field2 {
            // t.Fatalf() field2值错误...bla bla bla
        }
        // 遍历object所有值... bla bla bla
    }
...
}

上述代码充斥着大量if...else..判断,大段错误信息拼装(真·体力活…),运气不好碰到结构体判断要得将其遍历一遍——不直观,低效,实在是不fashion。 现在,我们使用 testify来改造一下上面的测试示例:

func TestSomeFun(t *testing.T){
    a := assert.New(t)
...
    a.Equal(v, want)
    a.Nil(err,"如果你还是想输出自己拼装的错误信息,可以传第三个参数")
    a.Equal(objectA, objectB)
...
}

三行搞定,测试含义一目了然——直观,高效,简短,fashion。

总结一下

testify使用简单,提升显著,可谓是用一次就会爱上的懒人神器。在结合表格驱动测试,gomock和testify后,我们已经能写出一手优雅漂亮的单元测试代码了。不过,光测试代码优雅还不够,我们还需要帮main.go也打扮打扮。在下一篇,也是本系列最后一篇文章中,我们将介绍wire依赖注入框架,帮main.go减肥瘦身。

1.6 - Golang单元测试04

搞定Go单元测试(四)—— 依赖注入框架(wire)

在第一篇文章中提到过,为了让代码可测,需要用依赖注入的方式来构建我们的对象,而通常我们会在main.go做依赖注入,这就导致main.go会越来越臃肿。为了让单元测试得以顺利进行,main.go牺牲了它本应该纤细苗条的身材。太胖的main.go可不是什么好的信号,本篇将介绍依赖注入框架(wire),致力于帮助main.go恢复身材。

臃肿的main

main.go中做依赖注入,意味着在初始化代码中我们要管理:

  1. 依赖的初始化顺序
  2. 依赖之间的关系

对于小型项目而言,依赖的数量比较少,初始化代码不会很多,不需要引入依赖注入框架。但对于依赖较多的中大型项目,初始化代码又臭又长,可读性和维护性变的很差,随意感受一下:

func main() {
    config := NewConfig()
    // db依赖配置
    db, err := ConnectDatabase(config)
    if err != nil {
        panic(err)
    }
    // PersonRepository 依赖db
    personRepository := NewPersonRepository(db)
    // PersonService 依赖配置 和 PersonRepository
    personService := NewPersonService(config, personRepository)
    // NewServer 依赖配置和PersonService
    server := NewServer(config, personService)
    server.Run()
}

实践表明,修改有大量依赖关系的初始化代码是一项乏味且耗时的工作。这个时候,我们就需要依赖注入框架来帮忙,简化初始化代码。

上述代码来自:blog.drewolson.org/dependency-…

使用依赖注入框架——wire

What is wire?

wire是google开源的依赖注入框架。或者引用官方的话来说:“Wire is a code generation tool that automates connecting components using dependency injection”。

github.com/google/wire

Why wire?

除了wire,Go的依赖注入框架还有Uber的dig和Facebook的inject,它们都是使用反射机制来实现运行时依赖注入(runtime dependency injection),而wire则是采用代码生成的方式来达到编译时依赖注入(compile-time dependency injection)。使用反射带来的性能损失倒是其次,更重要的是反射使得代码难以追踪和调试(反射会令Ctrl+左键失效…)。而wire生成的代码是符合程序员常规使用习惯的代码,十分容易理解和调试。 关于wire的优点,在官方博文上有更详细的的介绍: blog.golang.org/wire

How does it work?

本部分内容参考官方博文:blog.golang.org/wire

wire有两个基本的概念:provider和injector。

provider

provider就是普通的Go函数,可以把它看作是某对象的构造函数,我们通过provider告诉wire该对象的依赖情况:

// NewUserStore是*UserStore的provider,表明*UserStore依赖于*Config和 *mysql.DB.
func NewUserStore(cfg *Config, db *mysql.DB) (*UserStore, error) {...}

// NewDefaultConfig是*Config的provider,没有依赖
func NewDefaultConfig() *Config {...}

// NewDB是*mysql.DB的provider,依赖于ConnectionInfo
func NewDB(info ConnectionInfo) (*mysql.DB, error) {...}

// UserStoreSet 可选项,可以使用wire.NewSet将通常会一起使用的依赖组合起来。
var UserStoreSet = wire.NewSet(NewUserStore, NewDefaultConfig)

injector

injector是wire生成的函数,我们通过调用injector来获取我们所需的对象或值,injector会按照依赖关系,按顺序调用provider函数:

// File: wire_gen.go
// Code generated by Wire. DO NOT EDIT.
//go:generate wire
//+build !wireinject

// initUserStore是由wire生成的injector
func initUserStore(info ConnectionInfo) (*UserStore, error) {
    // *Config的provider函数
    defaultConfig := NewDefaultConfig()
    // *mysql.DB的provider函数
    db, err := NewDB(info)
    if err != nil {
        return nil, err
    }
    // *UserStore的provider函数
    userStore, err := NewUserStore(defaultConfig, db)
    if err != nil {
        return nil, err
    }
    return userStore, nil
}

injector帮我们把按顺序初始化依赖的步骤给做了,我们在main.go中只需要调用initUserStore方法就能得到我们想要的对象了。

那么wire是怎么知道如何生成injector的呢?我们需要写一个函数来告诉它:

  • 定义injector的函数签名
  • 在函数中使用wire.Build方法列举生成injector所需的provider

例如:

// initUserStore用于声明injector的函数签名
func initUserStore(info ConnectionInfo) (*UserStore, error) {
    // wire.Build声明要获取一个UserStore需要调用到哪些provider函数
    wire.Build(UserStoreSet, NewDB)
    return nil, nil  // 这些返回值wire并不关心。
}

有了上面的函数,wire就可以得知如何生成injector了。wire生成injector的步骤描述如下:

  1. 确定所生成injector函数的函数签名:func initUserStore(info ConnectionInfo) (*UserStore, error)
  2. 感知返回值第一个参数是*UserStore
  3. 检查wire.Build列表,找到*UserStore的provider:NewUserStore
  4. 由函数签名func NewUserStore(cfg *Config, db *mysql.DB)得知NewUserStore依赖于*Config, 和*mysql.DB
  5. 检查wire.Build列表,找到*Config*mysql.DB的provider:NewDefaultConfigNewDB
  6. 由函数签名func NewDefaultConfig() *Config得知*Config没有其他依赖了。
  7. 由函数签名func NewDB(info *ConnectionInfo) (*mysql.DB, error)得知*mysql.DB依赖于ConnectionInfo
  8. 检查wire.Build列表,找不到ConnectionInfo的provider,但在injector函数签名中发现匹配的入参类型,直接使用该参数作为NewDB的入参。
  9. 感知返回值第二个参数是error
  10. ….
  11. 按依赖关系,按顺序调用provider函数,拼装injector函数。

举个栗子

栗子传送门:wire-examples

注意

截止本文发布前,官方表明wire的项目状态是alpha,还不适合到生产环境,API存在变化的可能。 虽然是alpha,但其主要作用是为我们生成依赖注入代码,其生成的代码十分通俗易懂,在做好版本控制的前提下,即使是API发生变化,也不会对生成环境造成多坏的影响。我认为还是可以放心使用的。

总结一下

本篇是本系列的最后一篇,回顾前几篇文章,我们以单元测试的原理与基本思想为基础,介绍了表格驱动测试方法,gomock,testify,wire这几样实用工具,经历了“能写单元测试”到“写好单元测试”不断优化的过程。希望本系列文章能让你有所收获。

2 - adodb方式连接sqlServer2k

sqlServer2k 基本看不到了,但是某些系统竟然还用。之前项目遇到过就记录下来怎么连,这是全网唯一能真连成功的代码了

go.mod

require (
    github.com/go-ole/go-ole v1.2.4 // indirect
    github.com/mattn/go-adodb v0.0.1
    github.com/robfig/cron/v3 v3.0.1
    github.com/wonderivan/logger v1.0.0
    golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect
)

main.go

package main

import (
    "database/sql"
    "flag"
    "fmt"
    "log"
    "strconv"

    _ "github.com/mattn/go-adodb"
    "github.com/robfig/cron/v3"
    "github.com/wonderivan/logger"
)

var (
    local    bool
    remoteIP string
    remoteDS string
    database string
)

func init() {
    flag.BoolVar(&local, "local", false, "set window connect.")
    flag.StringVar(&remoteIP, "remoteIP", "192.168.0.10", "set up remote mssql of ip.")
    flag.StringVar(&remoteDS, "remoteDS", "MSSQLSERVER", "set up remote mssql of datasource.")
    flag.StringVar(&database, "database", "drivertest", "set up remote mssql of database.")
}

type Mssql struct {
    *sql.DB
    dataSource string
    database   string
    windows    bool
    sa         *SA
}

type SA struct {
    user   string
    passwd string
    port   int
}

func NewMssql() *Mssql {
    mssql := new(Mssql)
    dataS := "localhost"
    if !local {
        dataS = fmt.Sprintf("%s\\%s", remoteIP, remoteDS)
    }

    mssql = &Mssql{
        // 如果数据库是默认实例(MSSQLSERVER)则直接使用IP,命名实例需要指明。
        // dataSource: "192.168.1.104\\MSSQLSERVER",
        dataSource: dataS,
        database:   database,
        // windows: true 为windows身份验证,false 必须设置sa账号和密码
        windows: local,
        sa: &SA{
            user:   "elink",
            passwd: "elink888",
            port:   1433,
        },
    }

    return mssql

}

func (m *Mssql) Open() error {
    config := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s",
        m.database, m.dataSource)

    if m.windows {
        config = fmt.Sprintf("%s;Integrated Security=SSPI", config)
    } else {
        // sql 2000的端口写法和sql 2005以上的有所不同,在Data Source 后以逗号隔开。
        config = fmt.Sprintf("%s,%d;user id=%s;password=%s",
            config, m.sa.port, m.sa.user, m.sa.passwd)
    }

    var err error
    m.DB, err = sql.Open("adodb", config)
    fmt.Println(config)

    return err
}

func (m *Mssql) Select() {
    rows, err := m.Query("select uid, name from sysusers")
    if err != nil {
        fmt.Printf("select query err: %s\n", err)
    }
    i := 0
    for rows.Next() {
        var id, name string
        rows.Scan(&id, &name)
        fmt.Printf("id = %s, name = %s\n", id, name)
        i++
    }
    fmt.Println(i)
}

func main() {
    // flag.Parse()

    // m := NewMssql()
    // config1 := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s",
    //     m.database, m.dataSource)

    // if m.windows {
    //     config1 = fmt.Sprintf("%s;Integrated Security=SSPI", config1)
    // } else {
    //     // sql 2000的端口写法和sql 2005以上的有所不同,在Data Source 后以逗号隔开。
    //     config1 = fmt.Sprintf("%s,%d;user id=%s;password=%s",
    //         config1, m.sa.port, m.sa.user, m.sa.passwd)
    // }
    // fmt.Println(config1)
    // err := mssql.Open()

    // checkError(err)

    // mssql.Select()

    // config := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s\\MSSQLSERVER,%s;user id=%s;password=%s",
    //     "drivertest", "192.168.0.10", "1433", "elink", "elink888")
    config := fmt.Sprintf("Provider=SQLOLEDB;Initial Catalog=%s;Data Source=%s\\MSSQLSERVER,%s;user id=%s;password=%s",
        "znykt", "37.64.227.132", "1433", "elink", "elink888")
    DB, err := sql.Open("adodb", config)
    if err != nil {
        log.Fatal(err)
    }

    crontab := cron.New(cron.WithSeconds()) //精确到秒
    // 定时任务
    deviceStatusOnline_spec := "*/5 * * * * ?" //秒 分 时 日 月 周
    // 定义定时器调用的任务函数
    _, err = crontab.AddFunc(deviceStatusOnline_spec, func() {
        rows, err := DB.Query("select top 200 id from MYCARGOOUTRECORD order by id desc")
        if err != nil {
            fmt.Printf("select query err: %s\n", err)
        }
        i := 0
        for rows.Next() {
            // var id, name string
            // rows.Scan(&id, &name)
            // fmt.Printf("id = %s, name = %s\n", id, name)
            i++
        }
        logger.Info("结果数量:" + strconv.Itoa(i))
    })
    if err != nil {
        log.Fatal(err)
    }
    // 启动定时器
    crontab.Start()
    select {}
}

// func checkError(err error) {
//     if err != nil {
//         log.Fatal(err)
//     }
// }

3 - AES-ECB-PKCS5

AES-ECB-PKCS5,附上对应java能相互加解密的 java代码 aes的ecb模式是对称假面
package main

import (
    "bytes"
    "crypto/aes"
    "crypto/cipher"
    "encoding/base64"
    "encoding/hex"
    "fmt"
    "strings"
)

func Base64URLDecode(data string) ([]byte, error) {
    var missing = (4 - len(data)%4) % 4
    data += strings.Repeat("=", missing)
    res, err := base64.URLEncoding.DecodeString(data)
    fmt.Println("  decodebase64urlsafe is :", string(res), err)
    return base64.URLEncoding.DecodeString(data)
}

func Base64UrlSafeEncode(source []byte) string {
    // Base64 Url Safe is the same as Base64 but does not contain '/' and '+' (replaced by '_' and '-') and trailing '=' are removed.
    bytearr := base64.StdEncoding.EncodeToString(source)
    safeurl := strings.Replace(string(bytearr), "/", "_", -1)
    safeurl = strings.Replace(safeurl, "+", "-", -1)
    safeurl = strings.Replace(safeurl, "=", "", -1)
    return safeurl
}

func AesDecrypt(crypted, key []byte) []byte {
    block, err := aes.NewCipher(key)
    if err != nil {
        fmt.Println("err is:", err)
    }
    blockMode := NewECBDecrypter(block)
    origData := make([]byte, len(crypted))
    blockMode.CryptBlocks(origData, crypted)
    origData = PKCS5UnPadding(origData)
    // fmt.Println("source is :", origData, string(origData))
    return origData
}

func AesEncrypt(src, key string) []byte {
    block, err := aes.NewCipher([]byte(key))
    if err != nil {
        fmt.Println("key error1", err)
    }
    if src == "" {
        fmt.Println("plain content empty")
    }
    ecb := NewECBEncrypter(block)
    content := []byte(src)
    content = PKCS5Padding(content, block.BlockSize())
    crypted := make([]byte, len(content))
    ecb.CryptBlocks(crypted, content)
    // 普通base64编码加密 区别于urlsafe base64
    // fmt.Println("base64 result:", base64.StdEncoding.EncodeToString(crypted))

    // fmt.Println("base64UrlSafe result:", Base64UrlSafeEncode(crypted))
    return crypted
}

func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
    padding := blockSize - len(ciphertext)%blockSize
    padtext := bytes.Repeat([]byte{byte(padding)}, padding)
    return append(ciphertext, padtext...)
}

func PKCS5UnPadding(origData []byte) []byte {
    length := len(origData)
    // 去掉最后一个字节 unpadding 次
    unpadding := int(origData[length-1])
    return origData[:(length - unpadding)]
}

type ecb struct {
    b         cipher.Block
    blockSize int
}

func newECB(b cipher.Block) *ecb {
    return &ecb{
        b:         b,
        blockSize: b.BlockSize(),
    }
}

type ecbEncrypter ecb

// NewECBEncrypter returns a BlockMode which encrypts in electronic code book
// mode, using the given Block.
func NewECBEncrypter(b cipher.Block) cipher.BlockMode {
    return (*ecbEncrypter)(newECB(b))
}
func (x *ecbEncrypter) BlockSize() int { return x.blockSize }
func (x *ecbEncrypter) CryptBlocks(dst, src []byte) {
    if len(src)%x.blockSize != 0 {
        panic("crypto/cipher: input not full blocks")
    }
    if len(dst) < len(src) {
        panic("crypto/cipher: output smaller than input")
    }
    for len(src) > 0 {
        x.b.Encrypt(dst, src[:x.blockSize])
        src = src[x.blockSize:]
        dst = dst[x.blockSize:]
    }
}

type ecbDecrypter ecb

// NewECBDecrypter returns a BlockMode which decrypts in electronic code book
// mode, using the given Block.
func NewECBDecrypter(b cipher.Block) cipher.BlockMode {
    return (*ecbDecrypter)(newECB(b))
}
func (x *ecbDecrypter) BlockSize() int { return x.blockSize }
func (x *ecbDecrypter) CryptBlocks(dst, src []byte) {
    if len(src)%x.blockSize != 0 {
        panic("crypto/cipher: input not full blocks")
    }
    if len(dst) < len(src) {
        panic("crypto/cipher: output smaller than input")
    }
    for len(src) > 0 {
        x.b.Decrypt(dst, src[:x.blockSize])
        src = src[x.blockSize:]
        dst = dst[x.blockSize:]
    }
}

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {

    key, _ := base64.StdEncoding.DecodeString("cmVmb3JtZXJyZWZvcm1lcg==")
    strkey := string(key)
    fmt.Println(strkey)
    fmt.Println([]byte(strkey))
    fmt.Println()
    fmt.Println()
    fmt.Println()

    // wait_encode_str := "{\"stationNo\":\"241\"}"
    // encode_str := AesEncrypt(wait_encode_str, "reformerreformer")
    // hex.EncodeToString(encode_str)
    // fmt.Println(hex.EncodeToString(encode_str))

    wait_decode_str := "34D4F1A0506840B2A6D93C20C098312FAEE02E16FC26144A0621512E9F20CBB0AD121A74EFAD7A8935B132981D6C4198E06B8949E593E2A7D6AB4AB640CC498EE58CD0A66B5B0BBAAE1DBF4C9E4A02DE5658319E93B46831595515FA9F8EFD4202631F09DADDCD575B40C7F76739B31393A455B10AE73B2093EBD6B58DB88D24ACFB0F1DD123A9883051C8986BE060A168024690058318B8271F12E9D4795ACED75BDCC4F1F9AF75743BB81066F64D934B02CD41BE0528F2CEFFC3C40AC1645CA53943024C236A75F247DF3131704BECA1AFE7ADB81343E6D5914940F4E608BECF0D290EDD3B0F0B1F9CE0CD1557479B9E888007A48B19557A989E325193BD0F3EF40C9530B6B6F0D60F231A2EA2A58E9EA0D3B212E6C7BD177D0165C3FB052CBB672E9591D57F81115EBAEF9DC8116813F8D67289A440403D3CC9C7B0F716736E04780C06FCE2BC2D01938AE55A14FD6E0965A241608A371136FC9229BB61093367C8E674AAA35C66F4004C6478C11DE085A81A8D289DDF47EFA422BB9311355FB35218112F26981084812B3FBA2CC55D1267804FE913495C1518342E94B1C45A122B0516C01488D7FB6FEB3A06F37F"

    hex_data, _ := hex.DecodeString(wait_decode_str)
    decode_str := AesDecrypt(hex_data, key)
    fmt.Println(string(decode_str))

    // b1, _ := Base64URLDecode("cmVmb3JtZXJyZWZvcm1lcg==")
    // fmt.Println(string(b1))

    // b2 := Base64UrlSafeEncode([]byte("reformerreformer"))
    // fmt.Println(b2)
}

import java.security.Key;
import java.security.NoSuchAlgorithmException;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
 * AES加密类
 *
 */
public class AESUtils {
    /**
     * 密钥算法
     */
    private static final String KEY_ALGORITHM = "AES";

    private static final String DEFAULT_CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";

    /**
     * 初始化密钥
     *
     * @return byte[] 密钥
     * @throws Exception
     */
    public static byte[] initSecretKey() {
        // 返回生成指定算法的秘密密钥的 KeyGenerator 对象
        KeyGenerator kg = null;
        try {
            kg = KeyGenerator.getInstance(KEY_ALGORITHM);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            return new byte[0];
        }
        // 初始化此密钥生成器,使其具有确定的密钥大小
        // AES 要求密钥长度为 128
        kg.init(128);
        // 生成一个密钥
        SecretKey secretKey = kg.generateKey();
        return secretKey.getEncoded();
    }

    /**
     * 转换密钥
     *
     * @param key
     *            二进制密钥
     * @return 密钥
     */
    public static Key toKey(byte[] key) {
        // 生成密钥
        return new SecretKeySpec(key, KEY_ALGORITHM);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            密钥
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, Key key) throws Exception {
        return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            二进制密钥
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, byte[] key) throws Exception {
        return encrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            二进制密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
        // 还原密钥
        Key k = toKey(key);
        return encrypt(data, k, cipherAlgorithm);
    }

    /**
     * 加密
     *
     * @param data
     *            待加密数据
     * @param key
     *            密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 加密数据
     * @throws Exception
     */
    public static byte[] encrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
        // 实例化
        Cipher cipher = Cipher.getInstance(cipherAlgorithm);
        // 使用密钥初始化,设置为加密模式
        cipher.init(Cipher.ENCRYPT_MODE, key);
        // 执行操作
        return cipher.doFinal(data);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            二进制密钥
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, byte[] key) throws Exception {
        return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            密钥
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, Key key) throws Exception {
        return decrypt(data, key, DEFAULT_CIPHER_ALGORITHM);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            二进制密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, byte[] key, String cipherAlgorithm) throws Exception {
        // 还原密钥
        Key k = toKey(key);
        return decrypt(data, k, cipherAlgorithm);
    }

    /**
     * 解密
     *
     * @param data
     *            待解密数据
     * @param key
     *            密钥
     * @param cipherAlgorithm
     *            加密算法/工作模式/填充方式
     * @return byte[] 解密数据
     * @throws Exception
     */
    public static byte[] decrypt(byte[] data, Key key, String cipherAlgorithm) throws Exception {
        // 实例化
        Cipher cipher = Cipher.getInstance(cipherAlgorithm);
        // 使用密钥初始化,设置为解密模式
        cipher.init(Cipher.DECRYPT_MODE, key);
        // 执行操作
        return cipher.doFinal(data);
    }

    public static String showByteArray(byte[] data) {
        if (null == data) {
            return null;
        }
        StringBuilder sb = new StringBuilder("{");
        for (byte b : data) {
            sb.append(b).append(",");
        }
        sb.deleteCharAt(sb.length() - 1);
        sb.append("}");
        return sb.toString();
    }

    /**
     * 将16进制转换为二进制
     *
     * @param hexStr
     * @return
     */
    public static byte[] parseHexStr2Byte(String hexStr) {

        if (hexStr.length() < 1)
            return null;
        byte[] result = new byte[hexStr.length() / 2];
        for (int i = 0; i < hexStr.length() / 2; i++) {
            int high = Integer.parseInt(hexStr.substring(i * 2, i * 2 + 1), 16);
            int low = Integer.parseInt(hexStr.substring(i * 2 + 1, i * 2 + 2), 16);
            result[i] = (byte) (high * 16 + low);
        }
        return result;
    }

    /**
     * 将二进制转换成16进制
     *
     * @param buf
     * @return
     */
    public static String parseByte2HexStr(byte buf[]) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < buf.length; i++) {
            String hex = Integer.toHexString(buf[i] & 0xFF);
            if (hex.length() == 1) {
                hex = '0' + hex;
            }
            sb.append(hex.toUpperCase());
        }
        return sb.toString();
    }

    /**
     * @param str
     * @param key
     * @return
     * @throws Exception
     */
    public static String aesEncrypt(String str, String key) throws Exception {
        if (str == null || key == null)
            return null;
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
        byte[] bytes = cipher.doFinal(str.getBytes("utf-8"));
        return new BASE64Encoder().encode(bytes);
    }

    public static String aesDecrypt(String str, String key) throws Exception {
        if (str == null || key == null)
            return null;
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key.getBytes("utf-8"), "AES"));
        byte[] bytes = new BASE64Decoder().decodeBuffer(str);
        bytes = cipher.doFinal(bytes);
        return new String(bytes, "utf-8");
    }

    public static void main(String[] args) throws Exception {
        byte[] key = initSecretKey();
        System.out.println("key:" + Base64.encodeBase64String(key));
        System.out.println("key:" + showByteArray(key));


        // 指定key
//        String kekkk = "9iEepr1twrizIEKrs1hs2A==";
        String kekkk = "cmVmb3JtZXJyZWZvcm1lcg==";
        byte[] bs = Base64.decodeBase64(kekkk);
        System.out.println(bs);
        System.out.println("kekkk:" + showByteArray(Base64.decodeBase64(kekkk)));
        Key k = toKey(Base64.decodeBase64(kekkk));

        String data = "{\"requestName\":\"BeforeIn\",\"requestValue\":{\"carCode\":\"浙AD0V07\",\"inTime\":\"2016-09-29 10:06:03\",\"inChannelId\":\"4\",\"GUID\":\"1403970b-4eb2-46bc-8f2b-eeec91ddcd5f\",\"inOrOut\":\"0\"},\"Type\":\"0\"}";
//        System.out.println("加密前数据: string:" + data);
//        System.out.println("加密前数据: byte[]:" + showByteArray(data.getBytes()));
//        System.out.println();

        byte[] encryptData = encrypt(data.getBytes(), k);
        String encryptStr=parseByte2HexStr(encryptData);

        System.out.println("加密后数据: byte[]:" + showByteArray(encryptData));
        System.out.println("加密后数据: Byte2HexStr:" + encryptStr);
        System.out.println();

        byte[] encryptStrByte = parseHexStr2Byte("96B742F275ECC2C77374E888CC3AD5D46CDDDAFA3BBA1AA8184D07BED0D957B250DF794666F5ACA787A8D2F20FB7D195C39E78FBDBDDD2F14B00AFC021BA306B03DB52706969E8497F91084CCA48EB81D902E3C32112F1DB07B21B45314ECFA2742F838C368C770F9C21DE07B99844A691269F83C5582A4EE21FB2C22A2168420EBC4AEEE6FB7D3B182AE5158F1F8E62F3BE3AA26C4F220E382B304F91A52A8CD823B68129409BA6059621F3EC0C94BBE8C5A9C1236C9A137536502BB6354D56");
        System.out.println(encryptStrByte.length);
        byte[] decryptData = decrypt(encryptStrByte, k);
        System.out.println("解密后数据: byte[]:" + showByteArray(decryptData));
        System.out.println("解密后数据: string:" + new String(decryptData));

    }
}

java依赖

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>1.4</version>
        </dependency>

4 - base64

package main

import (
    "encoding/base64"
    "fmt"
    "strings"
)

//Base64UrlSafeEncode 需要替换掉一些字符
func Base64UrlSafeEncode(source []byte) string {
    // Base64 Url Safe is the same as Base64 but does not contain '/' and '+' (replaced by '_' and '-') and trailing '=' are removed.
    bytearr := base64.StdEncoding.EncodeToString(source)
    safeurl := strings.Replace(string(bytearr), "/", "_", -1)
    safeurl = strings.Replace(safeurl, "+", "-", -1)
    safeurl = strings.Replace(safeurl, "=", "", -1)
    return safeurl
}

func main() {

    //标准base64编码
    wait_encode_data := "reformerreformer"
    wait_decode_data := "cmVmb3JtZXJyZWZvcm1lcg"

    //使用标准库自带的方式加密
    sEnc := base64.StdEncoding.EncodeToString([]byte(wait_encode_data))
    fmt.Println(sEnc)

    sDec, _ := base64.StdEncoding.DecodeString("cmVmb3JtZXJyZWZvcm1lcg==")
    fmt.Println(string(sDec))

    //兼容base64编码
    uEnc := base64.URLEncoding.EncodeToString([]byte(wait_encode_data))
    fmt.Println(uEnc)

    uDec, _ := base64.URLEncoding.DecodeString(wait_decode_data)
    fmt.Println(string(uDec))

}

5 - cbor

CBOR(Concise Binary Object Representation)是一种轻量级的数据交换格式,类似于JSON,但它以二进制形式表示数据,而不是文本形式。CBOR设计用于在网络上传输数据时减少数据的大小和复杂性,同时保持良好的可读性和可扩展性。
package main

import (
    "encoding/hex"
    "fmt"

    "github.com/fxamacker/cbor/v2"
)

func main() {

    b, _ := cbor.Marshal("hello world")
    fmt.Println(hex.EncodeToString(b))

    cborBytes, _ := hex.DecodeString(hex.EncodeToString(b))

    s := ""
    cbor.Unmarshal(cborBytes, &s)

    fmt.Printf("%s", s)
}

6 - channel

  1. 顺序执行两个协程
package main

import (
    "fmt"
    "log"
    "math/rand"
    "sync"
    "time"
)

func main() {
    //顺序执行两个协程函数
    ch1 := make(chan string, 5)
    go testBoringWithChannelClose("boring!", ch1)
    ch2 := make(chan string, 5)
    go testBoringWithChannelClose("funning!", ch2)

    for b := range ch1 {
        go log.Printf("You say: %s", b)
    }

    for b := range ch2 {
        go log.Printf("You say: %s", b)
    }

}

func testBoringWithChannelClose(msg string, c chan string) {
    for i := 0; i < 20; i++ {
        c <- fmt.Sprintf("%s %d", msg, i)
        time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond)
    }
    fmt.Println(msg + "结束")
    close(c)
}
  1. 带缓冲的channel 就像一个携程池,去执行协程任务。
package main

import (
    "fmt"
    "log"
    "math/rand"
    "sync"
    "time"
)

func main() {


    //限制1024个协程数执行任务
    goNum1024Times()
}

// 控制1024个协程
func goNum1024Times() {
    var wg sync.WaitGroup
    ch := make(chan struct{}, 1024)
    for i := 0; i < 20000; i++ {
        wg.Add(1)
        ch <- struct{}{}
        go func() {
            defer wg.Done()
            fmt.Printf("%d\n", len(ch)) //打印通道的长度
            <-ch
        }()
    }
    wg.Wait()
}

7 - CRC16

package main

import (
    "CRC15/method" //因为gomod 定义了模块名为 module CRC15 所以引用目录下的method
    "encoding/hex"
    "fmt"
    "strings"
)

func main() {

    byteArr, _ := hex.DecodeString("7F39F60116C067321223344556677889800112230100000000F700000000000000000000000000000000323030B7C2D8BB353030FABBF7D63130")
    checkVal := method.UsMBCRC16(byteArr)
    fmt.Println(strings.ToUpper(fmt.Sprintf("%X", checkVal)))
}

go.mod

module CRC15

go 1.13

method/crc16.go

package method

import (
    "encoding/hex"
    "fmt"
    "strings"
)

var auchCRCLo = [256]uint8{
    0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06,
    0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04, 0xCC, 0x0C, 0x0D, 0xCD,
    0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09,
    0x08, 0xC8, 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A,
    0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC, 0x14, 0xD4,
    0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3,
    0x11, 0xD1, 0xD0, 0x10, 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3,
    0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
    0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A,
    0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38, 0x28, 0xE8, 0xE9, 0x29,
    0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED,
    0xEC, 0x2C, 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26,
    0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0, 0xA0, 0x60,
    0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67,
    0xA5, 0x65, 0x64, 0xA4, 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F,
    0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
    0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E,
    0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C, 0xB4, 0x74, 0x75, 0xB5,
    0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71,
    0x70, 0xB0, 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92,
    0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54, 0x9C, 0x5C,
    0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B,
    0x99, 0x59, 0x58, 0x98, 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B,
    0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
    0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42,
    0x43, 0x83, 0x41, 0x81, 0x80, 0x40,
}

var auchCRCHi = [256]uint8{
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
    0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
    0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
    0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
    0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
    0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
    0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
    0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
    0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
    0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
    0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0,
    0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40,
    0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1,
    0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
    0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0,
    0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
}

func UsMBCRC16(pucFrame []byte) int {
    ucCRCHi := 0xFF
    ucCRCLo := 0xFF
    iIndex := 0
    for i := 0; i < len(pucFrame); i++ {
        iIndex = ucCRCLo ^ int(pucFrame[i])
        ucCRCLo = ucCRCHi ^ int(auchCRCHi[iIndex])
        ucCRCHi = int(auchCRCLo[iIndex])
    }
    return ucCRCLo<<8 | ucCRCHi
}

func TopscommCrc16(param string, check string) bool {
    byteArr, _ := hex.DecodeString(param)
    checkVal := UsMBCRC16(byteArr)
    h := fmt.Sprintf("%X", checkVal)
    if h != strings.ToUpper(check) {
        return false
    }

    return true
}

8 - gorm连接mssql

go.mod

require (
    github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
    github.com/jinzhu/gorm v1.9.14
)
package main

import (
    "flag"
    "fmt"

    _ "github.com/denisenkom/go-mssqldb"
    "github.com/jinzhu/gorm"
)

type Dt_Cardtype struct {
    gorm.Model
    ID        string `gorm:"id"`
    CardCname string `gorm:"CardCname"`
}

var (
    debug         = flag.Bool("debug", true, "enable debugging")
    password      = flag.String("password", "Elink666.", "the database password")
    port     *int = flag.Int("port", 1433, "the database port")
    server        = flag.String("server", "172.16.183.131", "the database server")
    user          = flag.String("user", "sa", "the database user")
)

func main() {
    connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=carpark", *server, *user, *password, *port)
    db, err := gorm.Open("mssql", connString)
    if err != nil {
        panic("连接数据库失败")
    }
    defer db.Close()

    db.SingularTable(true)
    db.LogMode(true)
    db.CommonDB()

    var dc []Dt_Cardtype
    db.Find(&dc)

    fmt.Println(len(dc))
    // // 自动迁移模式
    // db.AutoMigrate(&Product{})

    // // 创建
    // db.Create(&Product{Code: "L1212", Price: 1000})

    // // 读取
    // var product Product
    // db.First(&product, 1)                   // 查询id为1的product
    // db.First(&product, "code = ?", "L1212") // 查询code为l1212的product

    // // 更新 - 更新product的price为2000
    // db.Model(&product).Update("Price", 2000)

    // // 删除 - 删除product
    // db.Delete(&product)
}

9 - gorose连接mssql

go.mod

require (
    github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
    github.com/gohouse/converter v0.0.3 // indirect
    github.com/gohouse/gorose v1.0.5
    github.com/gohouse/gorose/v2 v2.1.7
    github.com/jinzhu/gorm v1.9.14
)
package main

import (
    "flag"
    "fmt"
    "os"

    _ "github.com/denisenkom/go-mssqldb"
    gorose "github.com/gohouse/gorose/v2"
)

var err error
var engin *gorose.Engin

type DtCardtype struct {
    ID        int64  `gorose:"id"`
    CardCname string `gorose:"CardCname"`
    Mark      string `gorose:"Mark"`
}

var (
    debug         = flag.Bool("debug", true, "enable debugging")
    password      = flag.String("password", "123456", "the database password")
    port     *int = flag.Int("port", 1433, "the database port")
    server        = flag.String("server", "10.10.10.53", "the database server")
    user          = flag.String("user", "sa", "the database user")
)

func init() {
    // 全局初始化数据库,并复用
    // 这里的engin需要全局保存,可以用全局变量,也可以用单例
    // 配置&gorose.Config{}是单一数据库配置
    // 如果配置读写分离集群,则使用&gorose.ConfigCluster{}
    // mysql Dsn示例 "root:root@tcp(localhost:3306)/test?charset=utf8&parseTime=true"
    connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=carpark", *server, *user, *password, *port)
    engin, err = gorose.Open(&gorose.Config{Driver: "mssql", Dsn: connString})
    if err != nil {
        fmt.Println(err)
        os.Exit(-1)
    }
}
func DB() gorose.IOrm {
    return engin.NewOrm()
}
func main() {
    var u []DtCardtype
    DB().Table("Park").First()
    fmt.Println(u)
}

10 - go发送get或post请求

package main

import (
    "bytes"
    "crypto/md5"
    "crypto/tls"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "net/http"
    "strconv"
    "time"
)

func main() {
    // GetData()
    PostMethod()
}

//这是get请求
func GetData() {
    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //如果需要测试自签名的证书 这里需要设置跳过证书检测 否则编译报错
    client := &http.Client{Transport: tr}
    resp, err := client.Get("https://192.168.7.15:8080/v1/getaction.do")
    //此处需要注意经过测试,不论是否有err在此之前一定要判断resp.Body如果不为空则需要关闭,测试环境go1.16
    if resp.Body != nil {
        defer resp.Body.Close()
    }
    if err != nil {
        fmt.Println("error:", err)
        return
    }

    body, err := ioutil.ReadAll(resp.Body)
    fmt.Println(string(body))
}

//这是post请求
func PostMethod() {
    var rsp io.Reader
    tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} //如果需要测试自签名的证书 这里需要设置跳过证书检测 否则编译报错
    client := &http.Client{Transport: tr}
    data := "cmd=123"
    resp, err := client.Post("https://192.168.7.15:8080/v1/postaction.do", data, rsp)
    //此处需要注意经过测试,不论是否有err在此之前一定要判断resp.Body如果不为空则需要关闭,测试环境go1.16
    if resp.Body != nil {
        defer resp.Body.Close()
    }
    if err != nil {
        fmt.Println("err:", err)
    } else {
        body, er := ioutil.ReadAll(resp.Body)
        if er != nil {
            fmt.Println("err:", er)
        } else {
            fmt.Println(string(body))
        }
    }

}

func PostMethod() {
    //{"carmeraId":"epark-370211-dongjiakou~~cctv-dahua~channelCode~dahua!1000003$1$0$6","carmeraDesc":"","status":"1","areaId":"001","dataTime":"2020-11-17 14:30:12"}
    //{"carmeraId":"epark-370211-dongjiakou~~cctv-dahua~channelCode~dahua!1000003$1$0$7","carmeraDesc":"2F走廊西","status":"1","areaId":"001","dataTime":"2020-11-17 15:42:01"}
    m3 := map[string]string{
        "cameraId":   "epark-370211-dongjiakou~~cctv-dahua~channelCode~dahua!1000003$1$0$7",
        "cameraDesc": "2F走廊西",
        "status":     "1",
        "areaId":     "001",
        "dataTime":   "2020-11-17 15:42:01",
    }
    exportData, err := json.Marshal(m3)

    tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
    client := &http.Client{Transport: tr}
    req, err := http.NewRequest(http.MethodPost, "https://eug-test.elinkit.com.cn/epmet-acs/report/video/info", bytes.NewReader(exportData))
    if err != nil {
        log.Fatal(err.Error)
    }
    times := strconv.FormatInt(time.Now().Unix(), 10)
    data := []byte("c3c4029b9741f26ab4a4cbd98c2310ff" + times)
    has := md5.Sum(data)
    accessToken := fmt.Sprintf("%x", has)
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("AccessToken", accessToken)
    req.Header.Set("Timestamp", times)

    response, err := client.Do(req)
    //此处需要注意经过测试,不论是否有err在此之前一定要判断 response.Body 如果不为空则需要关闭,测试环境go1.16
    if response.Body != nil {
        defer response.Body.Close()
    }

    if err != nil {
        fmt.Println("err:", err)

    }

    body, er := ioutil.ReadAll(response.Body)
    if er != nil {
        fmt.Println("err:", er)
    }
    fmt.Println(string(body))
}

11 - go脚本编程接受命令行参数

package main

import (
    "flag"
    "fmt"
)

func main() {
    // 定义几个变量,用于接收命令行的参数值
    var user string
    var password string
    var host string
    var port int
    // &user 就是接收命令行中输入 -u 后面的参数值,其他同理
    flag.StringVar(&user, "u", "root", "账号,默认为root")
    flag.StringVar(&password, "p", "", "密码,默认为空")
    flag.StringVar(&host, "h", "localhost", "主机名,默认为localhost")
    flag.IntVar(&port, "P", 3306, "端口号,默认为3306")
    // 解析命令行参数写入注册的flag里
    flag.Parse()
    // 输出结果
    fmt.Printf("user:%v\npassword:%v\nhost:%v\nport:%v\n",
        user, password, host, port)

}

12 - hmac算法

‌ HMAC (Hash-based Message Authentication Code)是一种基于哈希函数的消息认证码算法‌,用于验证数据的完整性和认证消息的发送者。HMAC结合了哈希函数和密钥,通过将密钥与消息进行哈希运算来生成消息认证码‌

HMAC的工作原理
HMAC使用一个密钥和一个哈希函数(如MD5、SHA-1、SHA-256等)生成一个固定长度的哈希值。具体步骤如下:

‌输入‌:消息和密钥。
‌处理‌:HMAC对输入的消息和密钥进行哈希处理,生成一个固定长度的哈希值。
‌输出‌:该哈希值即为HMAC签名‌

HMAC的安全性
HMAC的安全性依赖于密钥的保密性和所使用的哈希函数的抗碰撞能力。只有持有密钥的一方能够生成或验证该消息的签名,从而确保消息在传输过程中没有被篡改‌

HMAC的应用场景
HMAC广泛应用于各种需要确保数据完整性和认证的应用场景中,例如:

‌ IPSec ‌:在IPSec中,HMAC用于验证IP数据包的完整性和真实性。
‌ SSL/TLS ‌:在SSL/TLS协议中,HMAC用于验证通信双方的身份和数据完整性。
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "fmt"
)

func GenHmacSha256(message string, secret string) string {
    h := hmac.New(sha256.New, []byte(secret))
    h.Write([]byte(message))
    return base64.StdEncoding.EncodeToString(h.Sum(nil))
}

func main() {

    key := "VHc1CBWM0YS204rydm+wWxkQrqJSVyXAAgFRktVpOF4="
    str := "/report/data/device?timestamp=123456789&appKey=9aON8USr&nonce=123456"
    ret := GenHmacSha256(str, key)

    fmt.Printf("ret: %s\n", ret)
}

//RdYq6cvBpeF84HO8iQErRF6Aq9XdE2fDx5w4qjDWfrg=
//RdYq6cvBpeF84HO8iQErRF6Aq9XdE2fDx5w4qjDWfrg=
//6D+MQzWc3eU4eqZkXaUQ4xmeviQPljjzs4CQmF18ldk=
//6D+MQzWc3eU4eqZkXaUQ4xmeviQPljjzs4CQmF18ldk=

13 - md5编码

package main

import (
    "crypto/md5"
    "fmt"
    "io"
)

func main() {
    str := "abcdefg"

    //方法一
    data := []byte(str)
    has := md5.Sum(data)
    md5str1 := fmt.Sprintf("%x", has) //将[]byte转成16进制

    fmt.Println(md5str1)

    //方法二

    w := md5.New()
    io.WriteString(w, str)                   //将str写入到w中
    md5str2 := fmt.Sprintf("%x", w.Sum(nil)) //w.Sum(nil)将w的hash转成[]byte格式

    fmt.Println(md5str2)
}

14 - modbustcp-client

go.mod

require (
    github.com/goburrow/modbus v0.1.0
    github.com/goburrow/serial v0.1.0 // indirect
)
package main

import (
    "log"
    "os"
    "time"

    "github.com/goburrow/modbus"
)

func main() {

    handler := modbus.NewTCPClientHandler("localhost:502")
    handler.Timeout = 10 * time.Second
    handler.SlaveId = 0 //SlaveId即modbustcp协议从0开始得第6字节称,在厂家协议中称为RTU地址无效字段,标准协议中如果是0则为广播地址
    handler.Logger = log.New(os.Stdout, "test: ", log.LstdFlags)
    // Connect manually so that multiple requests are handled in one connection session
    err := handler.Connect()
    if err != nil {
        log.Fatalf("连接错误:%+v", err)
    }
    defer handler.Close()

    // modbus.Tc
    // modbus.NewClient(handler)
    client := modbus.NewClient(handler)
    log.Printf("客户端:%+v", client)
    client.ReadHoldingRegisters(40001, 10)

    res, e1 := client.ReadHoldingRegisters(40001, 2)
    if e1 != nil {
        log.Fatalf("E1错误:%+v", e1)
    }
    log.Printf("资源内容1:%+v", res)
    res, _ = client.ReadHoldingRegisters(40000, 3)
    log.Printf("资源内容2:%+v", res)
    res, _ = client.ReadHoldingRegisters(40000, 3)
    log.Printf("资源内容3:%+v", res)
    res, _ = client.ReadHoldingRegisters(40000, 3)
    log.Printf("资源内容4:%+v", res)

    results1, err := client.ReadDiscreteInputs(15, 2)
    results2, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4})

}

15 - opc-client

package main

import (
    "fmt"

    "github.com/konimarti/opc"
)

func main() {
    client, _ := opc.NewConnection(
        "Matrikon.OPC.Simulation.1",               // ProgId
        []string{"localhost"},                     //  OPC servers nodes
        []string{"Random.Real8", "Random.String"}, // slice of OPC tags
    )
    defer client.Close()

    // read single tag: value, quality, timestamp
    fmt.Println(client.ReadItem("Random.Real8"))

    // read all added tags
    fmt.Println(client.Read())
}

16 - Post上传文件

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "io/ioutil"
    "log"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
)

func newfileUploadRequest(uri string, params map[string]string, paramName, path string) (UploadFileRes, error) {
    file, err := os.Open(path)
    if err != nil {
        return UploadFileRes{}, err
    }
    defer file.Close()

    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    part, err := writer.CreateFormFile(paramName, filepath.Base(path))
    if err != nil {
        return UploadFileRes{}, err
    }
    _, err = io.Copy(part, file)

    for key, val := range params {
        _ = writer.WriteField(key, val)
    }
    err = writer.Close()
    if err != nil {
        return UploadFileRes{}, err
    }

    request, err := http.NewRequest("POST", uri, body)
    if err != nil {
        log.Fatal(err)
        return UploadFileRes{}, err
    }
    request.Header.Add("Content-Type", writer.FormDataContentType())
    client := &http.Client{}
    resp, err := client.Do(request)
    if err != nil {
        log.Fatal(err)
        return UploadFileRes{}, err
    } else {
        defer resp.Body.Close()
        body, err := ioutil.ReadAll(resp.Body)
        // body := &bytes.Buffer{}
        // _, err := body.ReadFrom(resp.Body)
        if err != nil {
            log.Fatal(err)
        }

        // fmt.Println(resp.StatusCode)
        // fmt.Println(resp.Header)
        // fmt.Println(body.Bytes())

        var uploadFileRes UploadFileRes
        json.Unmarshal(body, &uploadFileRes)
        return uploadFileRes, nil
    }
    return UploadFileRes{}, fmt.Errorf("lib throwe")
}

func main() {

    extraParams := map[string]string{}
    res, err := newfileUploadRequest("http://192.168.1.56:1502/uploadeFile", extraParams, "file", "/home/koala/project/tmp/images/1.png")
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(res)

    sep := string(os.PathSeparator)
    fmt.Println(sep)
}

type UploadFileRes struct {
    ErrCode string `json:"errCode"`
    ErrMsg  string `json:"errMsg"`
    Data    string `json:"data"`
}

17 - RSA-encryption

csdn_private.pem

-----BEGIN csdn_privateKey-----
MIIEpAIBAAKCAQEAv5so1PIs1jaJPJ7fOQBUAzpIqf8kZ7vCETs9sGXCs2XzqwH4
sYyzPcU9NY5CBMMtev9bW0oPyETQCsaS5HtbpgHwRPqRfbcSWZXkodUUUgSNP2ss
/74tMjw4tnx4kxW/EmsZnGJGKuyY7Tf5IlI7QD6U9nWWRKAEsKSbyd/7H3HzLxoB
xZsMbXXI3MgERzJcsmacWPnVG98i7QH0oSu+a7zSse5zSrW/RtKQj8WtAOMVZ7FP
x4fqofj6JGXot4SFBkwSgnPD2613PSPMQn6/G2dHUEJqmqtJr4j8ptP+1T6tWpy0
BVm/WhbkN9ymCOlds+SGMkyeCUs6daBJilQ9VQIDAQABAoIBADDsj2qAQ86WskgW
UO0fFlSUp0Uw7rzGBnGb7M6DzUk9eRBrOnMreAEHwe9Q2a6Zn51OYqdWq9z5JR37
Qjqw/N/QkucqC8hL3JWfXnesDro6i05sMVtD1gqDsf92nNsBrH4pdqqltUD0lL/N
kQGgeZyX3jVoJOx0532rKlRLqrWGU5acSosZqTTuCQEjBH94mA1KTvLy0CqZWyWo
yZ/OBtF61TxXa5qejJl4MJ8050UV8T7tqkckK9h4bColVS58VvnYaKST5bf33YiY
sAbE7/o8B9NW/3ogwJG6yvjXnSnICOLNUofb26zftW9DrcYGmLMnsPtm8Tj2xPLj
7zAjbekCgYEA8CZNYs72yDxJjvewvYmeWSkAO9goVBHQSSyMJcgdhiWG4IkZbaAo
1Mrvs3LBIOY1VMr2OjopzwSupNvKqOokLZ+2zcijvqDmv8AgZdh79JdaRVqqXf83
Zvun6RD1Par8vIfNIWPV/ruiM0QFcgbpd3VshAOJbqLv0QVb3Lb/f5MCgYEAzECj
uf1HKCgguRDvajG8aImES+xWdXubyA0yakS6xiBt9Pyr5sNkcum3FDIynj9/i/ke
MrK/4PtewwYwemWpccWf+EAXhQhj7bqKC5TsQr3gu9eQ+A9RVOGJ7WBJca9VY8q6
7jAWf/UlEx8WPxaV11jtvchzA1erwmMwafLGUHcCgYEAwf9PGHj0psD88z9oSVT4
1DHo/G8b9P4G8nXIKWVFZG7ATHa0UfjFw1DE3oPfPAJ8Jqlmy5bc211+77KWPmoX
G7wf4pEopgA5J8G+6kc9q1LxG4GoixJ24Px+oiqO0mhkjrBtp4GNB6Dv4NYcSAcJ
ZvU22lY5GWUKsiHQGbbDI30CgYEAx9h7GdCaXc0db1YFmrb9LJ9YpVyhn6OI4a0f
5eBHivFSBMFwhIIrd0/7xLP02OcyKcdeZ6aDnWL17gXRSwDLULlXcvNqz8xM0d6R
kRFuNUNJbyFVA5EhN9bROEPcuHIgL1q9ma3NZfd7BgGFp8a2Z5ToUKee+Oc/9BtO
1Gso5LMCgYBZKa3mZp1nmWHn7zlD+HeP/MACs34ZgaR33dGIx9j5jO8s5Gw98wJg
Rfjvuf73Yq9T7VhlJp6mZ+fDVuhrbBlI32Itrb+p7FztIx3rVBXjvvuhqOFDupYl
UttiUvkrWuoTKfbPhKZkKVDYp+fm/dd6JBa+c0+2kuwo6tm/Yr3kbw==
-----END csdn_privateKey-----

csdn_PublicKey.pem

-----BEGIN csdn_PublicKey-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv5so1PIs1jaJPJ7fOQBU
AzpIqf8kZ7vCETs9sGXCs2XzqwH4sYyzPcU9NY5CBMMtev9bW0oPyETQCsaS5Htb
pgHwRPqRfbcSWZXkodUUUgSNP2ss/74tMjw4tnx4kxW/EmsZnGJGKuyY7Tf5IlI7
QD6U9nWWRKAEsKSbyd/7H3HzLxoBxZsMbXXI3MgERzJcsmacWPnVG98i7QH0oSu+
a7zSse5zSrW/RtKQj8WtAOMVZ7FPx4fqofj6JGXot4SFBkwSgnPD2613PSPMQn6/
G2dHUEJqmqtJr4j8ptP+1T6tWpy0BVm/WhbkN9ymCOlds+SGMkyeCUs6daBJilQ9
VQIDAQAB
-----END csdn_PublicKey-----
package main

import (
    "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "encoding/hex"
    "encoding/pem"
    "fmt"
    "os"
)

func Getkeys() {
    //得到私钥
    privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
    //通过x509标准将得到的ras私钥序列化为ASN.1 的 DER编码字符串
    x509_Privatekey := x509.MarshalPKCS1PrivateKey(privateKey)
    //创建一个用来保存私钥的以.pem结尾的文件
    fp, _ := os.Create("csdn_private.pem")
    defer fp.Close()
    //将私钥字符串设置到pem格式块中
    pem_block := pem.Block{
        Type:  "csdn_privateKey",
        Bytes: x509_Privatekey,
    }
    //转码为pem并输出到文件中
    pem.Encode(fp, &pem_block)

    //处理公钥,公钥包含在私钥中
    publickKey := privateKey.PublicKey
    //接下来的处理方法同私钥
    //通过x509标准将得到的ras私钥序列化为ASN.1 的 DER编码字符串
    x509_PublicKey, _ := x509.MarshalPKIXPublicKey(&publickKey)
    pem_PublickKey := pem.Block{
        Type:  "csdn_PublicKey",
        Bytes: x509_PublicKey,
    }
    file, _ := os.Create("csdn_PublicKey.pem")
    defer file.Close()
    //转码为pem并输出到文件中
    pem.Encode(file, &pem_PublickKey)

}

//使用公钥进行加密
func RSA_encrypter(path string, msg []byte) []byte {
    //首先从文件中提取公钥
    fp, _ := os.Open(path)
    defer fp.Close()
    //测量文件长度以便于保存
    fileinfo, _ := fp.Stat()
    buf := make([]byte, fileinfo.Size())
    fp.Read(buf)
    //下面的操作是与创建秘钥保存时相反的
    //pem解码
    block, _ := pem.Decode(buf)
    //x509解码,得到一个interface类型的pub
    pub, _ := x509.ParsePKIXPublicKey(block.Bytes)
    //加密操作,需要将接口类型的pub进行类型断言得到公钥类型
    cipherText, _ := rsa.EncryptPKCS1v15(rand.Reader, pub.(*rsa.PublicKey), msg)
    return cipherText
}

//使用私钥进行解密
func RSA_decrypter(path string, cipherText []byte) []byte {
    //同加密时,先将私钥从文件中取出,进行二次解码
    fp, _ := os.Open(path)
    defer fp.Close()
    fileinfo, _ := fp.Stat()
    buf := make([]byte, fileinfo.Size())
    fp.Read(buf)
    block, _ := pem.Decode(buf)
    PrivateKey, _ := x509.ParsePKCS1PrivateKey(block.Bytes)
    //二次解码完毕,调用解密函数
    afterDecrypter, _ := rsa.DecryptPKCS1v15(rand.Reader, PrivateKey, cipherText)
    return afterDecrypter
}

func main() {

    // Getkeys()

    //尝试调用
    msg := []byte("RSA非对称加密很棒")
    ciphertext := RSA_encrypter("csdn_PublicKey.pem", msg)
    //转化为十六进制方便查看结果
    fmt.Println(hex.EncodeToString(ciphertext))
    result := RSA_decrypter("csdn_private.pem", ciphertext)
    fmt.Println(string(result))

}

18 - stdin-stdout-stderr

  1. print
package main

import (
    "fmt"
)

func main() {
    v1 := "123"
    v2 := 123
    v3 := "Have a nice day\n"
    v4 := "abc"
    fmt.Print(v1, v2, v3, v4)
    fmt.Println()
    fmt.Println(v1, v2, v3, v4)
    fmt.Print(v1, " ", v2, " ", v3, " ", v4, "\n")
    fmt.Printf("%s%d %s %s\n", v1, v2, v3, v4)
}
  1. stderr
package main

import (
    "io"
    "os"
)

func main() {
    myString := ""
    arguments := os.Args
    if len(arguments) == 1 {
        myString = "Please give me one argument!"
    } else {
        myString = arguments[1]
    }
    io.WriteString(os.Stdout, "This is Standard output\n")
    io.WriteString(os.Stderr, myString)
    io.WriteString(os.Stderr, "\n")
}
  1. stdin
package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    var f *os.File
    f = os.Stdin
    defer f.Close()
    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        fmt.Println(">", scanner.Text())
    }
}
  1. stdin-command-line
package main

import (
    "fmt"
    "os"
    "strconv"
)

func main() {
    if len(os.Args) == 1 {
        fmt.Println("Please give one or more floats.")
        os.Exit(1)
    }

    arguments := os.Args
    min, _ := strconv.ParseFloat(arguments[1], 64)
    max, _ := strconv.ParseFloat(arguments[1], 64)
    for i := 2; i < len(arguments); i++ {
        n, _ := strconv.ParseFloat(arguments[i], 64)
        if n < min {
            min = n
        }
        if n > max {
            max = n
        }
    }
    fmt.Println("Min:", min)
    fmt.Println("Max:", max)
}
  1. stdout
package main

import (
    "io"
    "os"
)

func main() {
    myString := ""
    arguments := os.Args
    if len(arguments) == 1 {
        myString = "Please give me one argument!"
    } else {
        myString = arguments[1]
    }
    io.WriteString(os.Stdout, myString)
    io.WriteString(os.Stdout, "\n")
}

19 - UDP简单的示例


package main

import (
    "context"
    "encoding/hex"
    "fmt"
    "log"
    "net"
    "time"
)

func main() {
    // 本质上udp不存在传统意义上的服务端和客户端, 他其实是无连接的。
    // 但是实际使用中, 总要有一方去监听数据, 另一方去连接所以才有了 一个服务 一个客户

    // 创建一个监听服务
    // conn, err := net.Dial("udp", ":30000")
    // conn, err := net.DialUDP("udp", &net.UDPAddr{
    //     IP:   net.ParseIP("0.0.0.0"),
    //     Port: 45103,
    // }, &net.UDPAddr{
    //     IP:   net.ParseIP("127.0.0.1"),
    //     Port: 30000,
    // })

    // 连接已经创建的服务
    conn, err := net.DialUDP("udp", nil, &net.UDPAddr{
        IP:   net.ParseIP("127.0.0.1"),
        Port: 30000,
    })

    if err != nil {
        log.Fatalf("连接建立错误: %s", err.Error())
        return
    }
    defer conn.Close()

    //7F06FE01993FFF3C16
    //7F32F601143801001F0250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D63130381411111501893E
    //7F32F60115384000200250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D631303814111115012F09

    // go func() {
    // 创建一个计时器
    timeTickerChan := time.Tick(time.Second * 2)
    ctx, cancel := context.WithTimeout(context.Background(), time.Duration(time.Millisecond*800))
    defer cancel()
    for {
        select {
        case <-timeTickerChan:
            input, err := hex.DecodeString("7F06FE01993FFF3C16")
            if err != nil {
                fmt.Printf("输入值解码错误:%s\n", err.Error())
                continue
            }

            //客户端请求数据写入 conn,并传输
            conn.WriteToUDP([]byte(input), &net.UDPAddr{
                IP:   net.ParseIP("10.10.10.4"),
                Port: 8887,
            })
            writeCount, err := conn.Write([]byte(input))
            if err != nil {
                log.Printf("数据发送错误:%s\n", err.Error())
                continue
            }
            log.Printf("输入值:%s; 发送量: %d\n", string(input), writeCount)

            go func(ctx context.Context) {
                //服务器端返回的数据写入空buf
                buf := make([]byte, 1024)
                // conn.ReadFrom()
                // conn.ReadFromUDP()
                cnt, serverAddr, err := conn.ReadFrom(buf)
                if err != nil {
                    log.Printf("客户端读取数据失败 %s\n", err)
                    return
                }
                //回显服务器端回传的信息
                log.Printf("服务器端回复: %s; 服务端信息:%s", hex.EncodeToString(buf[0:cnt]), serverAddr.String())
            }(ctx)

            select {
            case <-ctx.Done():
                fmt.Println("call successfully!!!")
                continue
            case <-time.After(time.Duration(time.Millisecond * 900)):
                fmt.Println("timeout!!!")
                continue
            }

            // input, _ = hex.DecodeString("7F32F601143801001F0250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D63130381411111501893E")
            // log.Printf("输入值:%v\n", input)
            // //客户端请求数据写入 conn,并传输
            // conn.Write([]byte(input))
            // //服务器端返回的数据写入空buf
            // cnt, err = conn.Read(buf)
            // if err != nil {
            //     log.Printf("客户端读取数据失败 %s\n", err)
            //     continue
            // }
            // //回显服务器端回传的信息
            // log.Printf("服务器端回复" + hex.EncodeToString(buf[0:cnt]) + "\n")

            // input, _ = hex.DecodeString("7F32F60115384000200250000100000000000000000000000000000000323030B7C2D8BB353030FABBF7D631303814111115012F09")
            // log.Printf("输入值:%v\n", input)
            // //客户端请求数据写入 conn,并传输
            // conn.Write([]byte(input))
            // //服务器端返回的数据写入空buf
            // cnt, err = conn.Read(buf)
            // if err != nil {
            //     log.Printf("客户端读取数据失败 %s\n", err)
            //     continue
            // }
            // //回显服务器端回传的信息
            // log.Printf("服务器端回复" + hex.EncodeToString(buf[0:cnt]) + "\n")
        }
    }

    // }()

}

20 - websocket-client

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/url"
    "sync"
    "time"

    "github.com/gorilla/websocket"
)

var mutex sync.Mutex

func main() {
    // u := url.URL{Scheme: "ws", Host: "8.136.204.163:8000"}
    u := url.URL{Scheme: "ws", Host: "localhost:8001"}
    c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
    if err != nil {
        log.Fatalf("%+v", err)
    }
    defer c.Close()
    // c.SetCloseHandler(func(code int, text string) error {
    //     fmt.Printf("SetCloseHandler %d:%s", code, text)
    //     // message := websocket.FormatCloseMessage(code, "")
    //     // c.WriteControl(CloseMessage, message, time.Now().Add(writeWait))
    //     return nil
    // })
    // c.SetPingHandler(func(appData string) error {
    //     fmt.Printf("SetPingHandler %s", appData)
    //     return nil
    // })
    // c.SetPongHandler(func(appData string) error {
    //     fmt.Printf("SetPingHandler %s", appData)
    //     return nil
    // })

    pingParam := make(map[string]interface{})
    pingParam["msgId"] = 3
    pingParam["action"] = "Heartbeat"
    pingData := make(map[string]interface{})
    pingData["echo"] = "ping"
    pingParam["data"] = pingData

    param := make(map[string]interface{})
    param["msgId"] = 2
    param["action"] = "Login"
    data := make(map[string]interface{})
    data["authName"] = "taian"
    data["authToken"] = "7ee193f504125d7d9e715e0252648856"
    data["clientName"] = "taian123"
    param["data"] = data
    mutex.Lock()
    c.WriteJSON(param)
    mutex.Unlock()

    // res := make(map[string]interface{})
    // err1 := c.ReadJSON(&res)
    // if err1 != nil {
    //     fmt.Printf("%+v\n", err1)
    //     log.Fatalln(err1)
    // }
    // bs, _ := json.Marshal(res)
    // log.Printf("%s\n", string(bs))

    // mutex.Lock()
    // err0 := c.WriteJSON(pingParam)
    // mutex.Unlock()
    // if err0 != nil {
    //     log.Fatalf("写ping命令ERR:%+v\n", err1)
    // }

    // res = make(map[string]interface{})
    // err1 = c.ReadJSON(&res)
    // if err1 != nil {
    //     log.Fatalf("读ping命令ERR: %+v\n", err1)
    // }
    // bs, _ = json.Marshal(res)
    // log.Printf("%s\n", string(bs))

    go func() {
        t := time.NewTicker(time.Second * 4)
        defer t.Stop()
        for {
            <-t.C
            mutex.Lock()
            c.WriteJSON(pingParam)
            mutex.Unlock()
        }
    }()

    go func() {
        for {
            res := make(map[string]interface{})
            err1 := c.ReadJSON(&res)
            if err1 != nil {
                fmt.Printf("%+v\n", err1)
                continue
            }
            bs, _ := json.Marshal(res)
            log.Printf("%s\n", string(bs))
        }

    }()

    // param["action"] = "GetDevice"
    // data = make(map[string]interface{})
    // data["index"] = 1
    // data["count"] = -1

    param0 := make(map[string]interface{})
    param0["msgId"] = 4
    param0["action"] = "RegDeviceData"
    data0 := make(map[string]interface{})
    data0["names"] = []string{"S01"}
    data0["count"] = 1
    param0["data"] = data0

    mutex.Lock()
    err0 := c.WriteJSON(param0)
    mutex.Unlock()
    if err0 != nil {
        log.Fatalf("%+v", err0)
    }

    param1 := make(map[string]interface{})
    param1["msgId"] = 4
    param1["action"] = "GetRealDeviceData"
    data1 := make(map[string][]string)
    data1["names"] = []string{"S01"}
    param1["data"] = data1

    mutex.Lock()
    err1 := c.WriteJSON(param1)
    mutex.Unlock()
    if err1 != nil {
        log.Fatalf("%+v", err1)
    }
    select {}
}

21 - xorm连接mssql

go.mod

require (
    github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
    xorm.io/xorm v1.0.2
)

main.go

package main

import (
    "flag"
    "fmt"
    "time"

    _ "github.com/denisenkom/go-mssqldb"
    "xorm.io/xorm"
)

type Park struct {
    ID   string `xorm:"Id"`
    Name string `xorm:"Name"`
}

var (
    debug         = flag.Bool("debug", true, "enable debugging")
    password      = flag.String("password", "123456", "the database password")
    port     *int = flag.Int("port", 1433, "the database port")
    server        = flag.String("server", "10.10.10.53", "the database server")
    user          = flag.String("user", "sa", "the database user")
)

func main() {

    connString := fmt.Sprintf("server=%s;user id=%s;password=%s;port=%d;database=ed;encrypt=disable", *server, *user, *password, *port)
    engine, err := xorm.NewEngine("mssql", connString)
    //控制台打印SQL语句

    if err != nil {
        fmt.Println(err)
        return
    }
    defer engine.Close()
    engine.SetConnMaxLifetime(120 * time.Second)
    engine.SetMaxOpenConns(16)
    engine.SetMaxIdleConns(8)

    err = engine.Ping()
    if err != nil {
        fmt.Println(err)
        return
    }
    var sv []Park
    engine.ShowSQL(true)

    // err = engine.SQL("SELECT TOP 1000 id,CardCname FROM Dt_CardType order by id asc").Find(&sv)
    engine.Find(&sv)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(len(sv))
}

22 - 遍历字符串

package main

import "fmt"

func main() {
    var str = "Hello北京&"

    //1. 按index遍历,这种方式按照字节遍历,对于非ASCII字符会出现乱码。因为"北京"每个字占3个字节
    fmt.Println("按index遍历开始")
    for i := 0; i < len(str); i++ {
        fmt.Printf("%d:%c--", i, str[i])
    }
    fmt.Println("按index遍历结束")

    //2. 用for-range遍历,这种方式是按照字符遍历的,不会出现乱码,但是下标index会出现不连续的情况,它具有不确定性
    fmt.Println("for-range遍历开始")
    for i, ch := range str {
        fmt.Printf("%d:%c--", i, ch)
    }
    fmt.Println("for-range遍历结束")

    //3. 转rune切片遍历
    fmt.Println("转rune切片遍历开始")
    str2 := []rune(str)
    for i := 0; i < len(str2); i++ {
        fmt.Printf("%d:%c--", i, str2[i])
    }
    fmt.Println("\n转rune切片遍历结束")
}

23 - 定时执行

  1. 最简单的定时执行
package main

import (
    "fmt"
    "time"
)

func main() {

    go func() {
        // 创建一个计时器
        timeTickerChan := time.Tick(time.Second * 5)
        for {
            fmt.Println("123")
            <-timeTickerChan
        }
    }()

    fmt.Println(112233)
    //一定要阻止主线程退出定时任务才能有效
    select {}
}
  1. cron定时执行

require github.com/robfig/cron/v3 v3.0.1

package main

import (
    "fmt"
    "github.com/robfig/cron/v3"
    "time"
)

func main() {
    // 新建一个定时任务对象
    // 根据cron表达式进行时间调度,cron可以精确到秒,大部分表达式格式也是从秒开始。
    //crontab := cron.New()  默认从分开始进行时间调度
    crontab := cron.New(cron.WithSeconds()) //精确到秒
    //定义定时器调用的任务函数
    task := func() {
        fmt.Println("hello world", time.Now())
    }
    //定时任务
    spec := "*/5 * * * * ?" //cron表达式,每五秒一次
    // 添加定时任务,
    crontab.AddFunc(spec, task)
    // 启动定时器
    crontab.Start()
    // 定时任务是另起协程执行的,这里使用 select 简答阻塞.实际开发中需要
    // 根据实际情况进行控制
    select {} //阻塞主线程停止
}

24 - 读MySQL数据库所有表并输出见表语句

package main

import (
    "fmt"

     "gorm.io/driver/mysql"
     "gorm.io/gorm"
)

type ShowTablesRes struct {
    // 注意这里也要改数据库名
    TableName string `gorm:"column:Tables_in_${database}"`
}
type CreateRes struct {
    Table       string
    CreateTable string `gorm:"column:Create Table"`
}

func main() {

    dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", "${username}", "${password}", "${ip}:${port}", "${database}")
    // useUnicode=true&characterEncoding=utf8&autoReconnect=true&useSSL=true&serverTimezone=GMT%2B8"
    fmt.Println(dsn)

    db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
    if err != nil {
        panic("failed to connect database")
    }


    var showTablesRes []ShowTablesRes
    db.Raw("show tables").Scan(&showTablesRes)
    // fmt.Println(&showTablesRes)
    for _, v := range showTablesRes {
        // fmt.Printf("%s\n", v.TableName)
        var createRes []CreateRes
        showTableStruct := "show create table " + v.TableName
        db.Raw(showTableStruct).Scan(&createRes)
        fmt.Printf("%s;\n", createRes[0].CreateTable)

    }

}

25 - 链接sqlserver2008

go.mod

require (
    github.com/denisenkom/go-mssqldb v0.0.0-20200620013148-b91950f658ec
    github.com/go-ole/go-ole v1.2.4 // indirect
    github.com/mattn/go-adodb v0.0.1 // indirect
    golang.org/x/net v0.0.0-20200707034311-ab3426394381 // indirect
)
package main

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/denisenkom/go-mssqldb"
)

// var (
//     debug         = flag.Bool("debug", true, "enable debugging")
//     password      = flag.String("password", "123456", "the database password")
//     port     *int = flag.Int("port", 1433, "the database port")
//     server        = flag.String("server", "10.10.10.53", "the database server")
//     user          = flag.String("user", "sa", "the database user")
// )

func main() {
    // var debug = flag.Bool("debug", false, "enable debugging")
    // var password = flag.String("password", "123456", "the database password")
    // var port *int = flag.Int("port", 1433, "the database port")
    // var server = flag.String("server", "10.10.10.53", "the database server")
    // var user = flag.String("user", "sa", "the database user")
    // var database = flag.String("database", "ed", "the database name")

    // if *debug {
    //     fmt.Printf(" password:%s\n", *password)
    //     fmt.Printf(" port:%d\n", *port)
    //     fmt.Printf(" server:%s\n", *server)
    //     fmt.Printf(" user:%s\n", *user)
    // }
    connString := fmt.Sprintf("server=%s;database=%s;user id=%s;password=%s;port=%d;encrypt=disable", "10.10.10.53", "ed", "sa", "123456", 1433)

    fmt.Printf(" connString:%s\n", connString)

    db, err := sql.Open("mssql", connString)
    if err != nil {
        log.Fatal("Open connection failed:", err.Error())
        return
    }

    err = db.Ping()
    if err != nil {
        fmt.Print("PING:%s", err)
        return
    }
    fmt.Println(1)
}

26 - 使用redis

package main

import (
    "fmt"
    "log"
    "math/rand"
    "sync"
    "time"

    "github.com/go-redis/redis"
)

var redisdb *redis.Client
var wg sync.WaitGroup

func main() {
    wg.Add(1)
    go testRedisBase()
    wg.Wait()
}

var map1 map[string]string

func testRedisBase() {
    defer wg.Done()

    //连接服务器
    redisdb = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // use default Addr
        Password: "",               // no password set
        DB:       0,                // use default DB
    })

    //心跳
    pong, err := redisdb.Ping().Result()
    log.Println(pong, err) // Output: PONG <nil>

    result, err := redisdb.BLPop(10*time.Second, "list_test").Result()
    log.Println("result:", result, err, len(result))

    ch := make(chan struct{})
    map1 = make(map[string]string)
    go func() {
        for {
            content, _ := redisdb.BLPop(10*time.Second, "list_test").Result()
            if content != nil {
                ch <- struct{}{}
                map1[content[1]] = content[1]
            }
        }
    }()

    for {
        <-ch
        for k, v := range map1 {
            log.Println(k, ":", v)
        }
        if _, ok := map1["1"]; ok {
            log.Println(map1["1"])
        }
        delete(map1, "1")
        for k, v := range map1 {
            log.Println(k, ":", v)
        }
    }

    // ExampleClient_String()
    // ExampleClient_List()
    // ExampleClient_Hash()
    // ExampleClient_Set()
    // ExampleClient_SortSet()
    // ExampleClient_HyperLogLog()
    // ExampleClient_CMD()
    // ExampleClient_Scan()
    // ExampleClient_Tx()
    // ExampleClient_Script()
    // ExampleClient_PubSub()
}

func ExampleClient_String() {
    log.Println("ExampleClient_String")
    defer log.Println("ExampleClient_String")

    //kv读写
    err := redisdb.Set("key", "value", 1*time.Second).Err()
    log.Println(err)

    //获取过期时间
    tm, err := redisdb.TTL("key").Result()
    log.Println(tm)

    val, err := redisdb.Get("key").Result()
    log.Println(val, err)

    val2, err := redisdb.Get("missing_key").Result()
    if err == redis.Nil {
        log.Println("missing_key does not exist")
    } else if err != nil {
        log.Println("missing_key", val2, err)
    }

    //不存在才设置 过期时间 nx ex
    value, err := redisdb.SetNX("counter", 0, 1*time.Second).Result()
    log.Println("setnx", value, err)

    //Incr
    result, err := redisdb.Incr("counter").Result()
    log.Println("Incr", result, err)
}

func ExampleClient_List() {
    log.Println("ExampleClient_List")
    defer log.Println("ExampleClient_List")

    //添加
    log.Println(redisdb.RPush("list_test", "message1").Err())
    log.Println(redisdb.RPush("list_test", "message2").Err())

    //设置
    log.Println(redisdb.LSet("list_test", 2, "message set").Err())

    //remove
    ret, err := redisdb.LRem("list_test", 3, "message1").Result()
    log.Println(ret, err)

    rLen, err := redisdb.LLen("list_test").Result()
    log.Println(rLen, err)

    //遍历
    lists, err := redisdb.LRange("list_test", 0, rLen-1).Result()
    log.Println("LRange", lists, err)

    //pop没有时阻塞
    result, err := redisdb.BLPop(30*time.Second, "list_test").Result()
    log.Println("result:", result, err, len(result))
}

func ExampleClient_Hash() {
    log.Println("ExampleClient_Hash")
    defer log.Println("ExampleClient_Hash")

    datas := map[string]interface{}{
        "name": "LI LEI",
        "sex":  1,
        "age":  28,
        "tel":  123445578,
    }

    //添加
    if err := redisdb.HMSet("hash_test", datas).Err(); err != nil {
        log.Fatal(err)
    }

    //获取
    rets, err := redisdb.HMGet("hash_test", "name", "sex").Result()
    log.Println("rets:", rets, err)

    //成员
    retAll, err := redisdb.HGetAll("hash_test").Result()
    log.Println("retAll", retAll, err)

    //存在
    bExist, err := redisdb.HExists("hash_test", "tel").Result()
    log.Println(bExist, err)

    bRet, err := redisdb.HSetNX("hash_test", "id", 100).Result()
    log.Println(bRet, err)

    //删除
    log.Println(redisdb.HDel("hash_test", "age").Result())
}

func ExampleClient_Set() {
    log.Println("ExampleClient_Set")
    defer log.Println("ExampleClient_Set")

    //添加
    ret, err := redisdb.SAdd("set_test", "11", "22", "33", "44").Result()
    log.Println(ret, err)

    //数量
    count, err := redisdb.SCard("set_test").Result()
    log.Println(count, err)

    //删除
    ret, err = redisdb.SRem("set_test", "11", "22").Result()
    log.Println(ret, err)

    //成员
    members, err := redisdb.SMembers("set_test").Result()
    log.Println(members, err)

    bret, err := redisdb.SIsMember("set_test", "33").Result()
    log.Println(bret, err)

    redisdb.SAdd("set_a", "11", "22", "33", "44")
    redisdb.SAdd("set_b", "11", "22", "33", "55", "66", "77")
    //差集
    diff, err := redisdb.SDiff("set_a", "set_b").Result()
    log.Println(diff, err)

    //交集
    inter, err := redisdb.SInter("set_a", "set_b").Result()
    log.Println(inter, err)

    //并集
    union, err := redisdb.SUnion("set_a", "set_b").Result()
    log.Println(union, err)

    ret, err = redisdb.SDiffStore("set_diff", "set_a", "set_b").Result()
    log.Println(ret, err)

    rets, err := redisdb.SMembers("set_diff").Result()
    log.Println(rets, err)
}

func ExampleClient_SortSet() {
    log.Println("ExampleClient_SortSet")
    defer log.Println("ExampleClient_SortSet")

    addArgs := make([]redis.Z, 100)
    for i := 1; i < 100; i++ {
        addArgs = append(addArgs, redis.Z{Score: float64(i), Member: fmt.Sprintf("a_%d", i)})
    }
    //log.Println(addArgs)

    Shuffle := func(slice []redis.Z) {
        r := rand.New(rand.NewSource(time.Now().Unix()))
        for len(slice) > 0 {
            n := len(slice)
            randIndex := r.Intn(n)
            slice[n-1], slice[randIndex] = slice[randIndex], slice[n-1]
            slice = slice[:n-1]
        }
    }

    //随机打乱
    Shuffle(addArgs)

    //添加
    ret, err := redisdb.ZAddNX("sortset_test", addArgs...).Result()
    log.Println(ret, err)

    //获取指定成员score
    score, err := redisdb.ZScore("sortset_test", "a_10").Result()
    log.Println(score, err)

    //获取制定成员的索引
    index, err := redisdb.ZRank("sortset_test", "a_50").Result()
    log.Println(index, err)

    count, err := redisdb.SCard("sortset_test").Result()
    log.Println(count, err)

    //返回有序集合指定区间内的成员
    rets, err := redisdb.ZRange("sortset_test", 10, 20).Result()
    log.Println(rets, err)

    //返回有序集合指定区间内的成员分数从高到低
    rets, err = redisdb.ZRevRange("sortset_test", 10, 20).Result()
    log.Println(rets, err)

    //指定分数区间的成员列表
    rets, err = redisdb.ZRangeByScore("sortset_test", redis.ZRangeBy{Min: "(30", Max: "(50", Offset: 1, Count: 10}).Result()
    log.Println(rets, err)
}

//用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。
//每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数
func ExampleClient_HyperLogLog() {
    log.Println("ExampleClient_HyperLogLog")
    defer log.Println("ExampleClient_HyperLogLog")

    for i := 0; i < 10000; i++ {
        redisdb.PFAdd("pf_test_1", fmt.Sprintf("pfkey%d", i))
    }
    ret, err := redisdb.PFCount("pf_test_1").Result()
    log.Println(ret, err)

    for i := 0; i < 10000; i++ {
        redisdb.PFAdd("pf_test_2", fmt.Sprintf("pfkey%d", i))
    }
    ret, err = redisdb.PFCount("pf_test_2").Result()
    log.Println(ret, err)

    redisdb.PFMerge("pf_test", "pf_test_2", "pf_test_1")
    ret, err = redisdb.PFCount("pf_test").Result()
    log.Println(ret, err)
}

func ExampleClient_PubSub() {
    log.Println("ExampleClient_PubSub")
    defer log.Println("ExampleClient_PubSub")
    //发布订阅
    pubsub := redisdb.Subscribe("subkey")
    _, err := pubsub.Receive()
    if err != nil {
        log.Fatal("pubsub.Receive")
    }
    ch := pubsub.Channel()
    time.AfterFunc(1*time.Second, func() {
        log.Println("Publish")

        err = redisdb.Publish("subkey", "test publish 1").Err()
        if err != nil {
            log.Fatal("redisdb.Publish", err)
        }

        redisdb.Publish("subkey", "test publish 2")
    })
    for msg := range ch {
        log.Println("recv channel:", msg.Channel, msg.Pattern, msg.Payload)
    }
}

func ExampleClient_CMD() {
    log.Println("ExampleClient_CMD")
    defer log.Println("ExampleClient_CMD")

    //执行自定义redis命令
    Get := func(rdb *redis.Client, key string) *redis.StringCmd {
        cmd := redis.NewStringCmd("get", key)
        redisdb.Process(cmd)
        return cmd
    }

    v, err := Get(redisdb, "NewStringCmd").Result()
    log.Println("NewStringCmd", v, err)

    v, err = redisdb.Do("get", "redisdb.do").String()
    log.Println("redisdb.Do", v, err)
}

func ExampleClient_Scan() {
    log.Println("ExampleClient_Scan")
    defer log.Println("ExampleClient_Scan")

    //scan
    for i := 1; i < 1000; i++ {
        redisdb.Set(fmt.Sprintf("skey_%d", i), i, 0)
    }

    cusor := uint64(0)
    for {
        keys, retCusor, err := redisdb.Scan(cusor, "skey_*", int64(100)).Result()
        log.Println(keys, cusor, err)
        cusor = retCusor
        if cusor == 0 {
            break
        }
    }
}

func ExampleClient_Tx() {
    pipe := redisdb.TxPipeline()
    incr := pipe.Incr("tx_pipeline_counter")
    pipe.Expire("tx_pipeline_counter", time.Hour)

    // Execute
    //
    //     MULTI
    //     INCR pipeline_counter
    //     EXPIRE pipeline_counts 3600
    //     EXEC
    //
    // using one rdb-server roundtrip.
    _, err := pipe.Exec()
    fmt.Println(incr.Val(), err)
}

func ExampleClient_Script() {
    IncrByXX := redis.NewScript(`
        if redis.call("GET", KEYS[1]) ~= false then
            return redis.call("INCRBY", KEYS[1], ARGV[1])
        end
        return false
    `)

    n, err := IncrByXX.Run(redisdb, []string{"xx_counter"}, 2).Result()
    fmt.Println(n, err)

    err = redisdb.Set("xx_counter", "40", 0).Err()
    if err != nil {
        panic(err)
    }

    n, err = IncrByXX.Run(redisdb, []string{"xx_counter"}, 2).Result()
    fmt.Println(n, err)
}

27 - 支持RabbitMQ的AMQP

package main

import (
    "log"

    "github.com/streadway/amqp"
)

func failOnError(err error, msg string) {
    if err != nil {
        log.Fatalf("%s: %s", msg, err)
    }
}

func main() {
    // 连接 RabbitMQ
    conn, err := amqp.Dial("amqp://admin:admin@127.0.0.1:5673")
    failOnError(err, "连接失败")
    defer conn.Close()

    // 建立一个 channel ( 其实就是TCP连接 )
    ch, err := conn.Channel()
    failOnError(err, "打开通道失败")
    defer ch.Close()

    // 创建一个名字叫 "hello" 的队列
    q, err := ch.QueueDeclare(
        "hello", // name
        false,   // durable
        false,   // delete when unused
        false,   // exclusive
        false,   // no-wait
        nil,     // arguments
    )
    failOnError(err, "创建队列失败")

    // 构建一个消息
    body := "Hello World!"
    msg := amqp.Publishing{
        ContentType: "text/plain",
        Body:        []byte(body),
    }

    // 构建一个生产者,将消息 放入队列
    err = ch.Publish(
        "",     // exchange
        q.Name, // routing key
        false,  // mandatory
        false,  // immediate
        msg)
    log.Printf(" [x] Sent %s", body)
    failOnError(err, "Failed to publish a message")
}