gorilla/mux一个良好质量的Http路由器和Url匹配器,很好补充net/http包HTTP协议复杂的路由请求。本文主要记录gorilla的基础使用、和常用功能提取网址参数、路径前缀和子路由器、中间件。

基础使用

安装

go get -u github.com/gorilla/mux

创建路由器

通过 mux.NewRouter() 创建路由器,稍后将作为参数传递给服务器。它将接收所有 HTTP 连接并将其传递给您将在其上注册的请求处理程序。

package main

import (
	"fmt"
	"github.com/gorilla/mux"
	"net/http"
)

func main() {
	//创建一个新的路由器
	r := mux.NewRouter()

	//等效于http.HandleFunc()工作原理
	r.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "foo")
	})
	//等效于http.Handler()工作原理
	handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("bar")
		fmt.Fprintf(w, "bar")
	}))
	r.Handle("/foo", handler)
    //作为服务参数传入
   http.ListenAndServe(":80", r) 
  }  
  //请求http://yourip:80/foo  http://yourip:80/bar 
  //output
  //foo bar 

网址参数

gorilla/mux的优势之一是:从url中提前参数,例如一个restful api:/country/{country}/province/{province} ,从request参数

oneHander := func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		c := vars["country"]
		p := vars["province"]
		fmt.Printf("Welcome to  %s %s\n", c, p)
		fmt.Fprintf(w, "Welcome to  %s %s\n", c, p)
	}
	//URL 中提取段/变量
	r.HandleFunc("/country/{country}/province/{province}", oneHander)
    //请求 http://yourip/country/china/province/fujian   
    //output
    //Welcome to china fujian

路径前缀和子路由器

有些场景将请求处理程序限制为特定路径前缀,比如api、admin、cheat 或 v1、v2、v3 或 beta 、prod。

	allHanler := func(w http.ResponseWriter, r *http.Request) {
		vars := mux.Vars(r)
		c := vars["country"]
		p := vars["province"]
		fmt.Fprintf(w, "Welcome to %s %s\n", c, p)
	}
	//路径前缀和子路由器
	countryRouter := r.PathPrefix("/country").Subrouter()
	countryRouter.HandleFunc("/", allHanler)
	countryRouter.HandleFunc("/{province}", oneHander)
    //请求 http://yourip/country/china  
    //output
    //Welcome to china

中间件

Mux 支持向Router添加中间件,如果找到匹配项,中间件将按照添加的顺序执行,包括其子路由器。

func Middleware1(r *mux.Router) mux.MiddlewareFunc {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
			fmt.Println("do something before handler")
			next.ServeHTTP(w, req)
			fmt.Println("do something after handler")
		})
	}
}

	//中间件
	middleware1 := Middleware1(r)
	r.Use([]mux.MiddlewareFunc{middleware1}...)
    //请求 http://yourip/country/china/province/fujian   
    //output
    //do something before handler
    //Welcome to  china fujian
    //do something after handler

参考

写在前面

随着分布式系统的快速发展和广泛应用,如何处理共享资源的互斥访问场景呢?即分布式锁,常见采用有开源的 MySQL,Redis,ZooKeeper,Etcd 等三方组件来实现。今天主要介绍Golang基于Redis实现分布式互斥锁。

分布式锁特性

那么分布式锁需要处理哪些问题场景呢,或者说特性有哪些? 这是redis.io的原文:

安全和活力保证:

  1. 安全特性:互斥。在任何给定时刻,只有一个客户端可以持有一把锁。
  2. 活性属性 A:无死锁。最终总是有可能获得锁,即使锁定资源的客户端崩溃或分区。
  3. 活性属性 B:容错性。只要大多数 Redis 节点正常运行,客户端就可以获取和释放锁

结合实际场景,翻译一下:

  1. 互斥性。锁的基础特性,只能被第一个持有者持有;
  2. 防死锁。在持有方发生异常时,设置锁的超时释放机制,规避死锁风险
  3. 高可用、高性能。

go-redsyn

Redsync 为 Go 提供了基于 Redis 的分布式互斥锁实现 安装:

$ go get github.com/go-redsync/redsync/v4

例子:

package main

import (
	goredislib "github.com/go-redis/redis/v8"
	"github.com/go-redsync/redsync/v4"
	"github.com/go-redsync/redsync/v4/redis/goredis/v8"
)

func main() {
	//使用go-redis(或redigo)创建一个池,该池是redisync will
//在与Redis通信时使用这也可以是任何一个池子
//实现' redis. properties '池的接口。
	client := goredislib.NewClient(&goredislib.Options{
		Addr: "localhost:6379",
	})
    
    //redis集群
//client := goredislib.NewClusterClient(&goredislib.ClusterOptions{        //Addr: []string{"localhost:6379"},    })

	pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)

	//创建redisync实例,用于获得互斥锁。
	rs := redsync.New(pool)

	//为所有需要互斥对象的实例使用相同的名称来获取一个新的互斥对象
//相同的锁。
	mutexname := "my-global-mutex"
	mutex := rs.NewMutex(mutexname)

	//获取互斥锁在这个成功之后,没有其他人
//可以获得相同的锁(相同的互斥锁名),直到我们解锁它。
	if err := mutex.Lock(); err != nil {
		panic(err)
	}

	//做你需要锁的工作。
    
//释放锁,以便其他进程或线程可以获得锁。
	if ok, err := mutex.Unlock(); !ok || err != nil {
		panic("unlock failed")
	}
}

实现原理

加锁

在New一个互斥锁mutex后,通过mutex.Lock()加锁,查看源码实现:

