我们有这么一段业务代码,在 Gin 的 API Handler 中,开了一个子 goroutine 写 DB,代码大概是这样:

package main

import (
    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

var db *gorm.DB

func ServerHandler(c *gin.Context) {
    // 一些旁路逻辑,为了不影响接口耗时,在子goroutine中执行
    go func() {
       db.WithContext(c).Exec("update xxx")
    }()
    // 一些后置逻辑
}

代码在测试阶段一直没啥问题,但是一上线立马出现了大面积的 panic。panic 的栈也非常奇怪,挂在了 mysql driver 里面:

panic: sync/atomic: store of nil value into Value
goroutine 357413 [running]:
sync/atomic.(*Value).Store(0xc004097ef0, {0x0,0x0})

/usr/local/go/src/sync/atomic/value.go:47 +0xeb
github.com/go-sql-driver/mysql.(*atomicError).Set(..)
/root/go/pkg/mod/github.com/go-sql-driver/mysql@v1.6.0/utils.go:831
github.com/go-sql-driver/mysql.(*mysqlConn).cancel(0xc004e6fc20, {0x0, 0x0})
/root/go/pkg/mod/github.com/go-sql-driver/mysql@v1.6.0/connection.go:435 +0x3d
github.com/go-sql-driver/mysql.(*mysqlConn).startWatcher.func1()
/root/go/pkg/mod/github.com/go-sql-driver/mysql@v1.6.0/connection.go:622 +0x192
created by github.com/go-sql-driver/mysql.(*mysqlConn).startWatcher
/root/go/pkg/mod/github.com/go-sql-driver/mysql@v1.6.0/connection.go:611 +0x105
阅读全文 »

几乎世界上每个 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、kubernetes 等也都可以看到对 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 都可以。

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

阅读全文 »