标签 标准库 下的文章

将Roaring Bitmap序列化为JSON

本文永久链接 – https://tonybai.com/2023/02/01/serialize-roaring-bitmap-to-json

近期在实现一个数据结构时使用到了位图索引(bitmap index),本文就来粗浅聊聊位图(bitmap)。

一. 什么是bitmap

位图索引使用位数组(bit array,也有叫bitset的,通常被称为位图(bitmap),以下均使用bitmap这个名称)实现。一个bitmap是一个从某个域(通常是一个整数范围)到集合{0,1}中的值的映射:

映射:f(x) -> {0, 1}, x是[0, n)的集合中的元素。

以n=8的集合{1, 2, 5}为例:

f(0) = 0
f(1) = 1
f(2) = 1
f(3) = 0
f(4) = 0
f(5) = 1
f(6) = 0
f(7) = 0

如果用bit来表示映射后得到的值,我们将得到一个二进制数0b00100110(最右侧的bit位上的值指示集合中数值0的存在性),这样我们就可以用一个字节大小的数值0b00100110来表示{1, 2, 5}这个集合中各个位置的数值的存在性了。

我们看到相比于使用一个byte数组来表示{1, 2, 5}这个集合(即便是8个数值,也至少要8x8=64个字节),bitmap无疑具有更高的空间利用率。同时,通过bitmap的与、或、异或等操作,我们可以很容易且高性能地得到集合的交、并、Top-K等集合操作的结果。

不过,传统的bitmap并不总能带来空间上的节省,比如我们要表示{1, 2, 10, 50000000}这样一个集合,那么使用传统bitmap将带来很大的空间开销。对于这样的具有稀疏元素特性的集合,传统位图实现就失去了其优势,而压缩位图(compressed bitmap)则成为了更佳的选择。

二. 压缩位图(compressed bitmap)

压缩位图既可以很好的支持稀疏集合,又保留了传统位图的空间和高性能的集合操作优势。最常见的压缩位图的方案是RLE(run-length encoding),对这种方案的粗浅理解是对连续的0和1进行分别计数,比如下面这bitmap就可以压缩编码为n个0和m和1

0b0000....00001111...111

RLE方案(以及其变体)具有很好的压缩比并且编解码也很高效。不过其不足是很难随机访问某个bit,每次访问特定的bit都要从头进行解压缩。如果你想将两个大的bitmap进行交集操作,你必须解压缩整个大bitmap。

一种名为roaring bitmap的压缩位图方案可以解决上述的问题。

三. roaring bitmap工作原理简介

roaring bitmap 的工作方式是这样的:它将32位整型所能表示的整型数[0, 4294967296)划分为2^16个chunk(例如,[0,2^16),[2^16,2x2^16),...)。当向roaring bitmap加入一个数或从roaring bitmap获取一个数的存在性时,roaring bitmap通过这个数的前16位决定该数在哪个trunk中。一旦确定trunk后,便可以通过与该trunk关联的container指针找到真正存储该数后16位值的container,在container中通过查找算法定位:

如上图所示:roaring bitmap的trunk关联的container类型不止有一种:

  • array container:这是一个有序的16bit整型数组,也是默认的container type,最多存储4096个数值。当超出这个数量时,会考虑用bitset container存储;
  • bitset container:就是一个非压缩的bitmap,有2^16个bit位;
  • run container:这是一个采用RLE压缩的、适合存储连续数值的container type,从上面图中也可以看出,这个container中存储的是一个个数对<s,l>,表示的数值范围为[s, s + l]。

roaring bitmap会根据trunk中的数的特征选择适当的container类型,并且这种选择是动态的,以尽量减少内存使用为目标。当我们向roaring bitmap添加或删除值时,对应trunk的container type都可能会改变。不过从整体视角看,无论使用哪种container,roaring bitmap都支持对某个bit的快速随机访问。同时roaring bitmap在实现层面也更容易利用现代cpu提供的高性能指令,并且是缓存友好的。

四. roaring bitmap的效果