func (m *Mutex) acquire(ctx context.Context, pool redis.Pool, value string) (bool, error) {
	conn, err := pool.Get(ctx)
	if err != nil {
		return false, err
	}
	defer conn.Close()
	reply, err := conn.SetNX(m.name, value, m.expiry)
	if err != nil {
		return false, err
	}
	return reply, nil
}

可见,是通过Redis命令Setnx操作来实现。

Redis Setnx(SET if Not eXists) 命令在指定的 key 不存在时,为 key 设置指定的值。设置成功,返回 1 。 设置失败,返回 0 。相对于Set,Setnx具有原子性。

释放锁

func (m *Mutex) release(ctx context.Context, pool redis.Pool, value string) (bool, error) {
	conn, err := pool.Get(ctx)
	if err != nil {
		return false, err
	}
	defer conn.Close()
	status, err := conn.Eval(deleteScript, m.name, value)
	if err != nil {
		return false, err
	}
	return status != int64(0), nil
}

var deleteScript = redis.NewScript(1, `
	if redis.call("GET", KEYS[1]) == ARGV[1] then
		return redis.call("DEL", KEYS[1])
	else
		return 0
	end
`)

可见,Redis使用Lua脚本命令来Delete Key操作。

这里需要关注到Del的时候,需要传入value,value是在加锁时设置的随机唯一值。在删除前进行校验value是否一致,防止误删

锁超时-防死锁

如果持有锁的系统宕机、异常了导致没法release锁,那么其他方就无法活动该锁,导致死锁。

// NewMutex returns a new distributed mutex with given name.
func (r *Redsync) NewMutex(name string, options ...Option) *Mutex {
	m := &Mutex{
		name:   name,
		expiry: 8 * time.Second,
		tries:  32,
		delayFunc: func(tries int) time.Duration {
			return time.Duration(rand.Intn(maxRetryDelayMilliSec-minRetryDelayMilliSec)+minRetryDelayMilliSec) * time.Millisecond
		},
		genValueFunc:  genValue,
		driftFactor:   0.01,
		timeoutFactor: 0.05,
		quorum:        len(r.pools)/2 + 1,
		pools:         r.pools,
	}
	for _, o := range options {
		o.Apply(m)
	}
	return m
}

可见,Mutex.expiy 设置了超时时间,默认为8秒,可以通过Option修改超时时间

// WithExpiry can be used to set the expiry of a mutex to the given value.
func WithExpiry(expiry time.Duration) Option {
	return OptionFunc(func(m *Mutex) {
		m.expiry = expiry
	})
}

Example(设置超时时间、重复加锁):

//设置锁超时3秒
mutex := rs.NewMutex(mutexname,redsync.WithExpiry(time.Second*3)
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
    //重复加锁
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
OutPut

panic: lock already taken, locked nodes: [0]

Example(设置超时时间、超时释放锁):

//设置锁超时10秒
mutex := rs.NewMutex(mutexname,redsync.WithExpiry(time.Second*10)
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
    //11秒后锁超时,释放
    time.Sleep(11 * time.Second)
    //重复加锁但已释放、无异常
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
	if err := mutex.Lock(); err != nil {
		panic(err)
	}
OutPut

总结

本文主要介绍分布式锁的基础概念、特性互斥性、防死锁、高可用高性能。以及golang基于redis,使用go-redsync库实现分布式锁。通过例子简单介绍go-redsync的入门使用,以及通过源码阅读来刨析简介实现原理: 1.利用Redis Setnx原子性实现互斥性 ,2.利用Redis执行lua脚本、检查value值避免误删操作, 3.通过设置ExpiryOption设置超时时间,实现锁超时、避免死锁。

参考

本文简要介绍go实现lint和golangci-lint使用。

what

linter为静态代码检查、分析工具,golang常见的有govet\golint\errcheck 等。通过工具其一,可以提前发现一些语法问题,比如变量作用域问题、数组下标越界、内存泄露等;其二可以根据团队规范定制lint规则,提升可读性,可维护性。

golang的linters比较多,比如 awesome-go-linters中 golint检查变量、函数名、注释等。errcheck为未检查错误等。golangci-lint是一个 Go linters 聚合器,通过配置来选择需要的linters。其特征如下:

  • ⚡非常快:并行运行linters,重用Go构建缓存和缓存分析结果。
  • ⚙️基于yaml的配置
  • 🖥集成VS Code, Sublime Text, GoLand, GNU Emacs, Vim, Atom, GitHub动作。
  • 🥇包含了很多linters,不需要安装它们。
  • 📈由于调优的默认设置导致的最小误报数。
  • 🔥漂亮的输出颜色,源代码行和标记的标识符。

install

mac

macOS你可以使用brew在macOS上安装二进制版本:

brew install golangci-lint
brew upgrade golangci-lint

Install from Source

源码安装:

> go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1

查看安装是否成功:

>golangci-lint --version
>golangci-lint has version v1.50.1 built from (unknown, mod sum: "h1:C829clMcZXEORakZlwpk7M4iDw2XiwxxKaG504SZ9zY=") on (unknown)

how

GolangCI-Lint 可以零配置使用。默认情况下启用以下 linters

>golangci-lint help linters
>Enabled by default linters:
errcheck: Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false, auto-fix: false]
gosimple (megacheck): Linter for Go source code that specializes in simplifying code [fast: false, auto-fix: false]
govet (vet, vetshadow): Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false, auto-fix: false]
ineffassign: Detects when assignments to existing variables are not used [fast: true, auto-fix: false]
staticcheck (megacheck): It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint. [fast: false, auto-fix: false]
typecheck: Like the front-end of a Go compiler, parses and type-checks Go code [fast: false, auto-fix: false]
unused (megacheck): Checks Go code for unused constants, variables, functions and types [fast: false, auto-fix: false]

