我的博客之前多以技术文章为主(虽然现在基本是年更了),很少有自我表达的部分。主要我认为我的想法并不独特到可以分享出来让大家看到。

但最近一段时间看到一些观点,让我决定做出一些改变。

C++17 中引入了 std::any,可以非常方便地将任意类型的变量放到其中,做到安全的类型擦除。然而万物皆有代价,这种灵活性背后必然伴随着性能取舍。

std::any 的实现本身也并不复杂,本文将基于 libstd++ 标准库源码 深入解析其实现机制与性能开销。

我们在使用 C++ 的时候,有时会需要在类的内部获取自身的 shared_ptr,这就会用到 std::enable_shared_from_this。在实际使用过程中,std::enable_shared_from_this 有三个陷阱需要注意:

  1. 不能在构造函数中使用 shared_from_this(), 否则会抛出 std::bad_weak_ptr 异常。对应下面情况 1。
  2. 创建的对象必须由 shared_ptr 管理,shared_from_this() 才能生效,否则也会报 std::bad_weak_ptr 异常。对应下面情况 2。
  3. 对应类必须 public 继承 std::enable_shared_from_this, 不能是 protected 或 private 继承,否则也会报 std::bad_weak_ptr 异常。对应下面情况 3。

以上 case 均可以通过 wandbox 复现。

那么为什么会有这些限制呢?本文将从 std::enable_shared_from_this 的源码角度解读其原因。(本文基于 clang libc++ 的源码实现进行解读, 代码地址:shared_ptr.h#L1433)

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 中完全开放。
举个例子,有这么段代码:

1
2
3
4
5
6
7
8
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 只会有一个。以上代码等价于:

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

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