几乎世界上每个 Golang 程序员都踩过一遍 for 循环变量的坑,而这个坑的解决方案已经作为实验特性加入到了 Go 1.21 中,并且有望在 Go 1.22 中完全开放。
举个例子,有这么段代码:

var ids []*int
for i := 0; i < 10; i++ {
	ids = append(ids, &i)
}

for _, item := range ids {
	println(*item)
}

可以试着在 playgound 里面运行下:go.dev/play/p/O8MVGtueGAf

答案是:打印出来的全是 10。

这个结果实在离谱。原因是因为在目前 Go 的设计中,for 中循环变量的定义是 per loop 而非 per iteration。也就是整个 for 循环期间,变量 i 只会有一个。以上代码等价于:

var ids []*int
var i int
for i = 0; i < 10; i++ {
	ids = append(ids, &i)
}

同样的问题在闭包使用循环变量时也存在,代码如下:

阅读全文 »

Bigcache是用Golang实现的本地内存缓存的开源库,主打的就是可缓存数据量大,查询速度快。 在其官方的介绍文章《Writing a very fast cache service with millions of entries in Go》一文中,明确提出了bigcache的设计目标:

  1. 多: 缓存的元素数量非常大,可以达到百万级或千万级。
  2. 快: 对延迟有非常高的要求,平均延迟要求在5毫秒以内。redis、memcached之类的就不考虑在内了,毕竟用Redis还要多走一遍网络IO。
  3. 稳: 99.9分位延迟应在10毫秒左右,99.999分位延迟应在400毫秒左右。

目前有许多开源的cache库,大部分都是基于map实现的,例如go-cache,ttl-cache等。bigcache明确指出,当数据量巨大时,直接基于map实现的cache库将出现严重的性能问题,这也是他们设计了一个全新的cache库的原因。

本文将通过分析bigcache v3.1.0的源码,揭秘bigcache如何解决现有map库的性能缺陷,以极致的性能优化,实现超高性能的缓存库。

阅读全文 »

最近发现 Golang 标准库竟然自带了 varint 的实现,代码位置在 encoding/binary/varint.go,这个跟protobuf里面的varint实现基本是一致的。刚好借助 golang 标准库的 varint 源码,我们来系统地学习和梳理下 varint。

熟悉 protobuf 的人肯定对 varint 不陌生,protobuf 里面除了带 fix (如 fixed32、fixed64) 之外的整数类型, 都是 varint 编码。

varint 的出现主要是为了解决两个问题:

  1. 空间效率:以 uint64 类型为例,可以表示的最大值为 18446744073709551615。然而在实际业务场景中,我们通常处理的整数值远小于 uint64 的最大值。假设在我们的业务中,需要处理的整数值仅为 1,但在网络传输过程中,我们却需要使用 8 个字节来表示这个值。这就导致了大量的空间浪费,因为大部分字节并没有实际存储有效的信息。varint 编码通过使用可变长度的字节序列来表示整数,使得小的整数可以用更少的字节表示,提高空间效率。
  2. 兼容性:varint 使得我们可以在不改变编码 / 解码逻辑的情况下,处理不同大小的整数。这意味着我们可以在不破坏向后兼容性的情况下,将一个字段从较小的整数类型(如 uint32)升级到较大的整数类型(如 uint64)

本文将通过分析 Golang 标准库自带的 varint 源码实现,介绍 varint 的设计原理以及Golang标准库是如何解决 varint 在编码负数时遇到的问题。

阅读全文 »

sync.Pool 是 Golang 内置的对象池技术,可用于缓存临时对象,避免因频繁建立临时对象所带来的消耗以及对 GC 造成的压力。

在许多知名的开源库中,都可以看到 sync.Pool 的大量使用。例如,HTTP 框架 Gin 用 sync.Pool 来复用每个请求都会创建的 gin.Context 对象。 在 grpc-Go、kubernates 等也都可以看到对 sync.Pool 的身影。

但需要注意的是,sync.Pool 缓存的对象随时可能被无通知的清除,因此不能将 sync.Pool 用于存储持久对象的场景。

sync.Pool 作为 goroutine 内置的官方库,其设计非常精妙。sync.Pool 不仅是并发安全的,而且实现了 lock free,里面有许多值得学习的知识点。

本文将基于 go-1.16 的源码 对 sync.Pool 的底层实现一探究竟。

阅读全文 »

问题是这样的:我在代码里面调用了 os.Chmod("test.txt", 777),希望把该文件的读写及执行权限对所有用户开放。
执行完代码,顺手 ls 看了下。如下:

$ ls -l test.txt
-r----x--x  1 cyhone  1085706827  0 Jun 20 13:27 test.txt

结果出乎意料,不仅文件权限没有按预期的变成 rwxrwxrwx。反而执行完后,当前用户就只剩可读权限了,其他用户就只有可执行权限同时无读写权限。

因为这实在是一个简单又愚蠢的错误,所以先直接给出结论:

  1. 在 C 语言和 Go 语言中,如果想要将文件权限形式修改为 rwxrwxrwx,需要写成 0777,而非 777
  2. 0777 是八进制格式,777 是十进制格式。在用 Go 语言表示此类权限的时候,如果要对标 chmod 命令的表示形式,用八进制表示更方便和准确点。
  3. 如果不是在代码里,而是在命令行直接调 chmod 的话,那 0777777 都可以。

这个问题虽然非常简单,但尴尬的是我还踩了坑,所以把这个问题及原因分享出来。

阅读全文 »

我们在开发 HTTP Server 的时候,经常有对接口内容做缓存的需求。例如,对于某些热点内容,我们希望做 1 分钟内的缓存。短期内缓存相同内容不会对业务造成实质影响,同时也会降低系统的整体负载。

有时我们需要把缓存逻辑放在 Server 内部,而非网关侧如 Nginx 等,是因为这样我们可以根据需要便捷地清除缓存,或者可以使用 Redis 等其他存储介质作为缓存后端。

这样的缓存场景无非是有缓存时从缓存取,无缓存时从下游服务取,并将数据放入缓存中。这其实是个非常通用的逻辑,应该可以将其抽象出来。从而缓存逻辑无需侵入进业务代码。

cache

我常用的 HTTP 框架是 golang 的 gin。gin 官方就有一个 cache 组件:github.com/gin-contrib/cache,但这个 cache 组件无论在性能还是接口设计上,都有一些不足之处。

因此,我重新设计了一套 cache 中间件: gin-cache。 从压测结果来看,其性能相比于 gin-contrib/cache 明显提升。

阅读全文 »
0%