如何执行golangci-lint呢?在需要执行的项目目录下:

golangci-lint run

这相当于执行

golangci-lint run ./...

您可以选择要分析的目录和文件:

golangci-lint run dir1 dir2/... dir3/file1.go

接着,我们创建一个Example代码:linter_example.go

package main

import "fmt"

type People struct {
	Name string
}

func (p *People) String() string {
	return fmt.Sprintf("print: %v", p)
}

func main() {
	p := &People{}
	p.String()
}


执行lint,如果没有相关的配置文件,会走默认的lint配置

> golangci-lint run
level=warning msg="[runner] The linter 'golint' is deprecated (since v1.41.0) due to: The repository of the linter has been archived b
y the owner.  Replaced by revive."
linter_example.go:5:6: exported type `People` should have comment or be unexported (golint)
type People struct {
     ^
linter_example.go:10:9: printf: fmt.Sprintf format %v with arg p causes recursive (*demos/linter-demo.People).String method call (gove
t)
        return fmt.Sprintf("print: %v", p)
               ^
linter_example.go:15:10: unusedresult: result of (*demos/linter-demo.People).String call not used (govet)
        p.String()
                ^

我们可以根据lint提示修改

 printf: fmt。Sprintf格式%v与arg p导致递归( demos/ lininter -demo. people)。字符串方法调用(govet)。

在使⽤  fmt 包中的打印⽅法时,如果类型实现了这个接⼝,会直接调⽤。⽽中打印  p 的时候会直接调⽤  p 实现的  String() ⽅法,然后就产⽣了循环调⽤。

Custom Configuration

GolangCI-Lint 通过当前工作目录的以下路径中增加配置文件:

  • .golangci.yml
  • .golangci.yaml
  • .golangci.toml
  • .golangci.json

来实现自定义linters

例如我们添加了.golangci.yml

run:
  timeout: 5m
  modules-download-mode: readonly

linters:
  disable-all: true
  enable: # 下面是开启的lint列表
    - errcheck
    - goimports
    - golint
   # - govet 为了测试是否生效,关闭
    - staticcheck

linters-settings:
  golint:
    # minimal confidence for issues, default is 0.8
    min-confidence: 0.8

issues:
  exclude-use-default: false
  max-issues-per-linter: 0
  max-same-issues: 0

这里我们把govet 关闭了,在执行run

golangci-lint run
level=warning msg="[runner] The linter 'golint' is deprecated (since v1.41.0) due to: The repository of the linter has been archived b
y the owner.  Replaced by revive."
linter_example.go:5:6: exported type `People` should have comment or be unexported (golint)
type People struct {
     ^

可见关闭govet生效了。

reference

本文主要介绍ssh免密访问Ubuntu。

前言

What is SSH:

SSH 为 Secure Shell 的缩写,由 IETF 的网络小组(Network Working Group)所制定;SSH 为建立在应用层基础上的安全协议。SSH 是较可靠,专为远程登录会话和其他网络服务提供安全性的协议。利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题

SSH广泛使用的协议,用于安全的访问Linux服务器。

今天我们主要介绍ssh免密访问Ubuntu,其中:

  • 客户端(mac)
  • 服务器(Ubuntu)

实现步骤大致如下:

  1. 安装ssh服务
  2. ssh客户端生成生成公钥和私钥
  3. 上传公钥到服务器

安装ssh的服务端

在Ubuntu安装ssh服务(这里我使用的是vm默认安装):

sudo apt-get install openssh-server

重启服务

service ssh restart

ssh生成密钥

在Mac,生成密钥的命令:

ssh-keygen

生成的密钥路径: $HOME/.ssh 查看是否生成成功:

cd $HOME/.ssh
ls
id_rsa      id_rsa.pub

说明生成成功,其中id_rsa为私钥、id_rsa.pub为公钥。公钥将上传到你要连接的服务器,而私钥则存储在你将用来建立连接的计算机上。接着,将公钥钥上传到Ubuntu:

ssh-copy-id userName@ServerIp

Eg:

ssh-copy-id jefffff@192.168.1.88

接着,查看是否上传成功, 密钥会增加到Ubuntu,路径为$HOME/.ssh/authorized_key

免密访问

接着就可以ssh免密访问服务器了:

ssh -i ~/.ssh/id_rsa -p 22 -l userName ServerIp

Eg::

 [master] ssh -i ~/.ssh/id_rsa -p 22 -l jefffff 192.168.1.88
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-131-generic x86_64)
... 省略一些

*** System restart required ***
Last login: Sat Dec  9 09:21:34 2022 from 192.168.1.33
jeffffff@ubuntu1~$

说明,登入成功。

iterm2配置

为了更方便的访问,可以在iterm2(mac)配置

小结

使用SSH的访问Linux方式有密码和密钥两种方式,使用密钥相对于默认的密码方式优点有: 其一,密码更容易被暴力破解,密钥访问更安全。 其二,亦可通过修改sshd_config配置,来开闭用户对服务器的访问。 文中还通过一个Mac密钥登入Ubuntu的例子,来演示免密访问过程。

参考

本文主要介绍Go实现浅拷贝和深拷贝,以及工具库mohae/deepcopy。

定义

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源

深拷贝是指源对象与拷贝对象互相独立,其中任何一个对象的改动都不会对另外一个对象造成影响。比较典型的就是Value(值)对象,如预定义类型Int32,Double,以及结构(struct),枚举(Enum)等

浅拷贝

go使用copy内置函数将源片中的元素复制到目标切片,如果拷贝的 结构中不包含指针是没问题,否则则数据源和拷贝之间对应指针会共同指向同一块内存。

func shallowCopyReference() {
	brazil := []*Player{{"Neymar"}, {"Messi"}}
	argentina := make([]*Player, len(brazil))
	copy(argentina, brazil)
	fmt.Printf("%p, %p\n", brazil, brazil[0])
	fmt.Printf("%p, %p\n", argentina, argentina[0])
	brazil[0].Name = "Anthony"
	fmt.Printf("%p, %v, %p\n", brazil, brazil[0], brazil[0])
	fmt.Printf("%p, %v, %p\n", argentina, argentina[0], brazil[0])
}
//output
0xc0000412e0, 0xc0000412f0
0xc000041310, 0xc0000412f0
0xc0000412e0, &{Anthony}, 0xc0000412f0
0xc000041310, &{Anthony}, 0xc0000412f0 

如上,修改了src(拷贝原始结构)的指针对象,dst(拷贝目标结构)的值也被改动了,显然是不行的。

深拷贝

方法一:gob、json 序列化成字节序列再反序列化生成克隆对象

  • gob序列化
func deepCopyByGobExample() {
	players := append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})
	brazil := &Team{Players: players}
	argentina := &Team{}
	err := deepCopyByGob(brazil, argentina)
	if err != nil {
		fmt.Errorf("deepcopy error:", err)
	}
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}

