大家好,我是煎鱼。
Go1.23 吵来吵去的,现在已经基本尘埃落定了。在我开始写这个新版本特性系列时,Go1.23 rc2 已经发布了有一周多:
今天我们分享的是新的标准库 unique 的介绍和快速入门。
背景
基于 Go unique 官方提案,我简化了一下内容。要做这个主要原因是:Go 缺乏运行时的驻留支持,这与其他语言存在差距。
多年来,Go 社区对弱映射(weak map)和字符串驻留(string interning)的需求的需求已在过去几年的 GitHub 问题中有所体现。
来自 go/issues/62483 给出的动机:
虽然社区中要求这些功能的人很少,但我们看到了 go4.org/intern 包没有内置支持这一功能的后果(简单看了下代码。应该指的是:unsafe 骚操作,会动到 Go 的垃圾回收器逻辑,理论上要跟着 Go 版本调整。挺折腾!)
字符串驻留是什么?
前面的背景存在一些专业名词。尤其是字符串驻留是什么?可能会看的有些懵。这里我们补充一下基础知识。
根据 GPT-4 的概要总结如下:
1、字符串驻留(string interning)是一种在计算机科学中用于优化内存使用和提高性能的技术。
2、主要思想是对于每一个唯一的字符串值,只存储一个副本,这些字符串必须是不可变的。
3、以下是有关字符串驻留的一些关键点:
定义:
- 字符串驻留是一种存储技术,确保每个独特的字符串值在内存中只存在一个副本。
- 这意味着如果两个字符串具有相同的值,它们将共享同一个内存地址,而不是每个字符串都占用独立的内存空间。
优势:
- 节省内存:通过避免重复的字符串副本,可以显著减少内存消耗。
- 提高性能:字符串比较操作可以通过比较内存地址而不是逐字符比较来实现,从而加快速度。
标准库 unique
标准库 unique,文档非常的短小精悍。这次 Go 官方连个 example 都没有直接给。
在该标准库,unique 会对所有被添加的值进行全局的并发安全缓存,以确保值的唯一性和有效重用。会做到运行时的驻留支持,以此达到开销较佳。
API 如下:
Handle
是 T 类型值的全局唯一标识。Make
方法为 T 类型的值返回一个全局唯一的Handle
。Handle[T].Value
方法返回产生Handle
的 T 值的浅拷贝副本。
一眼看到底。比较直接,这个标准库就是围绕着 unique.Handle
来用。
到底怎么用和有什么好处,通过一个例子就能快速了解优点了。这个新特性代码片段来自 @Anton 大佬的分享。(还是社区的力量大,不像官方文档一个例子都不给)
在以前我们要用 Go 写一个随机词生成器,可以这么写。
生成单词的代码如下:
func wordGen(nDistinct, wordLen int) func() string {
vocab := make([]string, nDistinct)
for i := range nDistinct {
word := randomString(wordLen)
vocab[i] = word
}
return func() string {
word := vocab[rand.Intn(nDistinct)]
return strings.Clone(word)
}
}
func randomString(n int) string {
// 脑子进煎鱼了
const letters = "eddycjyabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
ret := make([]byte, n)
for i := 0; i < n; {
b := make([]byte, 1)
if _, err := rand.Read(b); err != nil {
panic(err)
}
ret[i] = letters[int(b[0])%len(letters)]
i++
}
return string(ret)
}
基于上述方法,我们生成 10000 个单词。看看要使用多少内存。
代码如下:
var words []string
func main() {
const nWords = 10000
const nDistinct = 100
const wordLen = 40
generate := wordGen(nDistinct, wordLen)
memBefore := getAlloc()
words = make([]string, nWords)
for i := range nWords {
words[i] = generate()
}
memAfter := getAlloc()
memUsed := memAfter - memBefore
fmt.Printf("Memory used: %dKB\n", memUsed/1024)
}
func getAlloc() uint64 {
var m runtime.MemStats
runtime.GC()
runtime.ReadMemStats(&m)
return m.Alloc
}
本地运行的输出结果是:
// 煎鱼
$ go run main.go
Memory used: 622KB
现在改用 Go1.23 的新标准库 unique 来写。代码如下:
var words []unique.Handle[string]
func main() {
const nWords = 10000
const nDistinct = 100
const wordLen = 40
generate := wordGen(nDistinct, wordLen)
memBefore := getAlloc()
words = make([]unique.Handle[string], nWords)
for i := range nWords {
words[i] = unique.Make(generate())
}
memAfter := getAlloc()
memUsed := memAfter - memBefore
fmt.Printf("Memory used: %dKB\n", memUsed/1024)
}
输出结果:
// 煎鱼在 Go Playground 执行的结果
Memory used: 95KB
内存使用从 622KB 减少到 95KB!优化效果非常明显。而且生成的数量越多,理论上优化效果更大。
为什么那么快就接纳了
如果有经常关注 Go 社区响应的同学,看到背景后可能会想到。这么小众的场景(提出者自己说的),居然这么一帆风顺的就直接过了。还很快来到了正式版本?
其实能对垃圾回收(GC)做这么骚操作,还不被喷的。社区上回会这么做的人不多。
因此无论是之前的 go4org/intern 库,还是这次新标准库 unique 提案。相关作者都是 Google 和 Go 团队里的关联者。当然推进的极快了!
总结
本次新标准库 unique 是基于 go4org/intern 库内化而来,虽然都是 Google 自己人开发的。但是该库的加入对于 Go 在运行时的驻留支持增添了一笔新力量。
一句题外话,官方对于自己的人的库真的是一路绿灯。文档和说明都非常的简洁。莫非是压根不想让别人用吧。
- 本文作者:煎鱼
- 公众号:脑子进煎鱼了
- 联系方式:cJY0728(加我拉你进技术交流群)
文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。