Giter VIP home page Giter VIP logo

ekit's People

Contributors

archer-wyz avatar cancan927 avatar chenmingyong0423 avatar cvenwu avatar dxyinme avatar fifth-month avatar flutterwang avatar flycash avatar flyhigher139 avatar heroyf avatar hhh111119 avatar hookokoko avatar jackj-ohn1 avatar johnwongx avatar juniaoshaonian avatar kangdan6 avatar kelipute avatar longyue0521 avatar michaelcheungdk avatar mousseqin avatar mymikasa avatar ogreks avatar sp187 avatar stone-afk avatar tangyumeng avatar uzziahlin avatar weijiadong avatar wureny avatar xxbiji avatar yzicheng avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ekit's Issues

list: 支持 LinkedList

仅限中文

使用场景

在频繁修改 List 结构的场景下, ArrayList 的性能差很多。往往这种情况下,我们都会考虑使用 LinkedList,也就是基于链表的实现。

在这里我们选择使用双向链表,那么在查找的时候可以根据下标位置,决定是从后往前,还是从前往后。

其它

任何你觉得有利于解决问题的补充说明

LinkedList 和 ArrayList 一样,也是提供给基础差的同学练习,所以依旧是一个方法一个合并请求。关键方法,Add, Get, Set, Delete, Append, AsSlice 需要给出基准测试。

在动手实现前,请先阅读 List 接口定义,确认每个方法要满足的约束条件。

不了解 LinkedList 的同学可以自由搜索相关资料,链表算是入门级数据结构

类型定义在 https://github.com/gotomicro/ekit/blob/dev/list/linked_list.go

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

setx: Set 接口设计和基于 map 的实现

仅限中文

使用场景

正常来说,Set 算是一个很常用的数据结构。不过令人惊奇的是 Go 并没有直接提供 Set 的数据结构,所以大多数时候我们都是依赖于使用 map 来间接达成 Set 效果。

当然这并不是什么问题,只是代码 review 的时候有点烦。

所以现在要求你设计一个 Set 接口,并且基于 map 提供一个实现。

要求:

  • 设计基准测试,将基于 map 的实现和直接使用 map 的性能进行比较

要求接口具备方法:

  • 增加元素
  • 删除元素
  • 判断一个元素是否存在
  • 返回所有的元素

按照我一贯的做法,接口的方法都要返回 error,但是我不确定这种习惯在 set 里面是否合适。之前在 Map 的设计与实现里面返回了 error 就有点累赘。

bean/copier: ReflectCopier 实现

仅限中文

使用场景

在实际业务中,我们经常会将系统分成很多层,在不同层会定义不同的实体。例如在 DAO 层有 PO(或者 Entity),在跨端调用的时候有 DTO,在和前端对接的时候有 VO。

大多数情况下,我们需要在这些实体进行转换,例如 DTO 转 PO 等。但是这种代码很呆板,因为仅仅是一个个字段赋值过去,所以在业务代码里面就会充斥着这种转化代码。

因此我们可以考虑设计一个复制器,完成不同类型直接的转换

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

Java BeanUtils

最为有名的应该是 Java 里面提供的 bean utils 依赖,里面支持在 bean 之间进行数据复制。同时 Java 还允许深度定制化复制的行为,例如允许忽略特定的字段,或者指定某个字段映射到另外一个字段。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

反射

也就是我们这里采用的方案,即一个个字段使用反射复制过去。这里要考虑复杂类型字段,以及组合。

代码生成

代码生成技术是另外一种可行的方案。例如说我们有两个结构体:

type SimpleSrc struct {
	Name    string
	Age     *int
	Friends []string
}

type SimpleDst struct {
	Name    string
	Age     *int
	Friends []string
}

为这两个结构体生成方法:

func From(src SimpleSrc) SimpleDst {
     return SimpleDst {
             Name: src.Name
            // ...
    }
}

这种方案的难点在于,SimpleSrc 和 SimpleDst 处于不同的层次。在实际中,它们基本上分属不同的包,因此我们需要额外解决引入包的问题。这是一个很棘手的问题。

其它

任何你觉得有利于解决问题的补充说明

你可以考虑分成好几个合并请求,也可以一个合并请求就支持完。

接口定义和类型定义,以及方法行为特征我都已经定义好了,在:https://github.com/gotomicro/ekit/blob/dev/bean/copier/reflect_copier.go

要求你必须:

  • 完善的测试用例
  • benchmark 测试:简单结构体,组合,或者复杂类型字段都需要有单独的 benchmark 测试

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

slice: FilterDelete

仅限中文

使用场景

近期我遇到了一个新的场景,即在 slice 中删除某些符合条件的元素,那么大体上来说,我需要方法:

func FilterDelete[Src any](src []Src, m func(idx int, src Src) bool) []Src {}

这个实现如果不考虑性能的话倒是蛮简单的,但是如果考虑性能的话,我希望能够做到:不会产生复制,只会把被删除元素之后的元素往前挪。

那么这里会有另外一个问题,就是如果我删除了多个元素,那么我希望对于一个没被删除的元素来说,只会被移动一次。

mapx: HashMap

仅限中文

使用场景

实际上,目前大多数的情况下,内置类型 map 都足够使用了。但是在一些很罕见的情况下,我们并不能使用 map。也就是所谓的如果我们希望用作 key 的类型不是 comparable 的类型。

在这种情况下,我们可以考虑提供一个自己的 HashMap。HashMap 本身有非常成熟的设计和落地方案,我们可以考虑采用其中的一种。

不过在考虑究竟要不要落地的时候,我们需要考虑一个问题:在我们打算提供一个基于树形结构的 Map 的情况下,我们还需要考虑 HashMap 吗?

行业分析

Java HashMap

最容易想到的就是 Java 的 HashMap。Java 的 HashMap 依赖于两个方法:equalshashcode。前者用于判断两个元素是否相等,后者用于生成 hash 值。

Java 的 HashMap 实现稍微有点精巧的地方,就是在它采用了拉链法来解决元素冲突的问题,如果同一个 hash 值对应的元素超过八个,那么这个拉链的链就会进化成为红黑树。

但是 Java 的 HashMap 扩容是一次性的。也就是说在扩容的时候它会直接把元素复制完。作为对比,Go 自带的 map 是渐进式的。

会不会退回来变成链我有点忘记了

Redis 的字典结构

Redis 的字典结构,也是一个 HashMap。它和 Go 的 map 比起来,扩容同样是渐进式的。不过我个人认为,Redis 采用渐进式扩容是比较符合我预期的,因为 Redis 作为一个支持高并发和大数据量的存储结构,渐进式扩容能够将扩容的开销平摊到每一次操作里面。

相比之下,Go 的我认为不是特别必要。

可行方案

在讨论可行方案的时候,我们需要确认:

  • 采用拉链法来解决冲突,这算是经典方案
  • 在拉链法的链较长的时候,要不要转变为树形结构
  • 要不要尝试引入渐进式的扩容
  • 要不要考虑缩容

或者换句话来说,如果我们准备抄一个 HashMap 的实现,抄 Java 的,还是抄 Go 的?或者说两者进行一个折中?

其它

在我们准备提供一个树形结构的 Map 实现的情况下,要不要这么一个 HashMap

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

mapx: LinkedMap 特性

仅限中文

使用场景

在一些场景下,我们需要获得 map 的插入顺序,也就是说我们需要一个类似 Java 中 LinkedHashMap 的结构。

在我们的 ekit 里面,已经有了 imap 的抽象,所以我们并不需要将实现局限在 HashMap 上。

我们可以考虑通过装饰器模式来无侵入式地提供一个新的实现。

type LinkedMap[K any, V any] struct {
    m imap[linkedKey, V]
}

type linkedKey[K any] struct {
    key K
    next *linkedKey
}

类似地,需要暴露两个初始化方法:

func NewLinkedHashMap[K Hashable]()

func NewLinkedTreeMap[ K any, V any]()

为了支持使用 HashMap 作为 imap 的实现,这里需要为 linkedKey 实现 Hashable 接口。这当中可能会有一些泛型方面的问题,遇到了的话就在这下面讨论。

重构randx.RandCode以及传入的RandType数值

仅限中文

需要重构函数 RandCode

当前实现缺陷

无法实现随机生成 数字+大写字母 / 数字 + 小写字母 / 大写字母 + 小写字母 的这种组合

重构方案

TYPE_DIGIT   TYPE = 1 //数字
TYPE_LETTER  TYPE = 1 << 1 //小写字母
TYPE_CAPITAL TYPE = 1 << 2 //大写字母
TYPE_MIXED   TYPE = (TYPE_DIGIT | TYPE_LETTER | TYPE_CAPITAL)

然后可以通过取按位与的值来确认最终的source字符串

source := ""
if (typ & TYPE_DIGIT) == TYPE_DIGIT {
    source += DigitCharset
}
...

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

sqlx: sqlx.NewNullXXX 方法

仅限中文

使用场景

在实践中,支持数据库中取值 NULL 的最佳方式就是使用 sql.NullXXX,但是这时候会面临一个问题,就是实际上对应类型的零值也代表了 NULL。

比如说数据库中存储了一个 NULL VARCHAR,而如果在业务上用的是 string,那么整个 "" 也会被认为是 NULL。

所以很多时候,初始化一个 sql.NullString 的代码就类似:

str := sql.NullString{String: val, Valid: val != ""}

但是这样写太累赘了。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

可以考虑提供 sqlx.NewNullXXX 的方法,这一个系列的方法,会在数据为零值的时候,把 Valid 设置为 false。

func NewNullString(val string) sql.NullString{
return sql.NullString{String: val, Valid: val != ""}
}

针对所有的 NullXXX 类型都提供一个类似的方法。

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

ekit:AnyValue 支持类型转换 - String 转数字类型

仅限中文

使用场景

AnyValue 在缓存等场景下,非常好用,但是还不够好用。那么这里还欠缺了一部分类型转换的 API。但是因为类型之间的转换有很多可能,所以这里暂时支持 String 转数字类型,后续再考虑支持别的。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

在 AnyValue 上定义 As 系列的方法。例如:

func (av AnyValue) AsInt() (int, error) {
	if av.Err != nil {
		return 0, av.Err
	}
	switch v := av.Val.(type) {
	case int:
		return v, nil
	case string:
		res, err := strconv.ParseInt(v, 10, 64)
		return int(res), err
	}
	return 0, errs.NewErrInvalidType("int", reflect.TypeOf(av.Val).String())
}

在这一次中,你只需要支持 string 转:

  • int 家族:int, int8, int16. int32, int64
  • uint 家族: uint, uint8, uint16, uint64
  • float 家族: float32, float64
  • []byte 也顺便转了

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

mapx: HashMap 增加 Delete 方法

仅限中文

删除方法应该算是一个逃不开的功能。Delete 方法将返回被删除的数据,

fun (h *HashMap) Delete(key) (V, bool) {

} 

第一个返回值是被删除的值,第二个值表达是否真的删除了元素。例如如果 key1 在 HashMap 中不存在,那么第二个返回值返回 false。

mapx: MutipleTreeMap

仅限中文

使用场景

在一些场景下,我们需要一个特殊的 map,这个 map 的值是切片。这种在别的框架里面的叫做 MultipleMap,Multiple 指的就是 map 的值不是单一的值,而是一个切片。

基于哪个 Map 来改造

在 ekit 里面有两个 map 的实现,一个是 HashMap,一个是 TreeMap。

理论上来说我们可以为这两个 Map 结构都提供 MutipleMap 的实现。在这种程度下,理论上:

type MultipleMap[K any, V any] struct {
   // 这里是 mapi 的接口
   m mapi[K, []V]
}

这种设计形态意思是对 mapi 进行一个二次封装,来实现 MultipleMap。MultipleMap 会实现 mapi 接口。

注意:任何返回数据的接口,例如说 Get,按照道理来说应该返回值,也就是一个切片,那么这个切片必须是一个副本。也就是说,原本的值是一个切片,你不能直接返回这个切片。因为这个切片会被人篡改掉。

而后暴露两个初始化方法:

func NewMultipleTreeMap[K any, V any](comparator ekit.Comparator) *MultipleMap[K, V] {

}

func NewMultipleHashMap[K Hashable, V any]() *MultipleMap[K, V] {

}

这种设计的好处是:

  • 整体是无侵入式的
  • 后续如果还有 mapi 的实现,这里增加一个初始化方法就可以了。

值是否应该使用集合

在 ekit 里面,值有多种选择。

  • 使用切片
  • 使用 List 结构

目前来说,我认为并没有太大的必要使用 List 结构,切片就足够了。

是否需要允许用户控制切片初始化的容量?

意思是在 Put 方法里面:

vals, ok := m.Get(key)
if !ok {
    vals = make([]V, 0, capacity)
}

这个 capacity 要不要允许用户控制?答案是暂时不需要暴露这个接口。也就是说我们直接无脑 append 就可以了。

这是因为,大部分情况下,值不会有很多元素,我个人用的话是一个元素居多。在元素不多的时候,即便扩容也是极快的。

retry: Strategy 接口设计与等间隔重试实现

仅限中文

使用场景

在很多业务,或者中间件里面我们需要用到重试机制,那么目前来说重试机制的接口设计各家都不同,比如说在 gRPC 里面的重试接口和go-micro 的就不一样。

实际上,这个东西其实犯不着大家都设计一遍的。我在 https://github.com/gotomicro/redis-lock/blob/main/retry.go 这里用过一个重试的接口,我觉得很不错:

type RetryStrategy interface {
	// Next 返回下一次重试的间隔,如果不需要继续重试,那么第二参数返回 false
	Next() (time.Duration, bool)
}

在这种情况下,很容易想到,对于等时间间隔的重试,或者退避重试,或者叠加重试次数控制,都是可以通过两个返回值来控制的。

那么这个接口,缺陷也是很明显的,就是有些重试的实现是通过判断 error 和 context 来决定要不要重试的。那么我们的 Next 方法没有接收任何参数,所以在这种场景之下会有一点问题。

不过话又说回来,这种用法终究是有点奇诡的。那么用户可以考虑自己定义接口,而没有必要使用 ekit 的接口。

所以这一次的目标就是,在 ekit 下面新加一个 retry 包,提供接口:

type Strategy interface {
	// Next 返回下一次重试的间隔,如果不需要继续重试,那么第二参数返回 false
	Next() (time.Duration, bool)
}

同时提供一个等时间间隔重试的实现。注意这个实现直接放在 retry 包下面就可以,以为过于简单,没有必要创建子包了。

等时间间隔要注意:

  • 用户可以控制重试次数。如果最大重试次数是 0 或者负数,那么就是没有限制;
  • 要求并发安全;

syncx: SegmentKeysLock

仅限中文

使用场景

在很多时候,我们需要对 key 进行加锁。但是如果一个 key 一个锁,又很难管理,大概率是需要借助 sync.Map 来实现。

但是我们有一种更加简单的办法来达成类似的效果。

也就是将 key 进行哈希,获得对应的 lock 之后加锁。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

type SegmentKeysLock struct{
   // 理论上来说,不需要使用指针
    locks []sync.RWMutex
}

func (s *SegmentKeysLock) Lock(key string) {
    hash := s.hash(key)
    lock := locks[hash%len(locks)]
    lock.Lock()
}

可以在初始化的时候直接初始化好全部的 locks,这样后续不管是加锁还是解锁,直接操作下标,都是只读的,所以不会有并发问题。

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

copier: ReflectCopier 支持忽略字段

仅限中文

使用场景

copier 包的一个使用场景就是在各种 O 之间的转换。比如说从 BO(business object) 转换到 VO(view object),即业务对象转换为视图对象。

那么我们显然知道,前端并不一定需要 BO 的所有字段。那么他们会因此希望忽略掉部分字段。

但是这种忽略可能是调用级别的,或者说,大多数时候都是调用级别的。

例如说我有两个接口,一个叫做 OrderBrief,返回订单的基本信息,一个叫做 OrderDetail 叫做订单详情。那么我在复制的时候,OrderBrief 会希望忽略掉,但是 OrderDetail 不希望。

所以你需要在调用级别控制住这个行为。

所以现在的 Copy 接口需要支持额外的选项。

可行方案

一个可行的方案是:

type Copier[Src any, Dst any] interface {
	// CopyTo 将 src 中的数据复制到 dst 中
	CopyTo(src *Src, dst *Dst, opts...Option) error
	// Copy 将创建一个 Dst 的实例,并且将 Src 中的数据复制过去
	Copy(src *Src, opts...Option) (*Dst, error)
}

type Option func(*options) {}
type options {
    ignoreFields  []string
}

func IgnoreFields(fields...string) Option {
     return fun(opts *options) {
       opts.ignoreFields = append(opts.ignoreFields, fields...)
    }
}

但是这个方案的缺陷是,引入了 options 这个结构体,它的语义直观,但不是特别直观。

list: ArrayList Delete 方法缩容

仅限中文

使用场景

在一些特殊的场景下,我们可能创建了一个巨大的切片,而后不断删除不断删除之后,可能只剩下很少的一部分元素。在这种情况下,我们现在的 ArrayList 的实现,依赖于切片,所以最终底层那个庞大的数组始终存活着,会浪费一部分内存。

因此我们可以考虑,在 Delete 方法里面支持缩容。

可行方案