func deepCopyByGob(dst, src *Team) error {
	var buf bytes.Buffer
	if err := gob.NewEncoder(&buf).Encode(src); err != nil {
		return err
	}
	return gob.NewDecoder(bytes.NewBuffer(buf.Bytes())).Decode(dst)
}
//output
brazil,&{Neymar}, 0xc000184ff0
argentina,&{Neymar}, 0xc000185330
brazil, &{Anthony}, 0xc000184ff0
argentina, &{Neymar}, 0xc000184ff0 
  • json序列化
func deepcopyByJsonExample() {
	players := append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})
	brazil := &Team{Players: players}
	argentina := &Team{}
	err := deepCopyByJson(brazil, argentina)
	if err != nil {
		fmt.Errorf("deepcopy error:", err)
	}
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}

func deepCopyByJson(src, dst *Team) error {
	if tmp, err := json.Marshal(src); err != nil {
		return err
	} else {
		err = json.Unmarshal(tmp, dst)
		return err
	}
}
//output
brazil,&{Neymar}, 0xc0000412e0
argentina,&{Neymar}, 0xc000041490
brazil, &{Anthony}, 0xc0000412e0
argentina, &{Neymar}, 0xc0000412e0

可以看到deepcopy出来的指针地址不一样,那么都dst的修改就不会影响到src了。

方法二:使用第三方工具库

DeepCopy对事物进行深度复制:未导出的字段值不会被复制。 feature:

  • copy slice
  • copy map
func deepCopyExample() {
	brazil := &Team{Players: append([]*Player{}, &Player{Name: "Neymar"}, &Player{Name: "Messi"})}
	argentinaIfc := deepcopy.Copy(brazil)
	argentina := argentinaIfc.(*Team)
	fmt.Printf("%v,%v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v,%v, %p\n", "argentina", argentina.Players[0], argentina.Players[0])
	brazil.Players[0].Name = "Anthony"
	fmt.Printf("%v, %v, %p\n", "brazil", brazil.Players[0], brazil.Players[0])
	fmt.Printf("%v, %v, %p\n", "argentina", argentina.Players[0], brazil.Players[0])
}
//output
brazil,&{Neymar}, 0xc0000412e0
argentina,&{Neymar}, 0xc000041330
brazil, &{Anthony}, 0xc0000412e0
argentina, &{Neymar}, 0xc0000412e0

小结

浅拷贝是值的拷贝,如果属性是引用,拷贝的就是内存地址,需要使用深拷贝。深拷贝的实现原理本质上是通过反射实现。通过将源对象转换成接口,再对接口通过反射判断其类型。所以可以通过序列化成gob、json等来实现。工具库mohae/deepcopy可以对切片、map、结构体、接口进行深拷贝,是个不错的选择。

参考

比较常见pprof 可以把调用栈可视化成调用图,embedded-struct-visualizer 可以把Go 的项目的代码分层结构和依赖都可视化成流程图。

安装 embedded-struct-visualizer

 go install github.com/davidschlachter/embedded-struct-visualizer@latest

查看命令选项参数

embedded-struct-visualizer -h
Usage: [OPTIONS] DirToScan
If the directory to scan is not provided, it defaults to './'
OPTIONS:
  -out <file>  path to output file (default: write to stdout)
  -v           verbose logging

Example

以官方给的例子 main.go

package main

import (
	"time"
)

type A struct {
	B
	C map[string]D
}

type B struct {
	E, F  string
	G     string
	Timer H
}

type D struct {
	I uint64
}

type H struct {
	Timer time.Ticker
	J     chan D
}

生成结构关系:

> embedded-struct-visualizer
digraph {
"main.A" -> { "main.B" "main.D" };
"main.B" -> { "main.H" };
"main.H" -> { "time.Ticker" "main.D" };
}

生成结构关系 并 输出到.gv 文件:

>embedded-struct-visualizer -out .\example.gv
> ls

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2022/11/27     10:54            112 example.gv
-a----        2022/11/26     22:06             31 go.mod
-a----        2022/11/26     22:07            208 main.go

项目目录下多出了example.gv ,具体内容如下