roaring bitmap官方提供了多种主流语言的实现,其中Go语言的实现是roaring包。roaring包的使用十分简单,下面就是一个简单的示例:

package main

import (
    "fmt"

    "github.com/RoaringBitmap/roaring"
)

func main() {
    rb := roaring.NewBitmap()
    rb.Add(1)
    rb.Add(100000000)
    fmt.Println(rb.String())
    fmt.Println(rb.Contains(1))
    fmt.Println(rb.Contains(2))
    fmt.Println(rb.Contains(100000000))

    fmt.Println("cardinality:", rb.GetCardinality())
    fmt.Println("rb size=", rb.GetSizeInBytes())
}

运行示例得到如下结果:

{1,100000000}
true
false
true
cardinality: 2
rb size= 16

我们看到{1, 100000000}的稀疏集合映射到roaring bitmap仅占用了16个字节的空间(和非压缩bitmap对比)。

下面是一个由3000w以内的随机整数构成的集合到roaring bitmap的映射示例:

func main() {
    rb := roaring.NewBitmap()

    for i := 0; i < 30000000; i++ {
        rb.Add(uint32(rand.Int31n(30000000)))
    }

    fmt.Println("cardinality:", rb.GetCardinality())
    fmt.Println("rb size=", rb.GetSizeInBytes())
}

下面是其执行结果:

cardinality: 18961805
rb size= 3752860

我们看到集合中一共加入近1900w个数,roaring bitmap总共占用了3.6MB的内存空间,这个和非压缩bitmap没有拉开差距。

下面是一个连续的3000w数字的集合到roaring bitmap的映射示例:

func main() {
    rb := roaring.NewBitmap()

    for i := 0; i < 30000000; i++ {
        rb.Add(uint32(i))
    }

    fmt.Println("cardinality:", rb.GetCardinality())
    fmt.Println("rb size=", rb.GetSizeInBytes())
}

其执行结果如下:

cardinality: 30000000
rb size= 21912

显然针对这样的连续数字集合,roaring bitmap的空间效率体现的十分明显。

五. roaring bitmap的序列化

以上是对roaring bitmap的粗浅入门介绍,如果对roaring bitmap感兴趣,可以去其官方站点或开源项目主页做深入了解和学习。不过这里我要说的是roaring bitmap的序列化问题(序列化后便可以传输和持久化存储了),以序列化为JSON和从JSON反序列化为例。

考虑到性能问题,json序列化我选择的是字节开源的sonic项目。sonic虽然说是一个Go开源项目,但由于其对JSON解析的极致优化的要求,目前该项目中Go代码的占比仅有30%不到,60%多都是汇编代码。sonic提供与Go标准库json包兼容的函数接口,并且sonic还支持streaming I/O模式,支持将特定类型对象序列化到io.Writer或从io.Reader中反序列化数据为一个特定类型对象,这个也是标准库json包所不支持的。当遇到超大JSON时,streaming I/O模式十分惯用,io.Writer和Reader可以让你的Go应用不至于瞬间分配大量内存,甚至被oom killed掉。

不过roaring bitmap并没有原生提供序列化(marshal)到JSON(或反向序列化)的函数/方法,那么我们如何将一个roaring bitmap序列化为一个JSON文本呢?Go标准库json包提供了Marshaler和Unmarshaler接口,凡是实现了这两个接口的自定义类型,json包都可以支持该自定义类型的序列化和反序列化。在这方面,sonic项目与Go标准库json包保持兼容

不过roaring.Bitmap类型并没有实现Marshaler和Unmarshaler接口,roaring.Bitmap的序列化和反序列化需要我们自己来完成。

那么,我们首先想到的就是基于roaring.Bitmap自定义一个新类型,比如MyRB:

// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/bitmap_json.go
type MyRB struct {
    RB *roaring.Bitmap
}

然后,我们给出MyRB的MarshalJSON和UnmarshalJSON方法的实现以满足Marshaler和Unmarshaler接口的要求:

// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/bitmap_json.go
func (rb *MyRB) MarshalJSON() ([]byte, error) {
    s, err := rb.RB.ToBase64()
    if err != nil {
        return nil, err
    }

    r := fmt.Sprintf(`{"rb":"%s"}`, s)
    return []byte(r), nil
}

func (rb *MyRB) UnmarshalJSON(data []byte) error {
    // data => {"rb":"OjAAAAEAAAAAAB4AEAAAAAAAAQACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4A"}

    _, err := rb.RB.FromBase64(string(data[7 : len(data)-2]))
    if err != nil {
        return err
    }

    return nil
}

我们利用roaring.Bitmap提供的ToBase64方法将roaring bitmap转换为一个base64字符串,然后再序列化为JSON;反序列化则是利用FromBase64对JSON数据进行解码。下面我们测试一下MyRB类型与JSON间的相互转换:

// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/bitmap_json.go

func main() {
    var myrb = MyRB{
        RB: roaring.NewBitmap(),
    }

    for i := 0; i < 31; i++ {
        myrb.RB.Add(uint32(i))
    }
    fmt.Printf("the cardinality of origin bitmap = %d\n", myrb.RB.GetCardinality())

    buf, err := sonic.Marshal(&myrb)
    if err != nil {
        panic(err)
    }

    fmt.Printf("bitmap2json: %s\n", string(buf))

    var myrb1 = MyRB{
        RB: roaring.NewBitmap(),
    }
    err = sonic.Unmarshal(buf, &myrb1)
    if err != nil {
        panic(err)
    }

    fmt.Printf("after json2bitmap, the cardinality of new bitmap = %d\n", myrb1.RB.GetCardinality())
}

运行该示例:

the cardinality of origin bitmap = 31
bitmap2json: {"rb":"OjAAAAEAAAAAAB4AEAAAAAAAAQACAAMABAAFAAYABwAIAAkACgALAAwADQAOAA8AEAARABIAEwAUABUAFgAXABgAGQAaABsAHAAdAB4A"}
after json2bitmap, the cardinality of new bitmap = 31

输出结果符合预期。

基于支持序列化的MyRB,顺便我们再看一下sonic和标准库json的benchmark对比,我们编写一个简单的对比测试用例:

// https://github.com/bigwhite/experiments/blob/master/roaring-bitmap-examples/benchmark_test.go

type Foo struct {
    N    int    `json:"num"`
    Name string `json:"name"`
    Addr string `json:"addr"`
    Age  string `json:"age"`
    RB   MyRB   `json:"myrb"`
}

func BenchmarkSonicJsonEncode(b *testing.B) {
    var f = Foo{
        N: 5,
        RB: MyRB{
            RB: roaring.NewBitmap(),
        },
    }

    for i := 0; i < 3000; i++ {
        f.RB.RB.Add(uint32(i))
    }

    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := sonic.Marshal(&f)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkSonicJsonDecode(b *testing.B) {
    var f = Foo{
        N: 5,
        RB: MyRB{
            RB: roaring.NewBitmap(),
        },
    }

    for i := 0; i < 3000; i++ {
        f.RB.RB.Add(uint32(i))
    }

    buf, err := sonic.Marshal(&f)
    if err != nil {
        panic(err)
    }
    var f1 = Foo{
        RB: MyRB{
            RB: roaring.NewBitmap(),
        },
    }

    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        err = sonic.Unmarshal(buf, &f1)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkStdJsonEncode(b *testing.B) {
    var f = Foo{
        N: 5,
        RB: MyRB{
            RB: roaring.NewBitmap(),
        },
    }

    for i := 0; i < 3000; i++ {
        f.RB.RB.Add(uint32(i))
    }

    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        _, err := json.Marshal(&f)
        if err != nil {
            panic(err)
        }
    }
}

func BenchmarkStdJsonDecode(b *testing.B) {
    var f = Foo{
        N: 5,
        RB: MyRB{
            RB: roaring.NewBitmap(),
        },
    }

    for i := 0; i < 3000; i++ {
        f.RB.RB.Add(uint32(i))
    }

    buf, err := json.Marshal(&f)
    if err != nil {
        panic(err)
    }
    var f1 = Foo{
        RB: MyRB{
            RB: roaring.NewBitmap(),
        },
    }

    b.ReportAllocs()
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        err = json.Unmarshal(buf, &f1)
        if err != nil {
            panic(err)
        }
    }
}

执行这个benchmark:

$go test -bench .
goos: darwin
goarch: amd64
pkg: demo
... ...
BenchmarkSonicJsonEncode-8         71176         16331 ns/op       49218 B/op         13 allocs/op
BenchmarkSonicJsonDecode-8         85080         13710 ns/op       37236 B/op         11 allocs/op
BenchmarkStdJsonEncode-8           24490         49345 ns/op       47409 B/op         10 allocs/op
BenchmarkStdJsonDecode-8           20083         59593 ns/op       29000 B/op         15 allocs/op
PASS
ok      demo    6.166s

从我们这个benchmark结果可以看到,sonic要比标准库json包快3-4倍。

本文中代码可以到这里下载。

六. 参考资料

  • Roaring Bitmap : June 2015 report - https://es.slideshare.net/lemire/roaringprezi-49478534
  • Roaring Bitmap官网 - https://roaringbitmap.org/
  • Roaring Bitmap Spec - https://github.com/RoaringBitmap/RoaringFormatSpec
  • Roaring Bitmap Go实现 - https://github.com/RoaringBitmap/roaring
  • 字节跳动的sonic项目 - https://github.com/bytedance/sonic
  • paper: Consistently faster and smaller compressed bitmaps with Roaring - https://arxiv.org/pdf/1603.06549.pdf
  • 基于Bitmap的精确去重和用户行为分析 - http://ai.baidu.com/forum/topic/show/987701
  • paper: Roaring Bitmaps: Implementation of an Optimized Software Library - https://arxiv.org/pdf/1709.07821.pdf

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 - https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go 1.20新特性前瞻

本文永久链接 – https://tonybai.com/2022/11/17/go-1-20-foresight


在近期Russ Cox代表Go核心团队发表的“Go, 13周年”一文中,他提到了“在Go的第14个年头,Go团队将继续努力使Go成为用于大规模软件工程的最好的环境,将特别关注供应链安全,提高兼容性和结构化日志记录,当然还会有很多其他改进,包括profile-guided optimization等”。

当前正在开发的版本是Go 1.20,预计2023年2月正式发布,这个版本也将是Go在其第14个年头发布的第一个版本。很多人没想到Go真的会进入到Go 1.2x版本,而不是Go 2.x。记得Russ Cox曾说过可能永远也不会有Go2了,毕竟Go泛型语法落地这么大的语法改动也没有让Go1兼容性承诺失效。

目前Go 1.20版本正在如火如荼的开发中,很多gopher都好奇Go 1.20版本会带来哪些新特性?在这篇文章中,我就带大家一起去Go 1.20 milestone的issues列表中翻翻,提前看看究竟会有哪些新特性加入Go。

1. 语法变化

Go在其1.18版本迎来了自开源以来最大规模的语法变化,然后呢?就没有然后了。Go在语法演进上再次陷入沉寂,没错,这就是Go长期以来坚持的风格。

如果Go 1.20版本真有语法层面的变化,那估计就是这个issue了:“spec: allow conversion from slice to array”,即允许切片类型到数组类型的类型转换

在Go 1.20版本之前,我们以Go 1.19版本为例写下下面代码:

package main

import "fmt"

func main() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl) // 编译器报错:cannot convert sl (variable of type []int) to type [7]int
    fmt.Println(sl)
    fmt.Println(arr)
}

这段代码中,我们进行了一个[]int到[7]int的类型转换,但在Go 1.19版本编译器针对这个转换会报错!即不支持将切片类型显式转换数组类型。

在Go 1.20版本之前如果要实现切片到数组的转换,是有trick的,看下面代码:

func main() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var parr = (*[7]int)(sl)
    var arr = *(*[7]int)(sl)
    fmt.Println(sl)  // [1 2 3 4 5 6 7]
    fmt.Println(arr) // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(sl)    // [11 2 3 4 5 6 7]
    fmt.Println(arr)   // [1 2 3 4 5 6 7]
    fmt.Println(*parr) // [11 2 3 4 5 6 7]
}

该trick的理论基础是Go允许获取切片的底层数组地址。在上面的例子中parr就是指向切片sl底层数组的指针,通过sl或parr对底层数组元素的修改都能在对方身上体现出来。但是arr则是底层数组的一个副本,后续通过sl对切片的修改或通过parr对底层数组的修改都不会影响arr,反之亦然。

不过这种trick语法还不是那么直观!于是上面那个“允许将切片直接转换为数组”的issue便提了出来。我们在go playground上选择“go dev branch”便可以使用最新go tip的代码,我们尝试一下最新语法:

func main() {
    var sl = []int{1, 2, 3, 4, 5, 6, 7}
    var arr = [7]int(sl)
    var parr = (*[7]int)(sl)
    fmt.Println(sl)   // [1 2 3 4 5 6 7]
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    sl[0] = 11
    fmt.Println(arr)  // [1 2 3 4 5 6 7]
    fmt.Println(parr) // &[11 2 3 4 5 6 7]
}

我们看到直接将sl转换为数组arr不再报错,但其语义与前面的“var arr = ([7]int)(sl)”语义是相同的,即返回一个切片底层数组的副本,arr不会受到后续切片元素变化的影响。

不过这里也有个约束,那就是转换后的数组长度要小于等于切片长度,否则会panic:

var sl = []int{1, 2, 3, 4, 5, 6, 7}
var arr = [8]int(sl) // panic: runtime error: cannot convert slice with length 7 to array or pointer to array with length 8

在写本文时,该issue尚未close,不过进入最终Go 1.20版本应该不是大问题。

2. 编译器/链接器和其他工具链

1) profile-guided optimization

Go编译器团队一直致力于对Go编译器/链接器的优化,这次在Go 1.20版本中,该团队很大可能会给我们带来“profile-guided optimization”

什么是“profile-guided optimization”呢?原先Go编译器实施的优化手段,比如内联,都是基于固定规则决策的,所有信息都来自编译的Go源码。而这次的“profile-guided optimization”顾名思义,需要源码之外的信息做“制导”来决定实施哪些优化,这个源码之外的信息就是profile信息,即来自pprof工具在程序运行时采集的数据,如下图(图来自profile-guided optimization设计文档)所示:

因此pgo优化实际上是需要程序员参与的,程序员拿着程序到生产环境跑,程序生成的profile性能采集数据会被保存下来,然后这些profile采集数据会提供给Go编译器,以在下次构建同一个程序时辅助优化决策。由于这些profile是来自生产环境或模拟生产环境的数据,使得这种优化更有针对性。并且,Google数据中心其他语言(C/C++)实施PGO优化的效果显示,优化后的性能保守估计提升幅度在5%~15%。

和其他新引入的特性一样,Go 1.20将包含该特性,但默认并不开启,我们可以手动开启进行体验,未来版本,pgo特性才会默认为auto开启。

2) 大幅减小Go发行版包的Size

随着Go语言的演进,Go发行版的Size也在不断增加,从最初的几十M到如今的上百M。本地电脑里多安装几个Go版本,(解压后)几个G就没有了,此外Size大也让下载时间变得更长,尤其是一些网络环境不好的地区。

为什么Go发行版Size越来越大呢?这很大程度是因为Go发行版中包含了GOROOT下所有软件包的预编译.a文件,以go 1.19的macos版本为例,在\$GOROOT/pkg下,我们看到下面这些.a文件,用du查看一下占用的磁盘空间,达111M:

$ls
archive/    database/   fmt.a       index/      mime/       plugin.a    strconv.a   time/
bufio.a     debug/      go/     internal/   mime.a      reflect/    strings.a   time.a
bytes.a     embed.a     hash/       io/     net/        reflect.a   sync/       unicode/
compress/   encoding/   hash.a      io.a        net.a       regexp/     sync.a      unicode.a
container/  encoding.a  html/       log/        os/     regexp.a    syscall.a   vendor/
context.a   errors.a    html.a      log.a       os.a        runtime/    testing/
crypto/     expvar.a    image/      math/       path/       runtime.a   testing.a
crypto.a    flag.a      image.a     math.a      path.a      sort.a      text/

$du -sh
111M    .

而整个pkg目录的size为341M,占Go 1.19版本总大小495M的近70%。

于是在Go社区提议下,Go团队决定从Go 1.20开始发行版不再为GOROOT中的大多数软件包提供预编译的.a文件,新版本将只包括GOROOT中使用cgo的几个软件包的.a文件。

因此Go 1.20版本中,GOROOT下的源码将像其他用户包那样在构建后被缓存到本机cache中。此外,go install也不会为GOROOT软件包安装.a文件,除非是那些使用cgo的软件包。这样Go发行版的size将最多减少三分之二。

取而代之的是,这些包将在需要时被构建并缓存在构建缓存中,就像已经为GOROOT之外的非主包所做的那样。此外,go install也不会为GOROOT软件包安装.a文件,除非是那些使用cgo的软件包。这些改变是为了减少Go发行版的大小,在某些情况下可以减少三分之二。

3) 扩展代码覆盖率(coverage)报告到应用本身

想必大家都用过go test的输出过代码覆盖率,go test会在unit test代码中注入代码以统计unit test覆盖的被测试包路径,下面是代码注入的举例:

func ABC(x int) {
    if x < 0 {
        bar()
    }
}

注入代码后:

func ABC(x int) {GoCover_0_343662613637653164643337.Count[9] = 1;
  if x < 0 {GoCover_0_343662613637653164643337.Count[10] = 1;
    bar()
  }
}

像GoCover_xxx这样的代码会被放置到每条分支路径下。

不过go test -cover也有一个问题,那就是它只是适合针对包收集数据并提供报告,它无法针对应用整体给出代码覆盖度报告。

Go 1.20版本中有关的“extend code coverage testing to include applications”的proposal就是来扩展代码覆盖率的,可以支持对应用整体的覆盖率统计和报告。

该特性在Go 1.20版本中也将作为实验性特性,默认是off的。该方案通过go build -cover方式生成注入了覆盖率统计代码的应用程序,在应用执行过程中,报告会被生成到指定目录下,我们依然可以通过go tool cover来查看这个整体性报告。

此外,新proposal在实现原理上与go test -cover差不多,都是source-to-source的方案,这样后续也可以统一维护。当然Go编译器也会有一些改动。

4) 废弃-i flag

这个是一个早计划好的“废弃动作”。自从Go 1.10引入go build cache后,go build/install/test -i就不会再将编译好的包安装到\$GOPATH/pkg下面了。

3. Go标准库

1) 支持wrap multiple errors

Go 1.20增加了一种将多个error包装(wrap)为一个error的机制,方便从打包后的错误的Error方法中一次性得到包含一系列关于该错误的相关错误的信息。

这个机制增加了一个(匿名)接口和一个函数:

interface {
    Unwrap() []error
}

func Join(errs ...error) error

同时增强了像fmt.Errorf这样的函数的语义,当在Errorf中使用多个%w verb时,比如:

e := errors.Errorf("%w, %w, %w", e1, e2, e3)

Errorf将返回一个将e1, e2, e3打包完的且实现了上述带有Unwrap() []error方法的接口的错误类型实例。

Join函数的语义是将传入的所有err打包成一个错误类型实例,该实例同样实现了上述带有Unwrap() []error方法的接口,且该错误实例的类型的Error方法会返回换行符间隔的错误列表。

我们看一下下面这个例子:

package main

import (
    "errors"
    "fmt"
)

type MyError struct {
    s string
}

func (e *MyError) Error() string {
    return e.s
}

func main() {
    e1 := errors.New("error1")
    e2 := errors.New("error2")
    e3 := errors.New("error3")
    e4 := &MyError{
        s: "error4",
    }
    e := fmt.Errorf("%w, %w, %w, %w", e1, e2, e3, e4)

    fmt.Printf("e = %s\n", e.Error()) // error1 error2, error3, error4
    fmt.Println(errors.Is(e, e1)) // true

    var ne *MyError
    fmt.Println(errors.As(e, &ne)) // true
    fmt.Println(ne == e4) // true
}

我们首先在Go 1.19编译运行上面程序:

e = error1 %!w(*errors.errorString=&{error2}), %!w(*errors.errorString=&{error3}), %!w(*main.MyError=&{error4})
false
false
false

显然Go 1.19的fmt.Errorf函数尚不支持多%w verb。

而Go 1.20编译上面程序的运行结果为:

e = error1 error2, error3, error4
true
true
true

将fmt.Errorf一行换为:

e := errors.Join(e1, e2, e3, e4)

再运行一次的结果为:

e = error1
error2
error3
error4
true
true
true

即Join函数打包后的错误类型实例类型的Error方法会返回换行符间隔的错误列表。

2) 新增arena实验包

Go是带GC语言,虽然Go GC近几年持续改进,绝大多数场合都不是大问题了。但是在一些性能敏感的领域,GC过程占用的可观算力还是让应用吃不消。

降GC消耗,主要思路就是减少堆内存分配、减少反复的分配与释放。Go社区的某些项目为了减少内存GC压力,在mmaped内存上又建立一套GC无法感知到的简单内存管理机制并在适当场合应用。但这些自实现的、脱离GC的内存管理都有各自的问题。

Go 1.18版本发布前,arena这个proposal就被提上了日程,arena包又是google内部的一个实验包,据说效果还不错的(在改进grpc的protobuf反序列化实验上),可以节省15%的cpu和内存消耗。但proposal一出,便收到了来自各方的comment,该proposal在Go 1.18和Go 1.19一度处于hold状态,直到Go 1.20才纳入到试验特性,我们可以通过GOEXPERIMENT=arena开启该机制。

arena包主要思路其实是“整体分配,零碎使用,再整体释放”,以最大程度减少对GC的压力。关于arena包,等进一步完善后,后续可能会有专门文章分析。

3) time包变化

time包增加了三个时间layout格式常量,相信不用解释,大家也知道如何使用:

    DateTime   = "2006-01-02 15:04:05"
    DateOnly   = "2006-01-02"
    TimeOnly   = "15:04:05"

time包还为Time增加了Compare方法,适用于time之间的>=和<=比较:

// Compare returns -1 if t1 is before t2, 0 if t1 equals t2 or 1 if t1 is after t2.
func (t1 Time) Compare(t2 Time) int

此外,time包的RFC3339时间格式是使用最广泛的时间格式,其解析性能在Go 1.20中得到优化,提升了70%左右,格式化性能提升30%

4. 其他

5. 参考资料

  • Go 1.20 milestone – https://github.com/golang/go/milestone/250
  • Exploring Go’s Profile-Guided Optimizations – https://www.polarsignals.com/blog/posts/2022/09/exploring-go-profile-guided-optimizations/
  • What’s coming to go 1.20 – https://twitter.com/mvdan_/status/1588242469577117696

“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2022年,Gopher部落全面改版,将持续分享Go语言与Go应用领域的知识、技巧与实践,并增加诸多互动形式。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

我爱发短信:企业级短信平台定制开发专家 https://51smspush.com/。smspush : 可部署在企业内部的定制化短信平台,三网覆盖,不惧大并发接入,可定制扩展; 短信内容你来定,不再受约束, 接口丰富,支持长短信,签名可选。2020年4月8日,中国三大电信运营商联合发布《5G消息白皮书》,51短信平台也会全新升级到“51商用消息平台”,全面支持5G RCS消息。

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats