2025-08-20 21:00:54
2025-08-20 期
原文链接:https://mp.weixin.qq.com/s/MMgJa-5EVsINXuMvN0SF_Q
一方面,我越来越擅长自学,也养成了独立思考的能力;另一方面,我变得孤僻,不善与人建立连接。面对人群,我总是局促不安,不知如何表达真实的自己;我害怕袒露想法,总觉得无人理解,也无人愿意理解。
人必须与外部世界建立连接,而表达,就是这场连接中的第一道桥梁。
我才明白,表达不是可有可无的能力,而是一门人生的必修课
真正的表达,不是为了炫耀,也无关于技巧,而是一种自我的袒露。你越愿意将内在推向外部,你就越清楚自己是谁、想要什么、和他人有什么不同。而你越害怕表达、越不敢袒露,你的“自我”就越容易被模糊、被裹挟、被湮没。久而久之,甚至你自己也不认识自己了。
表达像是一面镜子,一面不断校准自我认知的镜子。你表达得越多,越真实,就越靠近那个真实的“自己”
这篇文章非常触动我。因为我自己在生活中就是一个不愿意表达自我的人。似乎在传统教育中并不鼓励自我表达,我们习惯标准答案,过度的自我表达会被认定为标新立异,它会带来需要羞耻感。但等到自己想要真正表达自我的时候,就会发现无法准确的表达出自己的想法,甚至陷入自我怀疑的沼泽。
这篇文章让我意识到,表达自我其实是一个思考自我的练习方式。通过不断的练习表达,那个真实的我才会浮现出来。
所以表达自我很重要,哪怕无人倾听。
原文链接:https://blog.jsbarretto.com/post/software-is-joy
这篇文章讨论了经典的程序员要不要造“轮子”的问题,文中有一些非常有价值的观点和建议:
最后文章也给了一些玩具项目的例子:
原文链接:https://laike9m.com/blog/people-die-but-long-live-github,122/
如果你希望存储一段信息,让 100 年后的人也能访问,要怎么做?
三体中有类似的想法:如果人类灭亡,最好的保存文明的方式是什么?答案是:“刻在石头上”
但在一百年的时间尺度上,文章作者认为 GitHub 是存储信息的最好方式:
越来越多的人会把自己的信息搬到 GitHub 上,依托 GitHub 实现曾经人们可望而不可及的"永生"。
几十几百年后,GitHub 将成为世界上最大的数字公墓,注册用户大部分都已去世,然而个人主页,项目,commit 历史 还述说着他们生前做过的事——就比如 Joe 的博客。
这虽然是个比较 creepy 的推论,但从另一个角度想,却证明了人类的巨大进步:对抗死亡是人类文明的永恒主题,而我们已经实现了阶段性胜利。
这篇文章是 2019 年写的,当时大模型尚未出现。如今 Deepseek 和 chatgpt 几乎成为知识工作者的标配的时候。保存自己的数字内容,自己独特的数字历史上下文更有意义。
看完这篇文章后,我也决定之后把博客中的所有 md 文件都在 Github 上公开。
tk 教主分享在微博上的文章:“粉丝群有人问有没有关于怎么带团队的书。我觉得把《怎样当好一名师长》看明白就够了。这也是我非常推崇的一篇文章。”
原文链接:https://mp.weixin.qq.com/s/yUzJY7taYx3-9H0OeMiLrg
很多人应该在 B 站上看过稚晖君的硬核科技视频。这篇文章算是稚晖君的人生小传,值得一看。
其中有一句话引起了我的思考:
彭志辉建议大家想想自己除了各种琐事,真正投入有效学习的时间究竟有多少。
我不禁反思,自己在决心要研究的方向实际究竟花了多久。应该聚焦在少数的方向,并真的投入了有效时间。
来源:B 站纪录片《安藤忠雄:武士建筑师》 https://www.bilibili.com/bangumi/play/ep120908
安藤忠雄:要锻炼出创造性的身体,去看电影、听音乐会、去美术馆、看别人的建筑,然后要有想做得比他们更好的意愿,超越前人的勇气,体力衰退了,竞争意识就会变弱。 没有了创造性的身体,竞争意识就没了。这两个是一回事,要同时锻炼身体和意志
来源:https://yro.slashdot.org/comments.pl?sid=23765914&cid=65583466
8 月 11 号是苹果创始人史蒂夫.沃兹尼亚克的生日,他在这个站点上评论:
我捐出了所有来自苹果的财富,因为财富和权力并非我的追求。我享受生活的乐趣——在我的出生地圣何塞,我资助了许多重要的博物馆和艺术团体,他们以我的名字命名了一条街道以示认可。如今我从事公开演讲并成为行业顶尖,虽然不清楚具体资产数额,但经过 20 年的演讲事业,可能积累了约 1000 万美元和几处房产。我从不寻找任何避税手段,通过劳动获得的收入缴纳约 55%的综合税负。我是世界上最快乐的人。对我而言,生活从来不是关于成就,而是关于快乐,那正是笑容减去愁容的简单公式。这些人生理念在我 18 到 20 岁时就已形成,而我始终坚守至今。
最近看了《成为乔布斯》这本书以及听了播客半拿铁的《苹果简史》。对沃兹的印象就是他和乔布斯最开始的组合就是天使+魔鬼,沃兹是毫无疑问的赤子。 沃兹的这个留言更加印证了这个印象。
贝索斯的遗憾最小化原则:我们做任何决定,最终都有可能会后悔会遗憾,但是面对多个选择时,我们应该选择让自己后悔或者遗憾最少的那个。
原文链接:https://weibo.com/1401527553/PDdCPaKsk
tombkeeper: 股市是世界上最可爱的东西。对于股市你可以随便怎么想,怎么想都可以,甚至觉得“川大智胜”和川普有关系都可以。
一切观点、一切想法都可以掏钱验证。如果你是对的,那就不光是对的,还能赚钱。所以如果你这么坚定地相信,就没道理不掏钱。除非,你其实也没那么相信。
社会是一个周期更长的股市。每个人最终都会在这个股市里得到和自己的想法相匹配的回报,一切想法也最终都会变成收益或亏损。而且社会这个股市> 是由不得你不掏钱的,退不了,缩不了,怂不了。
不光是钱,你的整个人生都会被投入进去。所以收获的也不光是钱,是你整个人生的悲喜荣哀。所以,你可以随便怎么想,怎么舒服怎么想,或者,认真地思考自己的每一个判断,每一个决定,想清楚自己要的到底是简单、爽,还是有更想要的东西。
在股市人声鼎沸的当下,很有思考意义。
2025-08-20 20:21:54
我的博客之前多以技术文章为主(虽然现在基本是年更了),很少有自我表达的部分。主要我认为我的想法并不独特到可以分享出来让大家看到。
但最近一段时间看到一些观点,让我决定做出一些改变。
第一: 公众号文章 《人要大量地表达自己》,中间有几个观点非常触动我
真正的表达,不是为了炫耀,也无关于技巧,而是一种自我的袒露。你越愿意将内在推向外部,你就越清楚自己是谁、想要什么、和他人有什么不同。而你越害怕表达、越不敢袒露,你的“自我”就越容易被模糊、被裹挟、被湮没。久而久之,甚至你自己也不认识自己了。
表达像是一面镜子,一面不断校准自我认知的镜子。你表达得越多,越真实,就越靠近那个真实的“自己”
第二: 李继刚在 即刻中提到: 可以建立一个“人生周报”,让发生过的思考真正「存在」。
第三:最近看的《打造第二大脑》这本书中提到尽早输出,可以尽快得到反馈。
目前短期的想法是,月刊会是我收藏夹和笔记的定期整理。我把其中比较有价值的、让人耳目一新的或者受人启发的内容挑选出来。
我并不确定这些内容是否其他人同样感兴趣或者觉得有价值,但希望它是一个关于自我思考的好的开始。
至于发布频率,虽然说是月刊,其实会是不定期的更新。
另外,下决定做月刊的时候,又习惯性的准备把文章发布时间地拖延到月底或者下月初。后来决定尽早做,时间节点并不重要,尽早开始更加重要。
在整理资料的时候,也发现了类似的想法:
最后分享一些我自己经常的周刊或者类周刊的读物:
我之后自己的月刊,也会在这个索引页上定期更新。
2025-03-04 09:41:54
C++17 中引入了 std::any
,可以非常方便地将任意类型的变量放到其中,做到安全的类型擦除。然而万物皆有代价,这种灵活性背后必然伴随着性能取舍。
std::any 的实现本身也并不复杂,本文将基于 libstd++ 标准库源码 深入解析其实现机制与性能开销。
std::any 需要解决的核心问题在于:
从 libstd++ 源码中提取的关键类结构如下
1 |
class any { |
可以看到有两个核心变量:
_M_storage
:负责存储数据值本身或者指针。_M_manager
:函数指针,负责指向具体类型 template class 的实现,其中包含了类型信息。我们先看 _M_storage
的实现:
1 |
union _Storage |
_Storage
类是一个 union 实现。里面包含两个属性:_M_ptr
和长度为 sizeof(_M_ptr)
的 char 数组 _M_buffer
。即长度为指针大小,在 64 位机器下,_M_buffer
的长度是 8。
那么,在什么情况下分别使用 _M_ptr
和 _M_buffer
呢?主要通过以下模板变量进行编译期决策。
1 |
template<typename _Tp, typename _Safe = is_nothrow_move_constructible<_Tp>, bool _Fits = (sizeof(_Tp) <= sizeof(_Storage)) && (alignof(_Tp) <= alignof(_Storage))> |
简单来说:_Tp 可以无异常移动构造 && _Tp 能完全放入 _Storage 中
。
这是一个非常典型的 SOO(Small Object Optimization 小对象优化)。即:对于小尺寸对象,直接在容器自身的连续内存中 (通常为栈内存) 完成存储,这样可以避免在堆上开辟新的内存。
因此:
_M_buffer
中通过 placement new 创建对象。避免堆内存分配带来的性能开销,提升 CPU 缓存局部性(对高频访问的场景尤为重要)。_M_storage
存储对应的指针。但这个内存结构的设计,也存在着潜在的内存浪费:union 的内存等于最大字段的内存,因此即使在 std::any 中存储 1 字节的 char 类型变量,_M_storage
也需要 8 字节。
另外,我们发现在 _Storage
并未存储任何类型信息。但我们可以通过 std::any 的 type() 函数获取到对应的类型信息。这是如何做到呢?
接下来,我们看 _M_manager
的实现:
std::any 的做法非常巧妙,将所有需要类型信息的操作,都通过一个 template class 的 static 函数来实现。std::any 对象中只存储这个函数的指针,即 void (*_M_manager)(_Op, const any*, _Arg*)
。
1 |
template<typename _Tp> |
以 std::any 的 type() 函数实现为例, 代码如下:
1 |
const type_info& type() const noexcept |
我们可以看到,通过_M_manager
找到对应template class的具体实现,直接调用typeid(_Tp)
就可以获取到对应的 typeinfo 信息了。
但值得注意的是,在调用 _M_manager
函数的时候,额外传递了一个 enum 值 _Op_get_type_info
。
这是 std::any 的特殊设计,通过枚举值区分不同的逻辑,将所有需要类型信息的操作都整合到一个函数入口。这样做仅用一个函数指针即可,可以节省内存开销。
虽然 std::any 提供了极大的灵活性,且绝大部分场景下性能也够用。但根据我们对源码的深入分析,发现 std::any 的设计特点必然会带来一些额外的开销:
2025-01-03 22:41:54
我们在使用 C++ 的时候,有时会需要在类的内部获取自身的 shared_ptr,这就会用到 std::enable_shared_from_this
。在实际使用过程中,std::enable_shared_from_this
有三个陷阱需要注意:
以上 case 均可以通过 wandbox 复现。
那么为什么会有这些限制呢?本文将从 std::enable_shared_from_this 的源码角度解读其原因。(本文基于 clang libc++ 的源码实现进行解读, 代码地址:shared_ptr.h#L1433)
1 |
|
我把 enable_shared_from_this 的源码摘录下来,删掉了一些不太重要的逻辑以方便理解。代码如下:
1 |
template <class _Tp> |
从代码可以看出 enable_shared_from_this 核心的就是一个 weak_ptr 属性 __weak_this_
。而 shared_from_this 其实就是把 weak_ptr 转换成 shared_ptr。
那么问题来了,__weak_this_
是在什么时候设置呢?答案是:在创建 shared_ptr 对象的时候。
以下是 shared_ptr 中创建对象的逻辑,其中在 __enable_weak_this
中设置了 enable_shared_from_this 的 __weak_this_
属性。
1 |
template <class _Yp, class _CntrlBlk> |
在 __enable_weak_this
的实现中,因为 enable_shared_from_this 类里面将 shared_ptr<T>
设置为了 friend class。因此 shared_ptr 可以直接访问并设置 enable_shared_from_this 的 __weak_this_
属性。
同时,__enable_weak_this
使用 SFINAE 实现了一个模板匹配,即:只有当满足 __enable_if_t<is_convertible<_OrigPtr*, const enable_shared_from_this<_Yp>*>::value, int> = 0
时(即对应类可以转换成 enable_shared_from_this,也就是类 public 继承了 enable_shared_from_this), 才会设置 __weak_this_
。 否则会匹配到一个空实现。
1 |
// 匹配到 enable_shared_from_this |
解读完源码之后,一切情况非常明了。我们再回头看下文章刚开始提到的三个陷阱:
__weak_this_
属性,最终才能得到一个 shared_ptr 对象。所以在执行原始对象的构造函数时,__weak_this_
属性尚未设置,当然不能用 shared_from_this。__weak_this_
。__enable_weak_this
,从而设置 __weak_this_
。2024-05-06 17:25:54
我们有这么一段业务代码,在 Gin 的 API Handler 中,开了一个子 goroutine 写 DB,代码大概是这样:
1 |
package main |
代码在测试阶段一直没啥问题,但是一上线立马出现了大面积的 panic。panic 的栈也非常奇怪,挂在了 mysql driver 里面:
1 |
panic: sync/atomic: store of nil value into Value |
把 mysql driver 相关栈的源码扒出来,大概是这样:
1 |
func (mc *mysqlConn) startWatcher() { |
具体的故障现象大概明确了:
context.Done()
, 当 channel 返回时,将ctx.Err()
设置到原子变量里面。context.Done()
虽然返回了,ctx.Err()
却是 nil。这就导致了在 set 原子变量时直接 panic 了。这个问题非常难以理解,因为根据 context 的源码来看,只要context.Done()
返回了,ctx.Err()
就不可能是 nil。而且这个问题在测试环境无法复现,问题排查暂时陷入了僵局。
虽然 panic 的原因暂未查明,但是仔细看下这段业务逻辑,就可以看出来一些问题。
首先,我们需要知道这个 context 在什么时候会触发 Done,也就是什么时候 cancel 的。翻下 Golang HTTP Server 的源码,事情一目了然:
1 |
func (c *conn) serve(ctx context.Context) { |
在开始处理请求之前,HTTP Server 会创建一个 context 对象,在请求处理结束之后,会自动 cancel 这个 context。
也就是说:当 API Handler 的处理逻辑完成返回的时候,context 会主动 cancel。此时即使子 goroutine 的处理逻辑还没结束,db 请求也会取消。按照 mysql driver 的逻辑,应该会抛出来一个context canceled
的 Err。
翻了下测试环境的日志,的确有偶发的context canceled
。 之所以不是必现,是因为子 goroutine 之后还有后置的处理逻辑。如果子 goroutine 的处理逻辑快于接口的后续处理逻辑,那这个 Err 就不会触发。
实际上,这里业务代码对 Context 使用上出现了错误:在这个场景下,子 goroutine 的逻辑处理的生命周期实际上是和父层的逻辑完全没有关系,我们不需要用同一个 context 强行把两个逻辑的生命周期保持一致。
在这种情况下,子 goroutine 中可以用context.Background()
创建一个新的 context 对象 ,和外部接口主逻辑的 context 分离开,以免受到影响。
按照这个逻辑更改完成之后,测试环境没有了context canceled
错误,线上服务也正常恢复了。
问题虽然得到了解决,但是 panic 的原因还没有完全查明,问题的阴影仍然持续笼罩着:
继续深扒下源码,这次找到了 Gin 对请求的处理过程:在每个处理过程中,都有对sync.Pool
的使用。
对缓存的复用和清理一般是问题频发的根源,我们对这块着重进行了梳理,还真的找到了原因:
gin.Context
本质上是对c.Request.Context()
的封装。所有对 Context 的 Done、Err 方法调用,都会转发给c.Request.Context()
。sync.Pool
对gin.Context
进行对象复用。每次从sync.Pool
拿到一个 gin.Context 对象的时候,都会重置其 Request 属性。1 |
// ServeHTTP conforms to the http.Handler interface. |
1 |
// Done returns nil (chan which will wait forever) when c.Request has no Context. |
梳理下来,所有的情况都可以得到解释。简单来说:请求 1 中开的子 goroutine 持有的 context 对象,会被请求 2 复用,造成并发问题。
存在这样一种 case:请求1的子goroutine,在ctx.Done返回,并且要准备取ctx.Err之前。context刚好被复用,并且新的请求还没有结束。
ctx.Done
。整个外部处理逻辑结束,触发 HTTP Server 内部的 context cancel。此时,子 goroutine 中的ctx.Done
channel 返回,准备去取context.Err()
。同时请求 2 到来,复用了 context 对象。c.Request.Context().Err()
当然会返回 nil为什么测试环境很难复现: 测试环境请求非常稀疏:子 goroutine 在取ctx.Err()
之前,如果没有其他请求到来并复用这个 context,是不会出现问题的。
为了方便构造这种 case,我们需要复现两个充分必要条件:
ctx.Err()
之前的间隙,请求 2 复用其 context 对象,并重置 Request 对象。对于条件 1,我们需要简单了解下 sync.Pool 的原理,具体可以看我的另外一篇博客 《深度分析 Golang sync.Pool 底层原理》:
debug.SetGCPercent(0)
。因为每轮 GC 之后,sync.Pool 都会被强制清空。sync.Pool
会在每个 P 内部有一个私有对象和 localPool,只有设置为 1,才会保证一定可以复用上次请求的 context。对于条件 2,其实只要请求 QPS 足够大,基本是可以必现的。我们使用 sleep 协调下两个请求,以模拟这种 case。代码如下:
1 |
package main |
为了方便描述问题,这里还有个额外的情况没有说明:我们在使用 Gin 时开启了 ContextWithFallback
,这是在是在Gin的v1.8.1版本引入的。
如果你的Gin版本在 v1.8.1 之前或者 v1.8.1 之后并开启了 ContextWithFallback
,才会保证所有对gin.Context
的Done()
、Err()
函数的访问,全部转发给c.Request.Context()
。如果没有开启 ContextWithFallback
, 实际上ctx.Done()
channel 会永远阻塞, 并不会出现本文中的问题。
总结来说该问题的根源在于:不应该在子 goroutine 中继续使用gin.Context
,即使不会 panic,也会导致高概率的context.Canceled
错误。
我们之后应该如何避免:
方法一:其实可以将 gin 的 ContextWithFallback 设置为 false,这样这类问题都不会出现。
方法二:这种子 goroutine 的逻辑生命周期不需要和外部逻辑强行保持一致的 case, 直接利用context.Background
创建一个新的 context 对象即可。
方法三:如果确实有场景需要在子 goroutine 中用 gin 的 Context,可以使用gin.Context.Copy
函数复制出来一个新的 context 对象。
2023-11-29 13:05:01
几乎世界上每个 Golang 程序员都踩过一遍 for 循环变量的坑,而这个坑的解决方案已经作为实验特性加入到了 Go 1.21 中,并且有望在 Go 1.22 中完全开放。
举个例子,有这么段代码:
1 |
var ids []*int |
可以试着在 playgound 里面运行下:go.dev/play/p/O8MVGtueGAf
答案是:打印出来的全是 10。
这个结果实在离谱。原因是因为在目前 Go 的设计中,for 中循环变量的定义是 per loop 而非 per iteration。也就是整个 for 循环期间,变量 i
只会有一个。以上代码等价于:
1 |
var ids []*int |
同样的问题在闭包使用循环变量时也存在,代码如下:
1 |
var prints []func() |
根据上面的经验,闭包 func 中 fmt.Println(v)
,捕获到的 v
都是同一个变量。因此打印出来的都是 3。
在目前的 go 版本中,正常来说我们会这么解决:
1 |
var ids []*int |
定义一个新的局部变量, 这样无论闭包还是指针,每次迭代时所引用的内存都不一样了。
这个问题其实在 C++ 中也同样存在: wandbox.org/permlink/Se5WaeDb6quA8FCC。
但真的太容易搞错了,几乎每个 Go 程序员都踩过一遍,而且也非常容易忘记。即使这次记住了,下次很容易又会踩一遍。
甚至知名证书颁发机构 Let’s Encrypt 就踩过一样的坑 bug#1619047。代码如下:
1 |
// authz2ModelMapToPB converts a mapping of domain name to authz2Models into a |
在这个代码中,开发人员显然是很清楚这个 for 循环变量问题的,为此专门写了一段 kCopy := k
。但是没想到紧接着下一行就不小心用了 &v
。
因为这个 bug,Let’s Encrypt 为此召回了 300 万份有问题的证书。
Go 团队目前的负责人 Russ Cox 在 2022 年 10 月份的这个讨论 discussions/56010 里面,提到要修改 for 循环变量的语义,几乎是一呼百应。今年五月份,正式发出了这个提案proposal#60078。
在今年 8 月份发布的 Go 1.21 中已经带上了这个修改。只要开启 GOEXPERIMENT=loopvar
这个环境变量,for 循环变量的生命周期将变成每个迭代定义一次。
但毫无疑问,这是个 break change。如果代码中依赖了这个 for 循环变量是 per loop 的特性,那升级之后就会遇到问题。例如以下代码:
1 |
func sum(list []int) int { |
另外,对于程序性能也会有轻微影响, 毕竟新的方案里面将重复分配 N 次变量。对于性能极其敏感的场景,用户可以自行把循环变量提到外面。
同样的改变在 C# 也发生过,并没有出现大问题。
这个方案预计最早在 Go 1.22 就会正式开启了。按照 Go 每年发两个版本的惯例,在 2024 年 2 月份,我们就可以正式用上这个特性,彻底抛弃 x := x
的写法 ~
本文主要内容汇总自 go/wiki/LoopvarExperiment 和 proposal#60078