cat .\example.gv
digraph {
"main.A" -> { "main.B" "main.D" };
"main.B" -> { "main.H" };
"main.H" -> { "time.Ticker" "main.D" };

安装 graphviz 

graphviz 是一种将结构信息表示为抽象图和网络的图的工具。

接着,把生成的gv文件利用graphviz绘制的更美观。

首先下载安装:

https://graphviz.org/download/ 查看是否安装成功(Win):

 dot -v
dot - graphviz version 7.0.2 (20221119.0110)
... <为了不篇幅过长,省略一些细节>

利用graphviz绘图

dot -Tformat[:renderer[:formatter]] -o 将输出语言设置为支持的格式之一。默认情况下,生成带属性的点。

接着把上一步生成的gv文件,生成PNG输出

dot -Tpng example.gv -o example.png

在项目目录下多出example.png

接着,实用于一个相对复杂的Gin Engine的gv

  1. 源码链接: https://github.com/gin-gonic/gin/blob/master/gin.go

  2. 生成的gv:

embedded-struct-visualizer -out .\gin.gv

gin.gv 详细如下:

digraph {"gin.mockWriter" -> { "http.Header" };"binding.QueryTest" -> { "binding.appkey" };"binding.FooBarStruct" -> { "binding.FooStruct" };"binding.FooBarFileStruct" -> { "binding.FooBarStruct" 
"multipart.FileHeader" };"binding.FooBarFileFailStruct" -> { "binding.FooBarStruct" 
"multipart.FileHeader" };"binding.FooDefaultBarStruct" -> { "binding.FooStruct" };"binding.FooStructUseNumber" -> { "binding.any" };"binding.FooStructDisallowUnknownFields" -> { "binding.any" 
};"binding.FooBarStructForTimeType" -> { "time.Time" };"binding.FooStructForTimeTypeNotUnixFormat" -> { "time.Time" 
};"binding.FooStructForTimeTypeNotFormat" -> { "time.Time" };"binding.FooStructForTimeTypeFailFormat" -> { "time.Time" };"binding.FooStructForTimeTypeFailLocation" -> { "time.Time" 
};"binding.FooStructForMapType" -> { "binding.any" };"binding.FooStructForIgnoreFormTag" -> { "binding.string" };"binding.defaultValidator" -> { "sync.Once" 
"validator.Validate" };"binding.structFull" -> { "binding." "time.Time" 
"binding.string" };"binding.S" -> { "binding.S" };"binding.testFile" -> { "binding.byte" };"binding.structNoValidationValues" -> { 
"binding.substructNoValidation" "binding.int16" 
"binding.uint16" "time.Time" "binding.mapNoValidationSub" 
"binding." };"binding.structNoValidationPointer" -> { 
"binding.substructNoValidation" "binding.uint32" 
"binding.mapNoValidationSub" "binding.int8" "binding.int32" 
"binding.uint8" "binding.uint16" "binding.float64" 
"binding.map" "binding.int" "binding.int16" "binding.int64" 
"binding.float32" "time.Time" "binding.testInterface" 
"binding.uint" "binding.uint64" "binding.string" };"gin.Context" -> { "url.Values" "gin.responseWriter" 
"gin.HandlersChain" "gin.any" "gin.errorMsgs" 
"gin.skippedNode" "sync.RWMutex" "gin.string" 
"http.SameSite" "http.Request" "gin.ResponseWriter" 
"gin.Params" "gin.Engine" };"gin.interceptedWriter" -> { "gin.ResponseWriter" 
"bytes.Buffer" };"gin.Error" -> { "gin.error" "gin.ErrorType" "gin.any" };"gin.onlyFilesFS" -> { "http.FileSystem" };"gin.neuteredReaddirFile" -> { "http.File" };"gin.RouteInfo" -> { "gin.HandlerFunc" };"gin.Engine" -> { "gin.RouterGroup" "gin.string" 
"render.HTMLRender" "gin.HandlersChain" "sync.Pool" 
"gin.methodTrees" "render.Delims" "template.FuncMap" 
"gin.uint16" "net.IPNet" };"gin.LoggerConfig" -> { "gin.LogFormatter" "io.Writer" 
"gin.string" };"gin.LogFormatterParams" -> { "http.Request" "time.Time" 
"time.Duration" "gin.any" };"render.Data" -> { "render.byte" };"render.HTMLProduction" -> { "template.Template" 
"render.Delims" };"render.HTMLDebug" -> { "render.string" "render.Delims" 
"template.FuncMap" };"render.HTML" -> { "template.Template" "render.any" };"render.JSON" -> { "render.any" };"render.IndentedJSON" -> { "render.any" };"render.SecureJSON" -> { "render.any" };"render.JsonpJSON" -> { "render.any" };"render.AsciiJSON" -> { "render.any" };"render.PureJSON" -> { "render.any" };"render.MsgPack" -> { "render.any" };"render.ProtoBuf" -> { "render.any" };"render.Reader" -> { "io.Reader" };"render.Redirect" -> { "http.Request" };"render.String" -> { "render.any" };"render.TOML" -> { "render.any" };"render.XML" -> { "render.any" };"render.YAML" -> { "render.any" };"gin.responseWriter" -> { "http.ResponseWriter" };"gin.RouterGroup" -> { "gin.HandlersChain" "gin.Engine" };"protoexample.Test" -> { "protoexample.int32" 
"protoexample.int64" "protoexample.TestOptionalGroup" 
"protoimpl.MessageState" "protoimpl.SizeCache" 
"protoimpl.UnknownFields" "protoexample.string" };"protoexample.Test_OptionalGroup" -> { 
"protoimpl.MessageState" "protoimpl.SizeCache" 
"protoimpl.UnknownFields" "protoexample.string" };"gin.methodTree" -> { "gin.node" };"gin.node" -> { "gin.node" "gin.HandlersChain" 
"gin.nodeType" };"gin.nodeValue" -> { "gin.HandlersChain" "gin.Params" };"gin.skippedNode" -> { "gin.node" "gin.int16" };}


  1. 绘制png
 dot -Tpng gin.gv -o gin.png

<ps: 图片过大可下载预览>

参考

本文主要学习记录Kubernetes集群暴露服务的方式: Ingress。

简介

Ingress 是对集群中服务的外部访问进行管理的 API 对象,典型的访问方式是 HTTP。Ingress 可以提供负载均衡、SSL 终结和基于名称的虚拟托管。

IngressController 为了让 Ingress 资源工作,集群必须有一个正在运行的 Ingress 控制器。

上图清晰标识出了Ingress的流量走向,其中:

  • Ingress基于DNS名称(host)或URL路径把请求转发⾄指定的Service资源的规则。它仅是⼀组路由规则的集合。
  • Ingress控制器是真正实现“流量穿透”,可以由具有反向代理(HTTP/HTTPS)功能的服务程序实现 , 然后根据这些规则的匹配机制路由请求流量

Ingress 资源声明

Ingress是Kubernetes API的标准资源类型之⼀ ,一个最小Ingress例子:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: minimal-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx-example
  rules:
  - http:
      paths:
      - path: /testpath
        pathType: Prefix
        backend:
          service:
            name: test
            port:
              number: 80

其中:

  • Ingress 需要指定 apiVersion、kind、 metadata和 spec 字段。
  • Ingress 对象的命名必须是合法的 DNS 子域名名称。
  • Ingress annotations 来配置一些选项, ⽤于识别其所属的Ingress控制器的类别。
  • Ingress rules 提供了配置负载均衡器或者代理服务器所需的所有信息。 最重要的是,其中包含与所有传入请求匹配的规则列表。 Ingress 资源仅支持用于转发 HTTP(S) 流量的规则。

更多参考: https://kubernetes.io/zh-cn/docs/concepts/services-networking/ingress/

Ingress Controller

Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,目前支持和维护 AWS、 GCE 和 Nginx Ingress 控制器。

  • Nginx Ingress 作为反向代理和负载均衡器。
  • Apache APISIX Ingress 控制器 是一个基于 Apache APISIX 网关 的 Ingress 控制器。

更多参考:https://kubernetes.io/zh-cn/docs/concepts/services-networking/ingress-controllers/

安装 Ingress Nginx

wget https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.0.0/deploy/static/provider/cloud/deploy.yaml  -O ingress-nginx-deploy.yaml

kubectl apply -f  ingress-nginx-deploy.yaml

观测是否成功:

kubectl get pods -n ingress-nginx --watch

如果成功的话,查看namespace下所有的资源信息

root@master:/home/master# kubectl get all -n ingress-nginx
NAME                                            READY   STATUS    RESTARTS   AGE
pod/ingress-nginx-controller-123456   1/1     Running   0          1h

NAME                                         TYPE           CLUSTER-IP       EXTERNAL-IP    PORT(S)                      AGE
service/ingress-nginx-controller             LoadBalancer   10.104.182.98    192.168.1.100   80:31666/TCP,443:31888/TCP   1d
service/ingress-nginx-controller-admission   ClusterIP      10.111.228.99   <none>         443/TCP                      1d

NAME                                       READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/ingress-nginx-controller   1/1     1            1           1d

NAME                                                  DESIRED   CURRENT   READY   AGE
replicaset.apps/ingress-nginx-controller-123456   1         1         1      1d

NAME                                       COMPLETIONS   DURATION   AGE
job.batch/ingress-nginx-admission-create   1/1           1s         1d
job.batch/ingress-nginx-admission-patch    1/1           2s         1d

使用Ingress 发布 Tomcat

部署Tomcat Service

  1. 创建deployment

        kubectl create deployment web --image=tomcat:8.0.50-jre8-alpine
    
  2. 将 Deployment 暴露出来

    kubectl expose deployment web --type=NodePort --port=8080
    
    
  3. 将 Deployment 暴露出来

    root@master:/home/master# kubectl get service web
        NAME   TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)          AGE
        web    NodePort   10.100.183.22   <none>        8080:32562/TCP   30s
    
  4. 验证nodeport 是否正常访问 tomcat ,浏览器访问 http://matster_ip:32562

  5. 创建 ingress

 apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: ingressfoo
      annotations:
        kubernetes.io/ingress.class: "nginx"
      namespace: default
    spec:
      rules:
      - host: ingressfoo.io
        http:
          paths:
          - backend:
              service:
                name: web
                port:
                  number: 8080
            path: /bar
            pathType: Prefix