缩容的核心问题是,什么时候应该缩容?缩到什么地步?
当已有元素远远少于容量的时候,就应该缩容。缩容的策略是依据切片的扩容策略来决定。例如说目前切片的扩容策略 < 1024 则直接两倍,1024 之后则是 1.25 扩容。

那么相应的缩容就是:

  1. 如果当前容量 <=2048,那么就在已有元素是容量的四分之一的时候,就执行缩容,缩到原本的一半(要防止将来扩容)
  2. 如果当前容量 > 2048,那么在已有元素是容量的 1/2 的时候就可以缩容,缩到已有容量的 5/8

选择 2048 作为阈值,是因为 2048/2 = 1024 恰好是原本扩容的分界点。那么 2048 的 5/8 相比已有的元素数量,还有两百多个位置,后续即便不断增加元素,引起扩容的概率也小很多。

缩容还有另外一个问题。当容量小于一个固定阈值的时候,不需要继续缩容了。这个阈值,我认为 16,32, 64 都可以接受。因为即便不缩容,浪费的空间也不会很多。

此外,这两个阈值要对用户完全透明,即用户不允许控制这两个参数,后续我们可以进一步调研行业标准,来调整这些阈值。

其它

任何你觉得有利于解决问题的补充说明

如果你对缩容策略有什么想法,也可以在下面补充

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

mapx: Keys, Values 和 KeysValues 方法

仅限中文

使用场景

很简单,日常情况下,我们经常会遇到获得一个 map 的所有 key 或者所有 values 的场景,所以我们可以考虑提供三个方法:

func Keys[K comparable, V any](m map[K]V) []K {

}

func Values[K comparable, V any](m map[K]V) []V {

}

func KeysValues[K comparable, V any](m map[K]V) []K, []V {

}

唯一要注意的是,Keys 和 Values 要自己内部循环组织数据,而不能调用 KeysValues 以避免内存分配。

此外,如果输入是 nil 的情况下,返回空切片而不是返回 nil。(当然,这是一个偏好问题,你如果觉得返回 nil 更好也可以,只不过大多数时候 nil 有更大几率引起 panic 而已)

TreeUtil: 树构造器

仅限中文

使用场景

实现一个树构造器。
比如我们要实现一个列表,
mysql 数据结构:

id parent_id name sort
1 -1 北京 1
2 1 朝阳区 1
3 1 海淀区 2

列表形式:
北京
|- 朝阳区
|- 海淀区

每条数据通过parentId 关联并表示层级关系,考虑设计struct,辅助我们从 []any->tree->json 的转换

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

  1. 构造一个struct TreeNode ,显示指定构造树必须字段,并且通过map 映射数据库单行记录。
    constraints.Ordered 是一个扩展语法糖,约束参数必须可以通过< <= > >= 比较。
    通过go get golang.org/x/exp/constraints 安装

    type TreeNode[T comparable, E constraints.Ordered] struct {
    //ID
    id T
    //父节点ID
    parentId T
    //名称
    name string
    //权重
    sort E
    //扩展字段
    extra map[string]any
    }
  2. 提供一个默认函数,用于映射mysql 字段和treeNode 的映射

    // DefaultParser 将任意对象转换为TreeNode
    func DefaultParser[T comparable, E constraints.Ordered](src any) TreeNode[T, E] {
    
  3. 定义一个配置类

    type TreeOptional struct {
    //设置id对应名称
    idKey string
    //设置父id对应名称
    parentIdKey string
    //设置node对应名称
    nameKey string
    //设置权重对应名称
    sortKey string
    //配置递归深度,0表示不限制
    deep int
    }
  4. 定义Tree 本体

    // Tree 包含指针的树节点
    type Tree[T comparable] struct {
     mp     map[string]any
     config *TreeOptional
     children *[]Tree[T]
         }

5.提供一个构造函数

// BuildTree 参数:list 源切片数据,rootId 最顶层rootId, opt 配配置, fn 转换器,用于处理字段映射
func BuildTree[T comparable, E constraints.Ordered](list []any, rootId T, opt TreeOptional, fu func(src any) TreeNode[T, E]) Tree[T] {
}

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

bean/copier: Benchmark不全面问题

问题简要描述

bean/copier: 中递归实现与前缀树实现的Benchmark不全面需要分场景比较:

  1. 创建后仅调用一次copy
  2. 创建后会调用多次copy

复现步骤

请提供简单的复现代码

仔细翻看前缀树实现的Benchmark会发现NewReflectCopier这个耗时耗空间的操作并未被放入循环中.由此可知现有Benchmark比较的是第二次及以后两者copy的效率即“创建后会调用多次copy”

前缀树赢在在New阶段建立好备忘录信息,在后续多次copy中加速其过程.而递归实现会把两次完全相同的调用理解为两次不同的调用,重新解析复制.

错误日志或者截图

你期望的结果

  1. 为前缀树实现添加“创建后仅调用一次copy”的Benchmark
  2. 根据 #45 重新讨论一下ReflectCopier的使用场景:
# 创建后调用多次copy
copier := NewReflectCopier[VO, BO]
copier.Copy(instance1ofVO, instance1ofBO)
copier.Copy(instance2ofVO, instance2ofBO)
copier.Copy(instance3ofVO, instance3ofBO)
# 创建后调用一次copy
copier := NewReflectCopier[VO, BO]
copier.Copy(instanceofVO, instanceofBO)

copier := NewReflectCopier[VO1, BO1]
copier.Copy(instanceofVO1, instanceofBO1)

你排查的结果,或者你觉得可行的修复方案

可选。我们希望你能够尽量先排查问题,帮助我们减轻维护负担。这对于你个人能力提升同样是有帮助的。

在前缀树实现的Benchmark里添加如下代码,即“创建后仅调用一次copy”的场景:

func BenchmarkReflectCopier_Copy_Only_One_Time(b *testing.B) {
	for i := 1; i <= b.N; i++ {
		copier, err := NewReflectCopier[SimpleSrc, SimpleDst]()
		if err != nil {
			b.Fatal(err)
		}
		_, _ = copier.Copy(&SimpleSrc{
			Name:    "大明",
			Age:     ekit.ToPtr[int](18),
			Friends: []string{"Tom", "Jerry"},
		})
	}
}

func BenchmarkReflectCopier_CopyComplexStruct_Only_One_Time(b *testing.B) {
	for i := 1; i <= b.N; i++ {
		copier, err := NewReflectCopier[ComplexSrc, ComplexDst]()
		if err != nil {
			b.Fatal(err)
		}
		_, _ = copier.Copy(&ComplexSrc{
			Simple: SimpleSrc{
				Name:    "xiaohong",
				Age:     ekit.ToPtr[int](18),
				Friends: []string{"ha", "ha", "le"},
			},
			Embed: &EmbedSrc{
				SimpleSrc: SimpleSrc{
					Name:    "xiaopeng",
					Age:     ekit.ToPtr[int](88),
					Friends: []string{"la", "ha", "le"},
				},
				BasicSrc: &BasicSrc{
					Name:    "wang",
					Age:     22,
					CNumber: complex(2, 1),
				},
			},
			BasicSrc: BasicSrc{
				Name:    "wang11",
				Age:     22,
				CNumber: complex(2, 1),
			},
		})
	}
}

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

queue: 队列接口设计

仅限中文

使用场景

有些时候我们会需要使用队列来完成一些操作。实际上大多数的场景,我们使用 channel 就足够了。但是在一些必要的场景下,我们还是需要队列。

在我个人的开发经历中,我会在一些特定的时候需要一些队列:

  • 我需要遍历队列的时候
  • 我需要删除队列里面的元素的时候
  • 我需要优先级队列的时候
  • 我需要基于时间的优先级队列的时候。尤其是这个队列,我在设计各种中间件的时候,有时候会需要按照时间来调度一些东西,比如说控制过期时间,那么这个时候我会非常希望有一个时间优先级队列,当我从队列头取元素的时候,我能够被阻塞,直到最近一个过期时间到了

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

这个东西主要参考 Java 里面的队列接口设计。我个人不太熟悉其它语言的队列接口,我们可以综合讨论。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

其它

该 issue 是一个设计类的 issue,所以你可以直接创建出来一个合并请求,我们在合并请求里面深入讨论队列接口设计

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

pool: TaskPool 实现

仅限中文

使用场景

在有些时候,我们需要控制住 goroutine 的数量。例如在实际应用中,我们可能有快慢两种请求。对于慢请求来说,如果我们不限制它所能占据的 goroutine 数量,那么慢请求占据了一个 goroutine 之后一直不会释放,那么就会导致越来越多的 goroutine 被慢任务占据。

极端情况下,可能超过一半的 goroutine 都被慢任务所占据。这部分 goroutine 一直占据着资源在缓慢运行。

因此我们可能希望引入一种隔离机制,将慢任务和快任务分离,让慢任务在数量有限的 goroutine 上运行,而其它快任务则没有这种限制。

例如说我们在做数据迁移的时候,开启 10 个 goroutine 并发迁移不同表的数据。

因此我们需要设计一个任务池,用户提交任务,并且可以控制住并发执行任务的 goroutine 数量。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

这方面的典型设计,就是 Java 的 ExecutorService,也就是 Java 中线程池的公共接口,它的方法可以分成好几个部分:

image

可以认为,它的接口设计聚焦在解决几个问题:

  • 退出:Shutdown 和 ShutdownNow,以及 awaitingShutdown。归根结底就是用户希望在关闭的时候,可能希望尽量执行已经提交的任务,但是又不希望等待太久
  • 提交任务,任务分成有返回值和没有返回值两种。对应于 Java 里面的 Runnable 和 Callable
  • invokeX 系列,如何调度任务

可行方案

TaskPool 接口定义

接口定义只需要描述 TaskPool 所要遵循的规范,查看 TaskPool

每一个方法的设计理由是:

  • Submit 要考虑一个额外的阻塞因素。即如果任务池本身有容量的限制,那么用户在提交任务的时候可以被阻塞一会,在阻塞的这段时间,或许能够提交任务成功,否则就返回错误。影响 Submit 的还有 Start 方法和 Shutdown, ShutdownNow 两个方法。对于前者来说,实现者可以自由决定 Start 之后还是否允许继续提交任务——这本质上是一个线程安全的问题;Shutdown 的两个方法,则明确在调用之后就不再允许提交任务了;
  • Shutdown 和 ShutdownNow:Shutdown 返回了一个 channel,那么用户监听这个 channel,就可以得知所有任务什么时候被执行完毕,同时自己也可以控制等待所有任务执行完毕的时间。而 ShutdownNow 则可以立刻关闭线程池,并且返回所有剩下未执行的任务
  • Start 启动任务:用户可以在更加确切的地方开始调度任务的执行

Task 设计

Task 本身的设计并没有考虑任何返回值的问题,后面我们会提供一个允许返回值的任务的装饰器。为了简化用户的操作,即用户不希望自己的任务也需要实现 Task 接口,那么就可以使用我们的 TaskFunc 进行一个简单的转换:

t := TaskFunc(func(ctx context.Context) {
    //.. 做些事情
})

Task 的 Run 方法被设计为接收一个 ctx 参数,这意味着用户应该考虑任务需要进行超时控制的问题,即使是慢任务,用户可能也预期这个任务能够在一小时或者两小时内释放掉 goroutine,即便此时它还没有执行完毕。

BlockQueueTaskPool

基于队列的阻塞式的任务池实现。
核心在于通过 concurrency 和 queueSize 来控制 goroutine 数量和等待任务。
而如何控制 queueSize 和 concurrency 则有很多方案:

  • 使用 channel
  • 读写锁
  • 原子变量

这里我们不做这种限制

其它

你需要:

  • 提供详细的测试用例

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Tuple: 新增 Pair 和 Triple 两种类型

仅限中文

使用场景

  • 需要键值对(Key, Value)的时候,但是又没必要使用 map 这种复杂结构
  • 需要一个 Key 对应多个 Value 的时候

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

主要参考 KotlinPython 中的 tuple

Kotlin

官方文档 PairTriple,其官方示例如下所示

val (a, b) = Pair(1, "x")
println(a) // 1
println(b) // x

val (a, b, c) = Triple(2, "x", listOf(null))
println(a) // 2
println(b) // x
println(c) // [null]

都实现了 Serializeable 接口;可以单独获取每个位置的值;支持将其转为 StringList
额外支持使用 .copy(first=3) 的方式复制并修改值

Python(3.11)

官方文档,特点如下:

  1. 元组不可变
  2. 支持一般序列的各种操作,如查找、拼接、切片、长度、最大最小值等

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

目标

  1. 支持序列化和反序列化
  2. 支持 .copy() 来显式(复制)更改值
  3. 支持多种类型

包结构

最开始是打算第一层用 tuple 包,然后再下接 pairtriple 的,
但是发现这样存在一个很明显的问题:没必要这么设计:

不会存在 ekit.tuple(a,b,c)ekit.tuple(a,b) 混用的情况,而且这样设计也容易混淆 pairtriple

更多的值?

也就是支持列如 quadra(a,b,c,d) 的内容:
没有这个必要,如果有这么多的值的需求应该自己创建一个数组来解决

支持的类型

map 不一样,不存在需要做比较之类的操作,所以支持 any
但是因为涉及到序列化 toString()toList() ,所以需要特殊处理一些类型
直接使用 fmt 包和 []any 就可以了。所以不限制类型

关于 nil

在 Kotlin 中,Value 是可以为 null 的,但是设为 null 之后不可更改
这样可能不方便使用,故不参考

其它

任何你觉得有利于解决问题的补充说明

期待的代码示例

	pair := Pair{First: 1, Second: "one"}
	_ = pair.First                       // 1
	_ = pair.Second                      // one
	_ = pair.ToString()                  // <1,one>
	_ = pair.ToList()                    // [1,"one"]
	pair = pair.Copy(Pair{First: "two"}) // <"two","one">
	pair = pair.Copy(Pair{Second: 2})    // <"two",2>
	pair = pair.Copy(Pair{
		First:  2,
		Second: "two",
	}) // <2,"two">

	triple := Triple{
		First:  3,
		Second: "Three",
		Third:  true,
	}
	_ = triple.First                            // 3
	_ = triple.Second                           // three
	_ = triple.Third                            // true
	_ = triple.ToString()                       // <3,"three",true>
	_ = triple.ToList()                         // [3,"three", true]
	triple = triple.Copy(Triple{First: "four"}) // <"four","three",true>
	triple = triple.Copy(Triple{Second: 4})     // <"four",4,true>
	triple = triple.Copy(Triple{Third: false})  //<"four",4,false>
	triple = triple.Copy(Triple{
		First:  4,
		Second: "four",
		Third:  nil,
	}) // <4,"four",false>

pool: 重构去除 OnDemandBlockTaskPool 中的 context.Context 字段

仅限中文

当前实现缺陷

目前的 OnDemandBlockTaskPool 的定义里面使用了 context.Context 来作为字段类型,核心是用它来作为关闭的通知:

type OnDemandBlockTaskPool struct {
	// TaskPool内部状态
	state int32

	queue chan Task
	token chan struct{}
	num   int32
	wg    sync.WaitGroup

	// 外部信号
	done chan struct{}
	// 内部中断信号
	ctx        context.Context
	cancelFunc context.CancelFunc
}

但是从语义上来说,context.Context 所表达的是一个上下文概念,一般用作方法参数。即便不是用作方法参数,也是用在 http.Request 这种,和请求生命周期保持一致的场景中。

TaskPool 不同于此。TaskPool 的生命周期可以认为是极其漫长的,大多数的使用场景都是在应用或者服务启动的时候创建一个,而后在服务或者应用关闭的时候销毁。也就是说,它会跨越多个请求。

在这种场景之下,使用 context.Context 作为参数则不太合适了。

重构方案

描述可以如何重构,以及重构之后带来的效果,如可读性、性能等方面的提升

我们可以考虑使用简单的 channel 来发送关闭信号:

type OnDemandBlockTaskPool struct {
	// TaskPool内部状态
	state int32

	queue chan Task
	token chan struct{}
	num   int32
	wg    sync.WaitGroup

	// 外部信号
	done chan struct{}
	// 内部中断信号
	closing chan struct{}
}

其它

任何你觉得有利于解决问题的补充说明

要注意,最好将容量设置为1,并且在发送了关闭信号之后,要把 channel 给关闭掉

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

retry: 指数退避重试策略实现

仅限中文

设计与实现

#167 我们提供了接口设计与等时间间隔实现,那么这里要求提供一个指数退避的实现。

对于指数退避来说,用户可以设置:

  • 初始重试间隔,也就是第一次重试要等多久,而后就开始按照2的指数进行增长;
  • 最大重试间隔。指数增长是很快的,所以我们要防止这种增长导致重试间隔过长。比如说最长时间间隔是 10分钟;
  • 重试次数,0 或者负数代表无限制;

前置任务 #167

reflectx: IsNil 方法

仅限中文

使用场景

在自带的反射包里面,IsNil 只能在特定类型是调用,例如说 channel, map 之类的。如果在其他类型上调用,例如说基本类型,那么会引起 panic。

这种语义十分违背直觉,因为我们会预期说如果类型不对,那么我们就返回 false。

所以现在我们尝试在 ekit 里面提供类似的方法 IsNil。

那么对于这些会引起 panic 的类型,我们就返回一个false。

copier: 支持类型转换

仅限中文

使用场景

在复制的过程中,我们有时候需要进行一些类型转换。比如说在领域对象里面,我们普遍是用 time.Time 来记录时间,那么到了返回给前端的 VO(view object),就需要转换成特定格式的字符串。

还有类似的场景:

  • 在数据库查询的 entity 里面使用毫秒数记录时间,但是在领域对象里面要记录 time.Time。
  • 数字类型之间的互相转化
  • 数字类型转字符串

所以我们可以考虑在 copier 里面有限度地支持不同的类型之间的转换。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

我们可以延续在 #195 中的设计,允许用户指定转换类型,但是用户需要显式指定使用什么转换方式。

cp.Copy(src, dst, ConvertField(field, converter))

其中 converter 是特定接口的实现:

// 创建一个新的包叫做 converter
type Converter interface{
     Convert(src any) (any, error)
}

而后我们需要提高各种实现。

第一个实现是函数式编程的实现:

// 它也实现了 Converter 接口
type ConverterFunc func(src any) (any, error)

第二个是 time.Time 转字符串

type Time2String struct {
    Pattern string // 就是格式化的目标格式,那个 2006 啥的
}

后续我们根据需要提供实现。当然,如果你有空,可以同步提供 bytes 转 string, 数字转 string。注意的是,这里要避免大而全的方法,比如说 NumberToString,然后在里面判断 src 是什么类型。你应该准确提供 Uint2String 这种。因为对于一个用户来说,他是很明确知道自己的源类型是什么,目标类型是什么。

即便在一些特殊场景下,他不清楚自己的源类型和目标类型,我们也希望他自己去做这种类型判断。

同时,允许用户在创建 ReflectCopier 的时候指定默认的类型转换。比如说:

cp := NewReflectCopier(FieldsConverter(map[string]Converter))

注意,NewReflectCopier 也是使用 Options 模式,并且复用 #195 这里的设计。

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

setx: TreeSet

仅限中文

使用场景

额外提供一个基于 TreeMap 的 TreeSet 实现。

前置 issues #146

list:ConcurrentList

仅限中文

使用场景

在某些时候,我们拿到了一个原始的切片,或者说拿到了一个非线程安全的 List 实例,我们都需要考虑将它封装为线程安全的 List,在这种情况下,我们可以考虑说利用装饰器模式,允许用户将一个非线程安全的 List 封装成线程安全的 List。

这也就是 ConcurrentList 的设计初衷

其它

任何你觉得有利于解决问题的补充说明

因为这个实在是简单,所以只需要一个同学完成就可以,先到先得。该类型定义在

https://github.com/gotomicro/ekit/blob/dev/list/concurrent_list.go

queue: 线程安全的优先级队列

仅限中文

使用场景

在一些业务里面,我们会不断地往队列里面增加元素,但是在取出元素的时候,我们希望是按照优先级来取的。

同时在没有元素的时候,我们希望能够阻塞住,并且能够控制阻塞的时间。

优先级队列是否应该无界,需要进一步讨论。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

优先级队列,基本思路应该是使用大顶堆或者小顶堆。这里我们可以认为优先级是按照从高到低排序,即我们永远优先取出来优先级最高的队列。

其它

你需要:

  • 给出设计文档
  • 提供实现
  • 提供基准测试

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

pool: 基于 Arena 的对象池的实现

仅限中文

使用场景

目前来说,在 sync.Pool 的支持下,大多数情况下我们不需要额外设计一个新的对象池。但是在 sync.Pool 不管怎样,都还是被 GC 管理着。如果我们需要一个纯粹的、绕开了 GC 的对象池实现,那么就还是要考虑使用 Arena。

目前 Go 的 Arena 是一个新特性(实验特性),但是并不妨碍我们可以先做一些前期探讨。

所以你的目标就是利用 Arena 来实现一个对象池。在最开始的阶段,你可以不考虑控制内存使用量、过期和淘汰之类的问题。我们先看一看效果。

为了达成这个目标,你需要设计一个类似于 sync.Pool 的 API,同时提供例子展示怎么使用这个对象池。

syncx.Cond: Wait(time.Duration) 调用

仅限中文

使用场景

在实现并发阻塞队列的时候,我发现 sync.Cond 缺乏一个关键调用,即等待一段时间。如果等待的时候这个 goroutine 被唤醒,那么可以继续;又或者到期了,这个 goroutine 可以继续。

需求这种 API 是因为我们希望同时控制住阻塞和超时。

理论上来说,一种简单的做法是:

func Dequeue(ctx context.Context) {
    go func() {
         signal.Wait()
        // do something
        select {
        case  wake <- struct{}{}:
        default
        }
    }
    select {
        case <- wake:
        case <- ctx.Done()
    }
}

这种做法的缺陷非常明显,我们没有办法在 ctx.Done 返回的时候中断掉 goroutine 的执行。例如在我们的代码里面,do something 其实代表的就是出队。类似地,入队也有这种问题,而且入队没有办法真的中断,导致即便超时了,最终元素还是会放进去队列里面。

第二个缺点是这迫使我们开启一个 goroutine,性能有影响。在 goroutine 里面我们必须用 select + default 的形式,避免 goroutine 泄露,那么这种写法就会引起下面这个问题。

第三个缺点则是,我们其实希望的是在 Wait 的时候主动释放锁。这种写法迫使我们在进入 select 之前必须释放锁。那么就会造成释放锁之后还没进去 select,那么就有人唤醒了 goroutine,以至于我们直接丢掉了信号。
目前来看, sync.Cond 只提供了一个 Wait 调用,我们需要的是一个类似于 Wait(time.Duration) 的调用。在发起这个调用的时候,锁会被释放掉。

行业分析

最典型的就是 Java 的 Condition 设计,里面就提供了 Await 的方法,满足我们的要求:https://github.com/AdoptOpenJDK/openjdk-jdk12u/blob/master/src/java.base/share/classes/java/util/concurrent/locks/Condition.java#L270

可行方案

在 Go 里面有了类似的建议,但是目前还看不到什么时候才会实现:

  • golang/go#16620 : 这个 proposal 最大的问题在于没有讨论在 select 的时候我是希望当前 goroutine 放弃当前持有的锁

实际上我需要 API 是:

// 调用这个 goroutine 会被阻塞,直到超时,或者别人唤醒
// 在阻塞期间,c 上的锁会被释放
func (c Cond) Await(time.Duration) {

}

其它

目前来看,依赖于 Go SDK 难以实现,所以可以考虑使用 cgo 引入一些 C 的API 来支持。当然,如果你能用 Go 原生 API 支持是最好的。

在缺乏这个关键调用的情况下,实现支持超时的并发阻塞队列会非常复杂,可以参考:

  • #114
  • #115 这是一个错误的实现
  • #111 这也是一个错误的实现

111 和 115 都可以通过重构修改为真的线程安全的,但是其复杂度就不会比 #114 更低。所以我并不希望引入如此复杂的实现。

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

mapx: HashMap 增加 Keys 和 Values 方法

仅限中文

唯一的要求是,keys 和 values 两个方法不能返回 nil。并且要在注释里面注明,keys 和 values 的返回值顺序是不确定的,比如说可能是 key1 在前,也可能是 key2 在前

queue: 基于时间的线程安全优先级队列

仅限中文

使用场景

以我们设计本地缓存为例。一个很重要的目标是在缓存过期之后能够及时删除掉元素。但是问题在于我们难以监控所有元素的过期时间,所以大多数时候我们会采用一种懒删除 + 定时删除的策略。

而如果在有基于时间的线程安全队列时候,那么我们就可以做到:

  • 每次从队列中取元素。取出来的总是已经过期了的;
  • 如果元素没有过期,那么会将调用者阻塞,直到有元素过期;

这种队列是否应该无界,可以进一步讨论。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

基于时间的线程安全优先级队列难点就在于如何在时间没有到点的时候,阻塞住调用者,并且在时间到达了特定时间点的时候唤醒调用者。

其它

你需要:

  • 提供设计文档
  • 提供实现
  • 设计基准测试

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

mapx: 为 MultipleMap 添加 PutVals 方法

仅限中文

使用场景

MultipleMap 因为实现的是 mapi 接口,所以本身只有一个 Put 方法,存放一个值。

但是我们可以考虑添加一个额外的方法,叫做 PutVals 或者 PutMulti,值作为一个不定参数,方法名字可以自己决定,只要切合这个行为就可以,方法定义可以参考 Put 方法。

那么在引入这个方法之后,就可以用 Put 调用它,从而统一逻辑。

如果没有传入任何值,也不需要返回错误。

LinkedList: 修改capacity的业务逻辑

仅限中文

在提之前请先查找已有 issues,避免重复上报。

并且确保自己已经:

  • 阅读过文档
  • 阅读过注释
  • 阅读过例子

问题简要描述

  1. 基于 #110 对capacity的讨论,当前LinkedList的Cap() 返回链表当前长度不是很合理,与PriotityQueue中对capacity和length的定义不完全一致
  2. 现在LinkedList是无界的,可以增强为通过用户输入创建有界或无界的LinkedList
  3. 虽然可以通过组合新增有界LinkedList,但由于队列的设计要依托于链表,队列本身是分有界和无界的,如果分别基于不同的链表实现两种队列,也与已实现的PriorityQueue行为不一致

故不如从根上解决,统一LinkedList和PriorityQueue关于 capacity的业务逻辑,并把LinkedList增强为同时支持有界和无界

修改思路

  1. LinkedList增加capacity属性,capacity <= 0表示无界,否则为有界
  2. LinkedList已有的length属性,表示链表的当前长度,业务逻辑要与capacity有所区分
  3. Append()Add()方法,增加对于capacity的边界检查
  4. 增加CapacityOption,创建LinkedList时,通过该参数控制新建的链表为有界或无界

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

list: 迭代器设计

仅限中文

使用场景

有一类很常见的需求,就是遍历,发现符合条件的元素,就把它删除。

在目前的 API 里面,很难满足这一类的需求。我们虽然有一个 Delete 的方法,但是 Delete 方法本身接收的是下标。而Delete 本身就可能改变一个元素的下标,所以使用 Delete(idx) 很难达成我们预期的效果。

例如

l := len(list)
for i :=0; i < l; i ++ {
     if list[i] == xxx {
           list.Delete(i)
     }
}

例如如果我们预期删除 [1, 3, 4] 中的奇数,那么我们这一段类似的代码并不能达成我们的目标,我们会会遗漏,因为当我们删除 1 之后,3 的下标就调整到了 0,而我们已经遍历了下标 0,最终它就会成为漏网之鱼。

我所希望的效果是:
for ele, ok := itr.Next(); ok {
if ele == xxx {
itr.Delete()
}
}

这算是一个比较大的改进,所以如果你想要提供实现,你需要提供一份设计文档。参考

https://github.com/gotomicro/eorm/discussions/66

我们也可以在这里先讨论可行的方案。

你设置的的 Go 环境?

上传 go env 的结果

sqlx:ScanRows 和 ScanAll方法

仅限中文

使用场景

在数据库相关的中间件研发场景里面,很经常遇到的需求是我并不知道我可以用什么类型来接收数据,所以我希望用 sql.Rows 对应的 ScanType 来接收。

也就是逻辑是类似于:

typ := rows.ScanType()
val := reflect.New(typ)
rows.Scan(val)

目前来说,这种用法在分库分表的结果集处理里面被大量使用。

另外一个场景是,在设计通用的数据迁移和数据校验中间件里面,也出现了类似的需求。数据迁移是需要把源表的数据读取出来,然后再插入到对应的数据表里面。因为数据迁移要处理的表结构非常多样化,所以实际上开发的时候也难以预料到应该用什么 Go 类型来接收,那么这个 Scan 方法就很合适了。

那么这个方法的定义是:

func ScanRows(rows *sql.Rows) []any, error {

}

对应的还有一个方法是:

func ScanRow(row sql.Row) []any, error {

}

这个方法只会纯粹扫描 sql.Row。

进一步,如果我们试图将所有的结果取出来,那么定义成为:

func ScanAll(rows sql.Rows)  [][]any, error {
    
}

注意,你需要提供单元测试,这一个单元测试你使用sqlite 的内存模式来测试。

ScanRows 你可以考虑参考合并请求 https://github.com/ecodeclub/eorm/pull/193/files
image

slice.ContainsFunc破坏性修改确认

仅限中文

在提之前请先查找已有 issues,避免重复上报。

并且确保自己已经:

  • 阅读过文档
  • 阅读过注释
  • 阅读过例子

你的问题

slice.ContainsFunc方法在v0.0.7的方法签名如下:

// ContainsFunc 判断 src 里面是否存在 dst
// 你应该优先使用 Contains
func ContainsFunc[T any](src []T, dst T, equal equalFunc[T]) bool {

v0.0.8的方法签名如下:

// ContainsFunc 判断 src 里面是否存在 dst
// 你应该优先使用 Contains
func ContainsFunc[T any](src []T, equal func(src T) bool) bool {

请确认是否预期内的修改,如果是请将v0.0.8及以后版本中ContainsFunc的注释内容同步修改.

你使用的是 ekit 哪个版本?

v0.0.7 v.0.0.8

你设置的的 Go 环境?

上传 go env 的结果

pool: OnDemandBlockTaskPool 测试不稳定

仅限中文

当前实现缺陷

目前,OnDemandBlockTaskPool 的测试,都是采用了 time.Sleep 的方案来验证并发问题。

所以在执行 github action 的时候,容易出现偶发性的测试失败。因此我们要考虑如何解决测试不稳定的问题。

重构方案

描述可以如何重构,以及重构之后带来的效果,如可读性、性能等方面的提升

在这些测试里面,我们可以考虑使用两个工具来取代 time.Sleep。

  • sync.WaitGroup
  • channel

这样可控性会强很多。

但是缺陷是测试更加难以理解。不过相比不稳定的测试,难以理解的测试还是要更好一点,毕竟原本的 time.Sleep 就已经不容易理解了。或者说,在并发测试里面,都很难理解。

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

value: AnyValue 设计

仅限中文

使用场景

Go 因为泛型方面以及 error 处理两方面的限制,导致有一些 API 非常难用。举一个例子,在正常的情况下,我们的缓存 API 会设计成:

type Cache interface {
      Get(key string) (any, error)
}

那么用户在使用的时候,如果想要万无一失使用这个 API,它必须要进行两次检测:

val, err := c.Get("mykey")
if err != nil {
     return err
}
intVal, ok := val.(int)
if !ok {
     return errors.New(xxxx)
}
// 使用 IntVal

所以我们可以考虑参考类似于 sql.Row 的设计,提供一个已经封装好了的 API : AnyValue

type AnyValue struct {
    val any
    err error
}

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

核心的类型是

type AnyValue struct {
     val any
     err error
}

func (a AnyValue) Int() (int, error) {
      if err != nil {
          return 0, err 
     }
     val, ok := a.(int)
     if !ok {
         return errInvalidType
     }
     return val, nil
}

// 返回 int 数据,或者默认值
func (a AnyValue) IntOr(def int) int {
    val, err := a.Int()
    if err != nil {
       return nil
    }
}

这里我设计了两类方法:

  • 支持类型断言,并且返回值和 error
  • 支持类型断言,并且返回值或者指定的默认值

除了 int,还要支持以下类型:

  • int 族
  • uint 族
  • float 族
  • string
  • []byte

其它的 map 和切片,或者数组,因为 Go 类型的限制,我觉得就没有特别强的动力去支持了。

其它

要不要做类型转换

假如说值的真实类型是 int,但是用户要求返回一个 int64,那么按照道理我们可以将 int 转化为 int64 类型

但是如果我们打算支持类型转换,那么我们就需要进一步考虑,string 转基本类型要不要支持?基本类型转 string 类型要不要支持?于是我们就会陷入这种很尴尬的境地,即如果我们要支持类型转化,我们就要考虑这些类型的排列组合。

即便我们要支持,我也倾向于我们提供另外一组方法,以 Convert 为前缀

  • ConvertInt() (int error)
  • ConvertIntOr(def int) int

那么用户就可以知道,这些方法内部是支持类型转化的。这种转化在一个特定的场景下会非常有用:web 等操作 string 类型数据的框架。例如 Redis 的客户端拿到的永远是 string,我们就可以利用这种机制简化代码:

redis.Get("mykey").ConvertInt()

又或者是 Web 的路径参数,Header 的值,查询参数等,都可以利用这种 API 来简化代码

要不要支持特性类型的切片

例如要不要支持 []int, []uint 这种比较常见的切片类型?这个确实是可以考虑支持的,如果不嫌麻烦的话就是可以一起支持了

放在哪个包?

我暂时是想说新开一个 value(x) 的包,放在这里面。另外一个可以考虑的是直接放在顶级目录下。我还在犹豫,这个可以进一步讨论。

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

randx: 随机验证码生成器

仅限中文

使用场景

昨天的课里用的randn方法生成随机数,这个默认随机数种子是固定的,每次生成的随机数序列是固定的,所以我想提供一个随机数生成的工具

行业分析

很多场景都需要一个随机码,需要支持多种验证码类型,支持数字,字母以及混合类型的随机码生成
标准库提供的Intn方法有一个默认的随机数种子,这种随机数生成的随机数序列是固定的,也就是说,程序重启,随机数会重复,
当然可以以unix时间戳来当成种子生成,但是这种方式不能生成带字母的随机序列,
但是我也没发现有哪个其它的库实现了这个功能
所以,我想这个随机码还是使用场景比较广泛的

可行方案

核心想法就是利用标准库的rand.Int63()这个方法,这个方法可以一次生成63位的int整数,而我们通常只需要4-8位就够用了,这样就可以分段多次利用这个随机数,来生成我们自己的随机码,核心代码如下:

// generate 根据传入的随机源和长度生成随机字符串,一次随机,多次使用
func generate(source string, length, idxBits int) string {

	//掩码
	//例如: 使用低6位:0000 0000 --> 0011 1111
	idxMask := 1<<idxBits - 1

	// 63位最多可以使用多少次
	idxMax := 63 / idxBits

	result := make([]byte, length)

	//cache 随机位缓存
	//remain 当前还可以使用几次
	for i, cache, remain := 0, rand.Int63(), idxMax; i < length; {
		//如果使用次数剩余0,重新获取随机
		if remain == 0 {
			cache, remain = rand.Int63(), idxMax
		}

		//利用掩码获取有效的随机数位
		if randIndex := int(cache & int64(idxMask)); randIndex < len(source) {
			result[i] = source[randIndex]
			i++
		}

		//使用下一组随机位
		cache >>= idxBits

		//扣减remain
		remain--

	}
	return string(result)

}

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

最新版本

你设置的的 Go 环境?

上传 go env 的结果
set GO111MODULE=on
set GOARCH=amd64
set GOBIN=
set GOCACHE=C:\Users\felic\AppData\Local\go-build
set GOENV=C:\Users\felic\AppData\Roaming\go\env
set GOEXE=.exe
set GOEXPERIMENT=
set GOFLAGS=
set GOHOSTARCH=amd64
set GOHOSTOS=windows
set GOINSECURE=
set GOMODCACHE=D:\developer\GoPath\pkg\mod
set GONOPROXY=
set GONOSUMDB=
set GOOS=windows
set GOPATH=D:\developer\GoPath;C:\Users\felic\go
set GOPRIVATE=
set GOPROXY=https://goproxy.cn,direct
set GOROOT=D:\developer\Go
set GOSUMDB=sum.golang.org
set GOTMPDIR=
set GOTOOLDIR=D:\developer\Go\pkg\tool\windows_amd64
set GOVCS=
set GOVERSION=go1.20.6
set GCCGO=gccgo
set GOAMD64=v1
set AR=ar
set CC=gcc
set CXX=g++
set CGO_ENABLED=0
set GOMOD=D:\opensource\ekit\go.mod
set GOWORK=
set CGO_CFLAGS=-O2 -g
set CGO_CPPFLAGS=
set CGO_CXXFLAGS=-O2 -g
set CGO_FFLAGS=-O2 -g
set CGO_LDFLAGS=-O2 -g
set PKG_CONFIG=pkg-config
set GOGCCFLAGS=-m64 -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=C:\Users\felic\AppData\Local\Temp\go-build3885401515=/tmp/go-build -gno-record-gcc-switches

pool: TaskPool 的可观测性设计

仅限中文

使用场景

正如 #50@lincolnzhou 中提到的,一个符合生产条件要求的 TaskPool,必须要能够暴露观测接口,允许用户监控池子本身的状态。

但是我们可以对 #50 中讨论的观测性进一步讨论:

  • 运行指标(百分比+数量+top)
    • 任务运行时长
    • 任务等待时长
    • 任务池活跃度
    • 队列容量
  • 运行时报警策略
    • 任务运行超时
    • 任务池活跃度
    • 阻塞队列容量
    • 触发拒绝策略
    • 主流软件告警(微信,钉钉,飞书)
  • 监控运行可视化
    • Prometheus + Grafana第三方采集监控
    • 内置数据池数据采集 + 监控

核心是三个点:运行指标、告警策略与可视化。

这里我们站在 ekit 作为一个工具的角度出发,那么告警策略和可视化两个点 ekit 比较无能为力。因为 ekit 本身并不会与重量级的 Grafana 或者微信钉钉之类的集成。这种事情我们可以额外提供工具,或者在文档里面建议用户进行监控。

对于 ekit 本身来说,它只需要暴露”必要的“内部状态就可以了。用户可以监控这些内部状态,然后自己集成这些告警策略和可视化平台。

因此一切的问题都在于,该暴露什么状态,以及如何暴露。

目前来说,前面 lincolnzhou 提到的指标,以及我能够想到的一些状态:

  • 任务运行时长:理论上用户可以自己在任务里面监控,但是我们也可以暴露这种数据,因为本身它算是一个 AOP 的需求,毕竟所有的任务都需要监控运行时长
  • 等待时长:类似前者。与之类似的指标有阻塞队列容量,阻塞队列中等待的任务数(或者剩余容量),提交的时候被拒绝的任务数量
  • 活跃度:这个可以用运行中协程数量来进行替代
  • 任务运行状态:成功、失败或者超时。鉴于当前我们的 TaskPool 设计了 context 接口,所以超时控制我们也可以做到的;

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

从设计上来说,大体有:

回调机制

我们允许用户注册一些回调,比如说 OnPanic 和 OnError,它们会在任务触发了 Panic 或者返回了 error 的时候执行。例如

type Pool struct {
    OnPanic func(t, panicVal any) 
}

观察者模式

我们设计一些 Observer 接口,用户在使用 TaskPool 的时候可以传入不同的 Observer:

type TaskObserver struct {
    // 观测具体的任务的状态
   // int 来表达 task 的执行情况可能不是很合适,还要进一步考虑
   Observe(status int, task Task, duration time.Duration)
}

type PoolObserver struct {
    // 观测池子本身的状态,例如阻塞队列的情况
    Observe(...)
}

我们可以针对任何一个观测的东西都设计出来接口,也可以考虑将 TaskObsever 和 PoolObserver 进一步细分。

我一般的设计倾向都是细分接口,然后通过组合来组成大的接口。

channel 暴露状态

这种形态,我们就是单纯暴露一个 channel,而后往 channel 里面写数据。这里我们可以引入一个额外的 Event 抽象:

func (p Pool) Stats() <- chan Event {
    // ... 
    return ch
}

// 对于用户来说
func MyMetrics() {
     ch := p.Stats()
     event := <- ch
     switch event.(type){
     case TaskEvent:
               // 记录 Task 发生的事情  
     }
}

Event 可以设计成接口,然后用户进行 switch type 判断具体发生了什么事。但是使用 channel 不太好的地方就是不好估计 channel 的容量,并且可能阻塞住我们 TaskPool 本身

装饰器模式

装饰器只能解决 Task 的观测和 Pool 本身的部分观测问题。

类似于我们解决 panic 的思路。我们可以通过封装一个新的 Task:

type metricTask struct {
   t Task
   observer Oberver
}

func (t metricTask) Do(ctx context.Context) error {
    startTime := time.Now()
    t.t.Do(ctx)
    endTime := time.Now()
    t.obsever.Observe(endTime-startTime)
}

这种设计形态要和前面的 Observer 或者回调结合在一起。优点就是我们将观测这种事情改成了一种非侵入式的方案。

其它

任何你觉得有利于解决问题的补充说明

Task 的名字问题。类似于在观测的时候,用户可能需要给 Task 一个名字,例如 "task-find-hot-product"。

但是从目前的接口设计来看,我们并没有直接支持。

好在 Task 是一个接口,也就是用户可以完全实现自己的 Task(也可以是我们提供一个):

func MyTask() {
    Name string
}

不过目前我们是对 Task 进行来包装,来解决 panic 的问题,所以实际上我们在考虑通知用户发生了什么事情的时候,要把原始的 Task 传回去。

另外一种思路是我们暴露一个新的接口:

type NamingTask interface {
    Name() string
    Task
}
// 或者

type Naming interface {
    Name() string
}

这种形态也是可以的,我们只需要在实现内部检测是不是 NamingTask。

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

mapx: TreeMap

仅限中文

使用场景

基于哈希的 map 实现都有一个缺陷,即内存使用率不太高,并且存在扩容的问题。虽然在 Go 里面扩容被一种渐进式扩容给取代了,所以问题不大,但是内存利用率上,始终并不是很高。

所以我们可以考虑提供一个基于树形结构的 map 实现。

目前来说,我们并不需要提供非常多的方法,可以只提供 Put 和 Get 两个方法,在后续我们再尝试提供更加多的方法。

行业分析

Java 的 TreeMap

TreeMap 源码

TreeMap 中比较值得借鉴的是里面采用了 Comparator 的设计。在 ekit 里面已经有了这么一个接口,所以我们直接复用。

Java 的 TreeMap 采用的是红黑树结构,那么显然,我们也可以使用红黑树结构。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

list: Append 方法改造为不定参数形式

仅限中文

使用场景

在设计 List 接口的时候,我将 Append 设计为只接收一个参数,但是在实际中我们很可能会遇到 Append 多个元素的场景。

在这种考虑之下,我们应该将 List 的 Append 方法改造为不定参数:

Append(ts...T) error

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

Go 本身的切片操作, append 就是允许追加多个的,例如:

s := []int{1, 2, 3}
s = append(s, 4, 5, 6)

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

注意到,我前面的不定参数方案并没有返回实际追加了多少个。一种可能的方案是:

Append(ts...T)(cnt int, err error)

即在 Append 之后返回真实追加的元素个数。不过我觉得没有必要这么设计,道理很简单:在部分成功的情况下,用户不应该再使用这个 List 了,它已经处于一种不稳定状态。

如果用户真的有这种需求,他可以自己用装饰器模式重新封装 List,然后自己计数。

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

queue: 无界的并发安全队列设计与实现

仅限中文

使用场景

核心在于我们需要一个无界的并发安全队列。我们在设计一些连接池或者协程池的时候,本身是可以考虑使用无界的线程安全的队列的。虽然从效果上来说有界队列无界队列影响并不是很大,但是也足以说明在一些场景下,我们会考虑使用无界队列。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

目前来看,大体上有两种思路:

  • 一种是基于链表的无锁的实现。但是这种实现可能无法支持遍历之类的操作
  • 一种是基于锁的链表的实现(当然也可以用切片)

其它

你需要:

  • 给出设计文档,主要描述核心算法
  • 提供实现
  • 设计 benchmark 测试

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

mapx: 支持 Len 方法

仅限中文

使用场景

在一些时候,我们需要判定整个 Map 里面究竟放了多少个元素,那么就希望可以暴露一个 Len 的方法。

这个 Len 返回的是键值对的数量。也就是说,如果一个 key 对应了多个元素,类似于 MultipleMap,那么还是认为这些元素和key 只是一个键值对而已。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

type mapi[K any, V any] interface {
	Put(key K, val V) error
	Get(key K) (V, bool)
	// Delete 删除
	// 第一个返回值是被删除的 key 对应的值
	// 第二个返回值是代表是否真的删除了
	Delete(k K) (V, bool)
	// Keys 返回所有的键
	// 注意,当你调用多次拿到的结果不一定相等
	// 取决于具体实现
	Keys() []K
	// Values 返回所有的值
	// 注意,当你调用多次拿到的结果不一定相等
	// 取决于具体实现
	Values() []V
       // 返回键值对数量
        Len() int64
}

这里我认为返回 int64 是万无一失的。当然,即便直接返回 int 大多数时候也没问题。

其它

任何你觉得有利于解决问题的补充说明

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

slice: 切片辅助方法

仅限中文

使用场景

切片应该是日常中使用最多的一种数据结构了。
但是切片本身的操作非常有限,所以很多时候我们需要一些辅助方法。在有了泛型的支持下,这些辅助方法将会变得对用户非常友好。
目前我已经定义了一些辅助方法,在 https://github.com/gotomicro/ekit/tree/dev/slice

切片的这些辅助方法分成:

  • 索引类: index 文件内的方法
  • 映射类:map 文件里面的方法
  • 查找类:contains 文件里面的方法
  • 交集:intersect 文件里面的方法
  • 并集:union 文件里面的方法
  • 差集:diff 文件里面的方法

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

其它

任何你觉得有利于解决问题的补充说明

  • 你可以选择其中的一类来实现,不要一次性写完,太多了我看不过来,一类一类来
  • 你需要设计测试用例,尤其是考虑 nil,空切片等情况
  • 你需要写例子,查看 Go 例子写法
  • 你可以提供更加多的方法,但是依旧要提供注释,测试用例和例子

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

ArrayList: 支持 ArrayList

仅限中文

使用场景

目前来说,切片(slice)能够满足大多数的使用场景,但是切片本身只支持非常简单的操作,如 append。在一些场景下,例如我们希望删除元素的时候,切片就不能满足要求了。

在泛型出来之后,利用泛型支持一个 ArrayList 的条件已经成熟了。

行业分析

目前有很多数据结构库,但是都假定元素是 int,而并没有采用泛型来设计。

例如我们在 github 里面找到:
https://github.com/search?l=Go&q=%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&type=Repositories

大部分的仓库都是以学习算法和数据结构为基础的。

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

ArrayList 的实现大体上有两种思路:

  • 直接使用数组,那么我们需要自己手动解决扩容之类的问题
  • 直接使用切片,那么我们就相当于是在切片之上封装了新的功能

我已经完成了接口设计和结构体定义,包括测试用例都有了模板,可以具体参考
List 接口定义
array_list.go
array_list_test.go

注意我额外定义了一个创建 error 的方法 newErrIndexOutOfRange,也就是说,我希望你们在检测下标的时候,如果下标不符合预期,要返回这个错误。

其它

任何你觉得有利于解决问题的补充说明

可以预期的是,这个实现会比较简单,对性能的损耗和直接使用切片比起来,相差不大。但是我们也可以提供 benchmark 测试来验证这个问题

你使用的是 ekit 哪个版本?

v0.0.2

你设置的的 Go 环境?

上传 go env 的结果

unsafe 转换 string 和 []byte

正常在使用 string 和 []byte 之间转换的时候,会引起复制。有一种优化手段,就是利用 string 和 []byte 之间的关系,通过 unsafe 来转换。

请你提供这样的一个转换,新建一个 stringx 包。

sqlx: 加密列支持

仅限中文

使用场景

有一些时候我们处于安全的考虑,会考虑将存储在数据库中的数据进行加密。但是这种加密后的数据还需要解密出来才能使用。比如说在用户的个人信息里面,当我们存储用户的身份证号码的时候,就需要进行加密。但是我们在前端展示的时候,又需要解密之后展示出来。

在这种情况下,我们就需要对列进行加密和解密。

一般情况下,这种需求可以直接通过数据库的加密功能来完成。但是很多时候数据库性能都是瓶颈,所以我们不希望数据库承担这种加密和解密的职责。

所以我们可以选择在应用层面上对列进行加密和解密。

加密和解密的过程是一个和具体业务没有太多关系的东西,或者说加密和解密的过程不应该侵入到业务逻辑中。所以在这种情况下,我们可以考虑提供一个扩展的加密列的实现。

行业分析

如果你知道有框架提供了类似功能,可以在这里描述,并且给出文档或者例子

在我们的 sqlx 包里面,已经利用了类似的技术,实现了 JSON 列

type JsonColumn[T any] struct {
	Val   T
	Valid bool
}

可行方案

如果你有设计思路或者解决方案,请在这里提供。你可以提供多个方案,并且给出自己的选择

我已经定义好了接口:https://github.com/gotomicro/ekit/blob/dev/sqlx/encrypt.go

我们可以通过实现 driver.Valuer 和 sql.Scanner 两个接口来无侵入地提供这种功能:

// EncryptColumn 代表一个加密的列
// 一般来说加密可以选择依赖于数据库进行加密
// EncryptColumn 并不打算使用极其难破解的加密算法
// 而是选择使用 AES GCM 模式。
// 如果你觉得安全性不够,那么你可以考虑自己实现类似的结构体.
type EncryptColumn[T any] struct {
	Val T
	// Valid 为 true 的时候,Val 才有意义
	Valid bool
}

// Value 返回加密后的值
// 如果 T 是基本类型,那么会对 T 进行直接加密
// 否则,将 T 按照 JSON 序列化之后进行加密,返回加密后的数据
func (e EncryptColumn[T]) Value() (driver.Value, error) {
	//TODO implement me
	panic("implement me")
}

// Scan 方法会把写入的数据转化进行解密,
// 并将解密后的数据进行反序列化,构造 T
func (e *EncryptColumn[T]) Scan(src any) error {
	//TODO implement me
	panic("implement me")
}

其它

任何你觉得有利于解决问题的补充说明

正如类型定义里面提到的,这种加密并不是非常强调安全性,而是给一些有点敏感但是又不是特别敏感的数据提供的一种通用的加密支持。如果用户需要更加安全的加密手段,那么他们可以考虑参考这个实现设计一个类似的结构体。

所以我们这里只需要选择一个通用性强的,安全性也不错的算法,在这里我选择了额 AES 的 GCM 模式。从性能上来说它很不错,从破解难度上来说,也还不错。

如果你有更好的加密算法,也可以提出来。从安全性和性能两方面阐述选择它的理由。

实现这个结构体之后,你需要:

  • 设计测试用例:需要考虑用户使用基本类型,内置类型,结构体等各种情况
  • benchmark 测试:测试加密的性能,以 string 作为加密对象

你使用的是 ekit 哪个版本?

你设置的的 Go 环境?

上传 go env 的结果

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.