本文主要介绍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: 图片过大可下载预览>

参考

前言

游戏排行榜是一个常见需求,今天主要介绍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
清空后查询排行榜成员-升序 []

参考

go语言提供非常方便的创建轻量级的协程goroutine来并发处理任务,但是 协程过多影响程序性能,所以,这时goroutine池就登场了。本文简要介绍ants goroutine 池的基本的使用方法。

简介

ants是一个高性能的 goroutine 池,实现了对大规模 goroutine 的调度管理、goroutine 复用,允许使用者在开发并发程序的时候限制 goroutine 数量,复用资源,达到更高效执行任务的效果。 ants就是一个很多大厂广泛使用的goroute池。

官网地址

https://github.com/panjf2000/ants

功能

  • 自动调度海量的 goroutines,复用 goroutines
  • 定期清理过期的 goroutines,进一步节省资源
  • 提供了大量有用的接口:任务提交、获取运行中的 goroutine 数量、动态调整 Pool 大小、释放 Pool、重启 Pool
  • 优雅处理 panic,防止程序崩溃
  • 资源复用,极大节省内存使用量;在大规模批量并发任务场景下比原生 goroutine 并发具有更高的性能
  • 非阻塞机制

quick start

使用 ants v1 版本:

go get -u github.com/panjf2000/ants

使用 ants v2 版本 (开启 GO111MODULE=on):

go get -u github.com/panjf2000/ants/v2

接下来看一下官方demo:实现一个计算大量整数和的程序

NewPool

NewPool生成ants池实例。

package main

import (
	"fmt"
	"sync"
	"time"

	"github.com/panjf2000/ants/v2"
)

var sum int32

func demoFunc() {
	time.Sleep(10 * time.Millisecond)
	fmt.Println("Hello World!")
}

func main() {
	defer ants.Release()

	runTimes := 1000

	//使用公共池。
	var wg sync.WaitGroup
	syncCalculateSum := func() {
		demoFunc()
		wg.Done()
	}
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = ants.Submit(syncCalculateSum)
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", ants.Running())
    fmt.Printf("finish all tasks.\n")
 }

其中:

生成一个具有特定函数的ants池实例

  • NewPool(size int, options …Option) (*PoolWithFunc, error)

args.size 即池容量,即池中最多有 10 个 goroutine。 arg.Option 定制化 goroutine pool.

  • p.Submit向此池提交任务。
  • ants.Release关闭此池并释放工作队列。
  • defaultAntsPool 导入ants时初始化实例池。

NewPoolWithFunc

package main

import (
	"fmt"
	"github.com/panjf2000/ants/v2"
	"sync"
	"sync/atomic"
)

var sum int32

func myFunc(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	fmt.Printf("run with %d\n", n)
}

func main() {
	defer ants.Release()
	runTimes := 1000
	var wg sync.WaitGroup
	//使用带有函数的池
//设置goroutine池的容量为10,过期时间为1秒。
	p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		myFunc(i)
		wg.Done()
	})
	defer p.Release()
	//逐个提交任务。
	for i := 0; i < runTimes; i++ {
		wg.Add(1)
		_ = p.Invoke(int32(i))
	}
	wg.Wait()
	fmt.Printf("running goroutines: %d\n", p.Running())
	fmt.Printf("finish all tasks, result is %d\n", sum)
}

OutPut:

run with 1
run with 5
run with 11
run with 12
run with 13
run with 14
run with 7
...
run with 789
run with 809
running goroutines: 10
finish all tasks, result is 499500

其中:

生成一个具有特定函数的ants池实例

  • NewPoolWithFunc(size int, pf func(interface{}), options …Option) (*PoolWithFunc, error)

args.pf 即为执行任务的函数

优雅处理 panic

测试一些当任务触发panic的情况

func myFunc(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	if n%2 == 0 {
		panic(any(fmt.Sprintf("panic from task:%d", n)))
	}
	fmt.Printf("run with %d\n", n)
}

output:

run with 3
run with 13
...
run with 999
2022/10/21 21:41:05 worker with func exits from a panic: panic from task:6
2022/10/21 21:41:05 worker with func exits from a panic: panic from task:0
2022/10/21 21:41:07 worker with func exits from panic: goroutine 14 [running]:

可以看到,main routine 没有因此受影响。

Options

// Options包含实例化ants池时将应用的所有选项。
type Options struct {
	// ExpiryDuration is a period for the scavenger goroutine to clean up those expired workers,
	// the scavenger scans all workers every `ExpiryDuration` and clean up those workers that haven't been
	// used for more than `ExpiryDuration`.
	ExpiryDuration time.Duration

	// PreAlloc indicates whether to make memory pre-allocation when initializing Pool.
	PreAlloc bool

	// Max number of goroutine blocking on pool.Submit.
	// 0 (default value) means no such limit.
	MaxBlockingTasks int

	// When Nonblocking is true, Pool.Submit will never be blocked.
	// ErrPoolOverload will be returned when Pool.Submit cannot be done at once.
	// When Nonblocking is true, MaxBlockingTasks is inoperative.
	Nonblocking bool

	// PanicHandler is used to handle panics from each worker goroutine.
	// if nil, panics will be thrown out again from worker goroutines.
	PanicHandler func(interface{})

	// Logger is the customized logger for logging info, if it is not set,
	// default standard logger from log package is used.
	Logger Logger
}

比如 PanicHandler 遇到 panic会调用这里设置的处理函数,以上例中,我们遇到偶数会触发panic,修改NewPool函数

p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		myFunc(i)
		wg.Done()
	}, ants.WithPanicHandler(func(i interface{}) {
		fmt.Printf("panic recover %v", i)
	}))

out:

panic recover panic from i:992run with 880
panic recover panic from i:880run with 972
panic recover panic from i:972run with 994
run with 991

还有更多关于 Benchmarks 性能报告,可参考https://github.com/panjf2000/ants

参考:

前后端数据常用传输格式有:json、xml 和 proto等,不管是mvc还是ddd,都会在表现层的对不同的格式进行转化下层依赖所需的类、Object、 aggregate等。

Kratos 表现层

在实际开发场景中,前后端传输采用json。 但kratos使用proto定义Api, 那么我们以一个Http请求为例,来看看kratos的表现层是如何处理的。

以下是一个Kratos-Example,由编写好的proto文件,proto-go 等自动生成表现层的代码。 大概步骤:

  1. 编写proto,包括Service,Req,Reply
  2. 生成表现层代码
  3. 实现下层逻辑等

Example完整代码如下

func _Greeter_SayHello0_HTTP_Handler(srv GreeterHTTPServer) func(ctx http.Context) error {
	return func(ctx http.Context) error {
		var in HelloRequest
		if err := ctx.BindQuery(&in); err != nil {
			return err
		}
		if err := ctx.BindVars(&in); err != nil {
			return err
		}
		http.SetOperation(ctx, "/helloworld.v1.Greeter/SayHello")
		h := ctx.Middleware(func(ctx context.Context, req interface{}) (interface{}, error) {
			return srv.SayHello(ctx, req.(*HelloRequest))
		})
		out, err := h(ctx, &in)
		if err != nil {
			return err
		}
		reply := out.(*HelloReply)
		return ctx.Result(200, reply)
	}
}

可以看到:

  1. 这里对具体的格式无感,输入In还是输出Out都是一个proto生成的类。
  2. 具体的实现交给了ctx 上下文去实现 查看 ctx.Result的源码:
func (c *wrapper) Result(code int, v interface{}) error {
	c.w.WriteHeader(code)
	return c.router.srv.enc(&c.w, c.req, v)
}

这里的enc,没有特殊设置,使用默认的DefaultResponseEncoder,部分源码如下

func DefaultResponseEncoder(w http.ResponseWriter, r *http.Request, v interface{}) error {
	...
	codec, _ := CodecForRequest(r, "Accept")
	data, err := codec.Marshal(v)
	if err != nil {
		return err
	}
	w.Header().Set("Content-Type", httputil.ContentType(codec.Name()))
	_, err = w.Write(data)
    ...
}

通过codc接口隔离具体实现,调用接口的 codec.Marshal序列化。

codec包

Codec定义Transport用于编码和解码消息的接口。它的实现包括 json,xml,proto,yaml等

type Codec interface {
	// Marshal returns the wire format of v.
	Marshal(v interface{}) ([]byte, error)
	// Unmarshal parses the wire format into v.
	Unmarshal(data []byte, v interface{}) error
	// Name returns the name of the Codec implementation. The returned string
	// will be used as part of content type in transmission.  The result must be
	// static; the result cannot change between calls.
	Name() string
}

这里我们聚焦正在使用的Json

type codec struct{}
func (codec) Marshal(v interface{}) ([]byte, error) {
	switch m := v.(type) {
	case json.Marshaler:
		return m.MarshalJSON()
	case proto.Message:
		return MarshalOptions.Marshal(m)
	default:
		return json.Marshal(m)
	}
}

这里Marshal()使用第三方库““google.golang.org/protobuf/encoding/protojson”

实现proto转json。

protojson第三方包

protojson是Google提供的proto和json的转化

package api_test

import (
	"google.golang.org/protobuf/encoding/protojson"
	"testing"

	v1 "helloworld/api/helloworld/v1"
)

func TestToJson(t *testing.T) {
	reply := &v1.HelloReply{
		Message: "Jobs",
	}
	replyJson, err := MarshalOptions.Marshal(reply.ProtoReflect().Interface())
	if err != nil {
		t.Errorf("Marshal Error: %v", err)
	}
	t.Logf("replyJson:  %v", string(replyJson))
}

更多的接口请参考“https://google.golang.org/protobuf/encoding/protojson”

小结

本文主要以kratos的表现层的组织方式,来介绍常用的框架表现层处理方式。 其中:

  • kratos通过proto来生成表现层代码的类,包括http和grapc,内部对具体实现无感。
  • kratos通过解码器codec实现不同传输格式的解耦。
  • 第三方包protojson实现proto和json转换。

框架的主要功能之一就是标准化处理一下公共的问题场景,从源码中可以学习:通过接口隔离具体的实现,而使框架与具体实现解耦,且具备更好的拓展性。

参考

本文主要介绍go如何读取和写入csv文件,以及使用第三方库gocsv转换为Struct。

读取csv

go 标准库 “encoding/csv” 用于读写csv文件。

Reader从csv编码的文件中读取记录

  • type Reader
    • func NewReader(r io.Reader) *Reader
    • func (r *Reader) Read() (record []string, err error) 。
    • func (r *Reader) ReadAll() (records [][]string, err error)

创建一个csv文件servers.csv,具体如下

world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

ReadAll从r中读取所有剩余的记录,每个记录都是字段的切片,成功的调用返回值err为nil而不是EOF。因为ReadAll方法定义为读取直到文件结尾,因此它不会将文件结尾视为应该报告的错误。

package main

import (
	"encoding/csv"
	"fmt"
	"io"
	"os"
)
func main() {
	readeCsvAll() 
}
func readerCsvReadAll() {
	file, err := os.Open("servers.csv")
	defer file.Close()
	if err != nil {
		fmt.Println(err)
	}
	reader := csv.NewReader(file)
	servers, _ := reader.ReadAll()
	fmt.Println("ReadAll:", servers)
}

OutPut:

ReadAll:  [world-svc 1/1 Running 0 44m] [battle-svc 1/1 Running 0 7d]]

Read从r读取一条记录,返回值record是字符串的切片,每个字符串代表一个字段。

reader := csv.NewReader(file)
for {
    servers, err := reader.Read()
    if err == io.EOF {
        break
    } else if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println(servers)
}

输出和ReadAll是一样的。

csv读写逗号分隔值(csv)的文件,亦或可以定义数据的分隔符

reader.Comma = '|' 

亦或可以定义忽略的的行

reader.Comment = '#'

写入csv

Writer类型的值将记录写入一个csv编码的文件

  • type Writer
    • func NewWriter(w io.Writer) *Writer
    • func (w *Writer) Write(record []string) (err error)
    • func (w *Writer) WriteAll(records [][]string) (err error)
    • func (w *Writer) Flush()
    • func (w *Writer) Error() error

向w中写入一条记录,会自行添加必需的引号。记录是字符串切片,每个字符串代表一个字段。

func writerOne() {
	servers := []Server{
		{"world-svc", "1/1", "Running", 0, "44m"},
		{"battle-svc", "1/1", "Running", 0, "7d"},
	}
	file, err := os.Create("serversA.csv")
	defer file.Close()
	if err != nil {
		log.Fatalln("failed to open file", err)
	}
	w := csv.NewWriter(file)
   //将缓存中的数据写入底层的io.Writer。
	defer w.Flush()
	// 使用 Write
	for _, server := range servers {
		row := []string{server.Name, server.Ready, server.State, strconv.Itoa(server.Restarts), server.Age}
		if err := w.Write(row); err != nil {
			log.Fatalln("error writing server to file", err)
		}
	}

执行,在当前目录生成 serversA.csv,内容如下:

world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

WriteAll方法使用Write方法向w写入多条记录,并在最后调用Flush方法清空缓存

	var data [][]string
	for _, server := range servers {
		row := []string{server.Name, server.Ready, server.State, strconv.Itoa(server.Restarts), server.Age}
		data = append(data, row)
	}
	w.WriteAll(data)

To Struct

GoCSV包旨在提供 CSV 和 Go (golang) 值之间的快速和惯用的映射。 已有servers.csv 内容如下

Name,Ready,Status,Restart,Age
world-svc,1/1,Running,0,44m
battle-svc,1/1,Running,0,7d

UnmarshalFile(in *os.File, out interface{}) error UnmarshalFile从接口中的文件解析CSV。

func ToStruct() {
	clientsFile, err := os.OpenFile("servers.csv", os.O_RDWR|os.O_CREATE, os.ModePerm)
	if err != nil {
		panic(err)
	}
	defer clientsFile.Close()

	var servers []*Server
    //转换成 Server Object
	if err := gocsv.UnmarshalFile(clientsFile, &servers); err != nil {  
		panic(err)
	}
	for _, server := range servers {
		fmt.Println("Server", server.Name)
	}
}

OuPut:

Server world-svc
Server battle-svc

更多api使用方式可参考:https ://github.com/gocarina/gocsv

参考

原子操作

原子操作即执行过程不能被中断的操作。在针对某个值的原子操作执行过程当值,cpu绝不会再去执行其他针对该值的操作,无论这些其他操作是否为原子操作。

go-atomic

查看Mutex、RWMutex的源码,底层是通过atomic包中的一些原子操作来实现。

Go标准库 sync/atomic 提供了对基础类型 int32、int64、uint32、uint64、uintptr、Pointer(Add 方法不支持) 的原子级内存操作。其中包括:

  • Add (给第一个参数地址中的值增加一个 delta 值)
  • CompareAndSwap(判断相等即替换))
  • Swap(不需要比较旧值,直接替换,返回旧值)
  • Load(方法会取出 addr 地址中的值)
  • Store( 方法会把一个值存入到指定的 addr 地址中)

我们通过一个例子可以快速了解atomic封装的这些api

package main

import (
	"fmt"
	"sync/atomic"
)

func main() {
	var x int32 = 0

	//func AddInt32(addr *int32, delta int32) (new int32)
	y := atomic.AddInt32(&x, int32(1))
	fmt.Printf("x=%d,y=%d \n", x, y)
	//OutPut: x=1,y=1

	// func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
	// addr 地址里的值是不是 old,如果不等于 old,就返回 false;如果等于 old,就把此地址的值替换成 new 值,返回 true
	isCompare := atomic.CompareAndSwapInt32(&x, int32(0), int32(2))
	fmt.Printf("isCompare=%v,x=%d  \n", isCompare, x)
	//OutPut: isCompare=false,x=1
	//不相等,故x还是1
	isCompare2 := atomic.CompareAndSwapInt32(&x, int32(1), int32(2))
	fmt.Printf("isCompare=%v,x=%d  \n", isCompare2, x)
	//OutPut: isCompare=true,x=2
	//相等,故x还是2

	//func SwapInt32(addr *int32, new int32) (old int32)
	xOld := atomic.SwapInt32(&x, int32(3))
	fmt.Printf("xOld=%d,x=%d  \n", xOld, x)
	//OutPut: xOld=2,x=3
	//不比较,故x替换3

	//func LoadInt32(addr *int32) (val int32)
	vValue := atomic.LoadInt32(&x)
	fmt.Printf("vValue=%d \n", vValue)
	//OutPut: xOld=2,x=3
	//获取x的值3

	//func StoreInt32(addr *int32, val int32)
	atomic.StoreInt32(&x, 8)
	vValue2 := atomic.LoadInt32(&x)
	fmt.Printf("vValue2=%d \n", vValue2)
	//OutPut:vValue2=8 
}

小试牛刀

atomic 比较常见的类型 还提供了一个特殊的类型:Value,但是只支持 load、store。 这里模拟一个场景:当配置变更后,期待其他goroutine可以收到通知和变更。

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

type Config struct {
	Network      string
	Addr         string
	ReadTimeout  int32
	WriteTimeout int32
}

func loadConfig() Config {
	return Config{
		Network:      "redis",
		Addr:         "127.0.0.1:6379",
		ReadTimeout:  60,
		WriteTimeout: 60,
	}
}

var (
	done   bool
	config atomic.Value
)

func main() {
	config.Store(loadConfig())
	var cond = sync.NewCond(&sync.Mutex{})
	go waitForLoad(1, cond)
	go waitForLoad(2, cond)
	go beginLoad(cond)
	select {}
}

func beginLoad(cond *sync.Cond) {
	for {
		time.Sleep(3 * time.Second)
		config.Store(loadConfig())
		cond.Broadcast()
	}
}

func waitForLoad(node int, cond *sync.Cond) {
	cond.L.Lock()
	for {
		if !done {
			cond.Wait()
		}
		c := config.Load().(Config)
		fmt.Printf("node: %d - redis config: %+v\n", node, c)

	}
	cond.L.Unlock()
}

OutPut:

node: 2 - redis config: {Network:redis Addr:127.0.0.1:6379 ReadTimeout:60 WriteTimeout:60}
node: 1 - redis config: {Network:redis Addr:127.0.0.1:6379 ReadTimeout:60 WriteTimeout:60}

uber-go/atomic

uber-go/atomic对标准库进行进一步封装,采用面向对象的使用方式。 这些类型包括 Bool、Duration、Error、Float64、Int32、Int64、String、Uint32、Uint64 等 举个例子uint32的减法你可能是这么写的

atomic.AddUint32(&x, ^(delta - 1))

利用计算机补码的规则,把减法变成加法 。uber-go对它进行了封装

var atom atomic.Uint32
atom.Store(10)
atom.Sub(2)
atom.CAS(20, 1)

小结

本文简要介绍原子操作和go-atomic,使用场景,以及第三库uber-go/atomic。

参考

Go Corntab

本文简要介绍golang 使用crontab实现定时任务。

linux crontab

Linux crontab 是用来定期执行程序的命令。当安装完成操作系统之后,默认便会启动此任务调度命令

命令语法:

crontab [ -u user ] file

crontab [ -u user ] { -l | -r | -e }

*    *    *    *    *
-    -    -    -    -
|    |    |    |    |
|    |    |    |    +----- 星期中星期几 (0 - 6) (星期天 为0)
|    |    |    +---------- 月份 (1 - 12) 
|    |    +--------------- 一个月中的第几天 (1 - 31)
|    +-------------------- 小时 (0 - 23)
+------------------------- 分钟 (0 - 59)

go cron

第三方包“github.com/robfig/cron”来创建 crontab,以实现定时任务

package main

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

func main() {
	var (
		cronS = cron.New()
		spec  = "*/2 * * * * "
		count = 0
	)

	entityID, err := cronS.AddFunc(spec, func() {
		count++
		fmt.Println("count: ", count,"now:", time.Now().Unix())
	})
	if err != nil {
		fmt.Errorf("error : %v", err)
		return
	}
	cronS.Start()

	fmt.Println(entityID)
	defer cronS.Stop()
	select {}
}

go run main.go

输出:

count:  1 now: 1658053860
count:  2 now: 1658053920
count:  3 now: 1658053980
count:  4 now: 1658054040
count:  5 now: 1658054100

可以看到每隔1分钟,执行一次Func,count++

默认情况下标准 cron 规范解析(第一个字段是“分钟”) 可以轻松选择进入秒字段。

cronS = cron.New(cron.WithSeconds())
//注意这里多了一个参数
spec  = "*/2 * * * * * "

执行输出

count:  1 now: 1658053640
count:  2 now: 1658053642
count:  3 now: 1658053644
count:  4 now: 1658053646
count:  5 now: 1658053648 

可以看到每隔两秒执行一次

场景

在项目开发中,需要用到缓存和对一个列表数据分页查询,但由于redis是key-value的存储方式,我们期望的使用类似postgresql的offset和limit,不至于需要一个个key遍历过去。

设计

分析一下我们需求,那么需求需要实现的接口大概是:

FindByPage(ctx context.Context, page, size int) ([]Object, error)

大致我们需要解决两个问题:

  • 存储对象
  • 列表分页快速查找

对象存储

Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表,hash 特别适合用于存储对象。

  • HDEL key field1 [field2]删除一个或多个哈希表字段
  • HEXISTS key field查看哈希表 key 中,指定的字段是否存在。
  • HGET key field获取存储在哈希表中指定字段的值。
  • HGETALL key获取在哈希表中指定 key 的所有字段和值

如此,我们可以 redis hash 和 json.Encoding 来存储。 获得一个Object

//【ObjectID】= "{"foo": "123", "bar":"456"}"

import  "github.com/go-redis/redis/v8"

result, err := rdb.HGet(ctx, "key", id.String()).Result()
if err != nil {
     return
}
var obj = &YourObject{}
err = json.Unmarshal([]byte(result), obj)
if err != nil {
     return
}

增加一个Object:

ob, err := json.Marshal(Object{}) 
	err = repo.data.rdb.HSet(ctx, "key", ob.ID, ob).Err()
	if err != nil {
		return err
	}

分页

我们的需求中,ObjectID是一个唯一的int,那么可以使用 zset

ZXyy redis 有序集合的基本命令。

  • ZADD key score1 member1 [score2 member2]向有序集合添加一个或多个成员,或者更新已存在成员的分数
  • ZCARD key获取有序集合的成员数
  • ZRANGE key start stop [WITHSCORES]通过索引区间返回有序集合指定区间内的成员

那么可以把 zset 的key 和 value 都设置为 ObjectID 增加一个Object的代码:

if err := rdb.ZAdd(ctx, "key", &redis.Z{
     Score:  float64(Object.ID),
     Member: strconv.Itoa(int(Object.ID)),
}).Err(); err != nil {
     return err
}

获取X分页的代码:

var (
     start = int64((page - 1) * size)
     end   = start + int64(size)
)
result, err := rdb.ZRange(ctx, "key", start, end).Result()
if err != nil {
     return
}

测试

我们增加N个Object:

[1 a]
[2 b]
[3 c]
[11 aa]
[12 bb]
[13 cc]
[21 aaaa]

每页的长度为4,获取第一页

[1 2 3 11]

第二页:

[12 13 21]

第三页:

[ ]

参考

本文主要介绍 jinzhu/copier 的使用和常用场景

简介

在go后端项目开发中,内部rpc服务返回的字段跟api服务相差无几,一个个赋值比较费事儿。那么就需要Object、List、HashMap 等进行值拷贝。jinzhu/copier即提供了这些场景的支持。

copier 特性

  • 从方法复制到具有相同名称的字段
  • 从字段复制到具有相同名称的方法
  • 从一个切片复制到另一个切片
  • 从结构体复制到切片
  • 从map复制到map
  • 强制复制带有标记的字段
  • 忽略带有标记的字段
  • 深拷贝

Usage

Copy from struct

package main

import (
	"github.com/jinzhu/copier"
	"testing"
)

type User struct {
	Name         string
	Role         string
	Age          int32
	EmployeeCode int64 `copier:"EmployeeNum"` // specify field name

	// Explicitly ignored in the destination struct.
	Salary int
}

//目标结构体中的标签提供了copy指令。复制忽略
//或强制复制,如果字段没有被复制则惊慌或返回错误。
type Employee struct {
	//告诉copier。如果没有复制此字段,则复制到panic。
	Name string `copier:"must"`

	//告诉copier。 如果没有复制此字段,则返回错误。
	Age int32 `copier:"must,nopanic"`

	// 告诉copier。 显式忽略复制此字段。
	Salary int `copier:"-"`

	DoubleAge  int32
	EmployeeId int64 `copier:"EmployeeNum"` // 指定字段名
	SuperRole  string
}

func TestCopyStruct(t *testing.T) {
	var (
		user     = User{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 200000}
		employee = Employee{Salary: 150000}
	)
	copier.Copy(&employee, &user)
	t.Logf("%#v \n", employee)
}

output:

    copier_test.go:47: main.Employee{Name:"Jinzhu", Age:18, Salary:150000, DoubleAge:36, EmployeeId:0, SuperRole:"Super Admin"} 

Copy from slice to slice

func TestCopySlice(t *testing.T) {
	var (
		users     = []User{{Name: "Jinzhu", Age: 18, Role: "Admin", Salary: 100000}, {Name: "jinzhu 2", Age: 30, Role: "Dev", Salary: 60000}}
		employees = []Employee{}
	)
	employees = []Employee{}
	copier.Copy(&employees, &users)

	t.Logf("%#v \n", employees)
}`

output :

    copier_test.go:57: []main.Employee{main.Employee{Name:"Jinzhu", Age:18, Salary:0, DoubleAge:36, EmployeeId:0, SuperRole:"Super Admin"}, main.Employee{Name:"jinzhu 2", Age:30, Salary:0, DoubleAge:60, EmployeeId:0, SuperRole:"Super Dev"}} 

Copy from Map to Map

func TestCopyMap(t *testing.T) {
	// Copy map to map
	map1 := map[int]int{3: 6, 4: 8}
	map2 := map[int32]int8{}
	copier.Copy(&map2, map1)

	t.Logf("%#v \n", map2)
}

output :

    copier_test.go:66: map[int32]int8{3:6, 4:8} 

场景 1(rpc&api)

实际开发中,免不了服务间通讯,比较前文所说的场景,一个内部的rpc服务返回的参数和api服务差不多,那么就可以使用copier。

//伪代码如下
func ApiLogin(ctx context.Context,request *api.LoginRequest)(reply *api.LogingReply,err error)  {
	grpcClient := v1.NewGameGrpcClient(ctx)
	reply, err := client.Login(ctx, &grpc.api.LoginRequest{ 
					})
	user := api.LogingReply.User{}
 	copier.Copy(&user, reply.User())
return &api.LoginReply{
	User:user,
},err

场景 2 (model-object/aggregate)

实际开发中,不管是mvc\ddd 都会有从model到object/aggreate的repository,那么就可以使用copier。

func (r *UserRepo) Get(ctx context.Context, uid int64) (u User,err error) {
	model, err := db.User.Get(ctx, uid)
	if err != nil {
		return
	}
	obj:= User{}
	copy(&obj,model)
	return obj,nil
}

小结

copier提供不同类型之间相同的字段名,使用tag或者方法支持不同的字段名的赋值。减少一些重复的工作量,小巧实用。

参考

jefffff

Stay hungry. Stay Foolish COOL

Go backend developer

China Amoy