查看ingress是否创建成功

root@master:/home/master# kubectl get ingress
NAME         CLASS    HOSTS                     ADDRESS        PORTS   AGE
ingressfoo   <none>   ingressfoo.io   192.168.1.100   80      1h

说明Ingress创建成功,修改hosts :

ingressfoo.io 192.168.1.100

验证访问 ingressfoo.io/bar

success

参考

本文简要介绍Go网络抓包、引流工具GoReplay。

前言

在后端的实际开发中,会遇到以下一些场景:

  • 用户通过作弊手段绕过前端,利用抓包,进行破解,模拟,魔改交互数据进行伪装。后端程序需要对其操作记录进行重现和跟踪。
  • 某个bug在测试环境无法复现。
  • 服务压测数据和线上数据有偏差,压测数据希望能和线上接近 。

GoReplay是Go语言编写的流量回放工具,侦听器服务器捕获http流量并将其发送到重放服务器或保存到文件。重播服务器将流量转发给给定的地址。

安装

测试环境: Windows。 版本: Version:1.3.0 安装分2步骤:

  1. 前置条件: 安装npcap https://npcap.com

GoReplay可以在Windows机器上工作,但由于Windows堆栈的不同网络层的性质,它有一些细节。 默认情况下,Windows不像Unix系统那样有支持包捕获的网络驱动程序,如果您想捕获通信量,则必须单独安装它。其中一个选项是安装https://nmap.org/npcap/。

  1. 安装:这里测试使用的是 gor-1.3.3_windows.zip https://github.com/buger/goreplay/releases 下载解压即可。

捕获流量

启动两个http服务:

  1. httpServerA: http://localhost:8000 监听端口8000
  2. httpServerB: http://localhost:8001 监听端口8001

路由为 “get: “/helloworld/{name}”

监听端口8000,并输出到stdout

 ./gor --input-raw :8000 --output-stdout

打开浏览器请求接口 http://localhost:8000/helloworld/lilei

在gor的命令行窗口:

1 db0a1f4000000001c344a1d3 1668261985986369000 0
GET /helloworld/lilei HTTP/1.1
Host: localhost:8000
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v=" 此处省略一些系数
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
此处省略一些参数

可见,gor已经把整个http请求的完整信息记录下来。 同时gor还支持输出到文件和ElasticSearch,进行回放和分析。记录到文件request.gor

 ./gor --input-raw :8000 --output-file request.gor --input-raw-track-response --input-raw-override-snaplen

打开浏览器请求接口 http://localhost:8000/helloworld/hanmeimei

写入到文件 request_0.gor

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ---- 
-a----        2022/11/12     21:36           1876 request_0.gor

request_0 内容如下:

1 d4571f4000000001b7adc1a6 1668260139796335000 0
GET /helloworld/lilei HTTP/1.1
Host: localhost:8000
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", 此处省略一些参数
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64), 此处省略一些参数
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
此处省略一些参数


🐵🙈🙉
2 d4571f4000000001b7adc1a6 1668260139796652000 0
HTTP/1.1 200 OK
Content-Type: application/json
Date: Sat, 12 Nov 2022 13:35:39 GMT
Content-Length: 25

{"message":"Hello lilei"}
🐵🙈🙉
1 d4571f4000000001b7adc22b 1668260148757949000 0
GET /helloworld/hanmeimei HTTP/1.1
Host: localhost:8000
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", 此处省略一些参数
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) , 此处省略一些参数
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
此处省略一些参数


🐵🙈🙉
2 d4571f4000000001b7adc22b 1668260148758268000 0
此处省略一些参数

{"message":"Hello hanmeimei"}
🐵🙈🙉

流量回放

接着将该文件request_0.gor 进行流量回放,并转发到另外一台服务器(HttpServerB)上:

./gor --input-file request_0.gor --output-http http://localhost:8001

观察HttpServerB服务器控制台输出:

INFO ts=2022-11-12T21:37:26+08:00 caller=greeter.go:44 service.id=LAPTOP-H3HBMV8A service.name= service.version= trace.id= span.id= msg=CreateGreeter: lilei
INFO ts=2022-11-12T21:37:34+08:00 caller=greeter.go:44 service.id=LAPTOP-H3HBMV8A service.name= service.version= trace.id= span.id= msg=CreateGreeter: hanmeimei

从日志中可以看到两个请求(http://localhost:8000/helloworld/ilei、http://localhost:8000/helloworld/hanmeimei)分别重放请求到了HttpServerB。

gor支持进行流量缩小、放大、倍速重放,实现真实流量的压测效果。 更多命令 ./gor -help 查看

参考

本文主要介绍Kubernetes集群资源监控机制和资源指标 前言 临近双11,对于码农,尤其是后端,尤其是某宝某东的后端,那是多么激动人心(心惊胆战)的一夜,为什么?怕宕机呀~。那么就让我们来构建护城河–监控与自动扩容,来抵挡千军万马–高并发场景。首先让我们学习一些基础:k8s集群资源监控机制和资源指标。 分析 从需求出发,我们自然需要收集一些数据(资源指标),再根据指标做一系列的操作(control),比如说预警警告、统计、自动扩容等。 首先,我们希望可以监控整个Kubernetes集群的健康状况,包括: 整个集群的资源利⽤率 集群中的所有⼯作节点是否运⾏正常、系统资源容量⼤⼩ 每个⼯作节点上运⾏的容器化应⽤的数量 k8s资源控制 我们看看k8s如何资源控制 限制节点 以购物平台为例,微服务广受推崇,比如双11当天,用户进首页是流畅的,但是进活动主页就卡顿,到了零点时,加入购物车的按钮都转圈了。设想你的设计可能是三个微服务(主页服务 、双十一活动服务、订单服务),可想而知,活动服务是压力最大的,我们希望,就算宕机,不要影响其他的服务。所以可以把活动服务限制运行在某节点。 创建一个会被调度到特定节点上的 Pod,你也可以通过设置 nodeName 将某个 Pod 调度到特定的节点 nodeName: foo-node # 调度 Pod 到特定的节点 限制内存 还是上面的例子,我们把活动服务调度到了 foo-node。那么剩下的服务没有去限制,但想想订单服务的压力也不小,这里我们希望限制它的资源上限。 要为容器指定内存请求,请在容器资源清单中包含 resources:requests 字段。 同理,要指定内存限制,请包含 resources:limits。 resources: requests: memory: "1000Gi" limits: memory: "1000Gi" 限制CPU 要为容器指定 CPU 请求,请在容器资源清单中包含 resources: requests 字段。 要指定 CPU 限制,请包含 resources:limits resources: limits: cpu: "100" requests: cpu: "100" Example 环境准备: k8s集群(master * 1,node * 2 ) kubectl 命令行工具( 笔者使用rancher) 创建一个namespace(stress)。 增加一个deployment, 修改内存上限为10M 1.

前言

游戏排行榜是一个常见需求,今天主要介绍go语言使用redis-sort-sets来实现排行榜常见功能。

Redis Sort Sets

官方介绍:

Redis排序集是按相关分数排序的惟一字符串(成员)的集合。当多个字符串具有相同的分数时,字符串将按字典顺序排列。排序集的一些用例包括:

游戏排行榜。例如,您可以使用排序集轻松地维护大型在线游戏中最高分数的有序列表。 速率限制器。特别是,您可以使用一个排序集来构建滑动窗口速率限制器,以防止过多的API请求。

常用于排行榜的命令:

  • ZRANGE 返回排序集的成员(升序)
  • ZREVANGE 返回排序集的成员(降序序)
  • ZADD 向排序集添加一个新成员和相关分数。如果成员已经存在,则更新评分。
  • ZREM 删除排序集一个成员
  • ZRANK 返回提供的成员的排名(升序)
  • ZREVRANK 返回提供的成员的排名(降序)

Go Redis

Go Redis为各种风格的Redis提供Go客户端,基础的使用例子如下:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v9"
)

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // use default Addr
		Password: "",               // no password set
		DB:       0,                // use default DB
	})
	pong, err := rdb.Ping(ctx).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(pong, err)
	if err != nil {
		fmt.Println(err)
		return
	}
    defer rdb.Close()
}

Examples

这里我们用go-redis实现排行榜常见的功能,包括:

  • 获取排行榜成员和分数 (升序、降序)
  • 加入排行榜成员
  • 删除排行榜成员
  • 更新排行榜成员分数
  • 获取排行榜成员分数
  • 获取排行榜名次成员
  • 清空排行榜

完整代码如下:

package main

import (
	"context"
	"fmt"
	"github.com/go-redis/redis/v9"
	"math/rand"
	"strconv"
)

func main() {
	ctx := context.Background()
	rdb := redis.NewClient(&redis.Options{
		Addr:     "localhost:6379", // use default Addr
		Password: "",               // no password set
		DB:       0,                // use default DB
	}) 
	var err error
	defer rdb.Close()
	//增加玩家分数的变化
	for i := 1; i <= 5; i++ {
		err = rdb.ZAdd(ctx, "leaderboard", redis.Z{
			Score:  float64(rand.Intn(100)),
			Member: "user:" + strconv.Itoa(i),
		}).Err()
		if err != nil {
			fmt.Println(err)
			return
		}
	}
	if err != nil {
		fmt.Println(err)
		return
	}
	//查询排行榜成员-升序
	members, err := rdb.ZRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员-升序", members)
	//查询排行榜成员和分数-升序
	membersWithScore, err := rdb.ZRevRangeWithScores(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员和分数-升序", membersWithScore)

	//查询排行榜成员-降序
	membersRev, err := rdb.ZRevRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员-降序", membersRev)
	//查询排行榜成员和分数-升序
	membersRevWithScore, err := rdb.ZRangeWithScores(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜成员和分数-升序", membersRevWithScore)

	//获取某玩家的分数
	user2Score, err := rdb.ZScore(ctx, "leaderboard", "user:2").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("获取某玩家 user:2 的分数", user2Score)
	//更新某玩家的分数
	err = rdb.ZAdd(ctx, "leaderboard", redis.Z{
		Score:  user2Score,
		Member: "user:1",
	}).Err()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("更新某玩家 user:1 的分数", user2Score)
	//查询排行榜成员和分数-升序
	membersRevWithScore, err = rdb.ZRangeWithScores(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜变化后的排行榜成员和分数-升序", membersRevWithScore)

	//查询排行榜某玩家的分数-升序
	memberRank, err := rdb.ZRank(ctx, "leaderboard", "user:2").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜某玩家  user:2 的排名", memberRank)

	//查询排行榜某玩家的分数-降序
	memberRevRank, err := rdb.ZRevRank(ctx, "leaderboard", "user:2").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("查询排行榜某玩家 user:2 的排名-降序", memberRevRank)

	//删除排序集一个成员
	zrem, err := rdb.ZRem(ctx, "leaderboard", "user:1").Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("删除排序集一个成员", zrem)

	//删除后查询排行榜成员-升序
	members, err = rdb.ZRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("删除后查询排行榜成员-升序", members)

	//清空排行榜
	clear, err := rdb.ZRemRangeByRank(ctx, "leaderboard", 0, 4).Result()
	if err != nil {
		fmt.Println("err zrem", err)
		return
	}
	fmt.Println("清空排行榜", clear)
	//清空后查询排行榜成员-升序
	members, err = rdb.ZRange(ctx, "leaderboard", 0, 5).Result()
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println("清空后查询排行榜成员-升序", members) 

OutPut:

查询排行榜成员-升序 [user:3 user:4 user:1 user:5 user:2]
查询排行榜成员和分数-升序 [{87 user:2} {81 user:5} {81 user:1} {59 user:4} {47 user:3}]
查询排行榜成员-降序 [user:2 user:5 user:1 user:4 user:3]
查询排行榜成员和分数-升序 [{47 user:3} {59 user:4} {81 user:1} {81 user:5} {87 user:2}]
获取某玩家 user:2 的分数 87
更新某玩家 user:1 的分数 87
查询排行榜变化后的排行榜成员和分数-升序 [{47 user:3} {59 user:4} {81 user:5} {87 user:1} {87 user:2}]
查询排行榜某玩家  user:2 的排名 4
查询排行榜某玩家 user:2 的排名-降序 0
删除排序集一个成员 1
删除后查询排行榜成员-升序 [user:3 user:4 user:5 user:2]
清空排行榜 4
清空后查询排行榜成员-升序 []

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy