Golang Go语言中 看某个教程,不太理解为啥 Init Logger 要加锁

发布于 1周前 作者 songsunli 来自 Go语言
var (
	mu sync.Mutex
// std 定义了默认的全局 Logger.
std = NewLogger(NewOptions())

)

// Init 使用指定的选项初始化 Logger. func Init(opts *Options) { mu.Lock() defer mu.Unlock()

std = NewLogger(opts)

}

一般一个项目日志也就 Init 一次吧,这里加这个锁是为了什么呢 🤔 求大佬解惑

完整源码: https://github.com/marmotedu/miniblog/blob/master/internal/pkg/log/log.go


Golang Go语言中 看某个教程,不太理解为啥 Init Logger 要加锁

更多关于Golang Go语言中 看某个教程,不太理解为啥 Init Logger 要加锁的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html

15 回复

这代码没什么营养,不要纠结为何这么写。

更多关于Golang Go语言中 看某个教程,不太理解为啥 Init Logger 要加锁的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


这只是一个普通的函数

接 #1
1. 这个函数是 Init ,不是 init
2. 锁只锁了 std = NewLogger(opts) 语句,使用时没加锁,std 变量存在 race 问题。
3. func NewLogger(opts *Options) *zapLogger 可导出方法返回了不可导出的类型,不够优雅。

初学者还是应该拜读更规范的源码,免得入门时被低质量代码熏陶,一些错误理念会根深蒂固。

抱着解答问题的心态点进来,看见楼上的评论就笑了。名著你可以揣测一下作者的写作思路,地摊文学就算了……

代码层面的问题 在楼上已经说得比较清楚了,我补充一点我的理解。

这种代码水平在我之前的面试标准里面,按照初级/中级/高级三档水平,只能划分到初级那一档。我只通过看链接里那个文件就做出了判断,而且对此我非常有把握。

划分到初级的主要理由是:无法准确使用接口 Interface 来实现功能解耦。这个能力在我之前负责面试的时候是中级技能里的。相比之下,代码层面反倒是小问题了。

如果看不懂的话,我可以抽时间单独就 Go 通过接口来解耦专门做个解释。有隔壁那个代码氛围的帖子在,其实不想过多评价。但我还是要说,这段代码的作者的 Go 水平真就是初学者,出来卖课就是误人子弟了。

这里的第三点说说我的想法,大佬看看是否有问题,如果结构体需要比较复杂的初始化流程或者说在结构体初始化完成之后需要对其他的字段进行赋值,这么做应该也没有问题吧?

bv 说的第三点主要还是说返回了不可导出的类型吧,这种类型的变量在 go 里面感觉确实有点奇葩,其他包可以用,但是不能传递给其他函数(因为无法声明类型)。

另外复杂的结构体初始化可以用 Builder 或者 Option 模式吧

加锁是对的,因为写了变量 std ,同时有两个线程写同一个变量是 data race 。
通常只会 init 一次,也不排除有人在多个线程上 init 这种情况。虽然这种用法大概率是错的,不过作为 library 让这种情况不出 data race 也是正常的。

使用的时候不需要加锁,因为同时有多个线程读同一个变量不是 data race

正确与否看具体的业务逻辑。一般来说这段代码如果真被并发调用,哪怕加了锁还是会初始化两次,不是典型的做法。如果希望初始化一次尽量使用 sync.Once 。

为啥不用 once…

看了错误的代码示范了吧。。

确实,我一直想着依赖接口,忽略了直接使用类型的情况。

我猜估计是 go race 的时候看到问题了,又懒得改

确实他应该下面这么写,我猜他这个 std 应该是想保留具体类型而不是接口类型

go<br>var _ Logger = &amp;zapLogger{}<br><br>var (<br> mu sync.Mutex<br> std = newLogger(NewOptions())<br>)<br><br>func Init(opts *Options) {<br> mu.Lock()<br> defer mu.Unlock()<br> std = newLogger(opts)<br>}<br><br>func NewLogger(opts *Options) Logger {<br> return newLogger(opts)<br>}<br><br>func newLogger(opts *Options) *zapLogger {<br>

优雅一点的方式是使用 sync.Once (内部机制也是使用了锁),比如这样:
var (<br> stdOnce sync.Once<br><br> // std 定义了默认的全局 Logger.<br> std = NewLogger(NewOptions())<br>)<br><br>// Init 使用指定的选项初始化 Logger.<br>func Init(opts *Options) {<br> <a target="_blank" href="http://stdOnce.Do" rel="nofollow noopener">stdOnce.Do</a>(func() {<br> std = NewLogger(opts)<br> })<br>}<br>

在Go语言中,初始化日志记录器(Logger)时加锁的做法,主要是为了确保在多线程(或称为goroutine)环境下的线程安全性。虽然初始化Logger本身可能是一个快速且一次性的操作,但在并发编程中,即便是这样的简单操作也需要谨慎处理,以避免数据竞争和未定义行为。

  1. 数据竞争:如果多个goroutine同时尝试初始化同一个Logger实例(例如,在没有同步机制的情况下检查Logger是否为nil并赋值),就可能导致数据竞争,使得Logger的状态变得不可预测。

  2. 一致性:加锁可以确保Logger的状态在初始化过程中保持一致,从而避免部分goroutine看到未完全初始化的Logger实例。

  3. 代码健壮性:在开发大型或复杂的Go项目时,很难预测哪些代码段会在未来被并发执行。因此,即使在当前的上下文中看似不必要的加锁,也可能为未来可能的并发执行预留了安全空间。

  4. 最佳实践:在Go社区中,对于可能涉及共享资源(如全局Logger)的操作,加锁通常被视为一种最佳实践。这有助于培养编写健壮并发代码的习惯。

因此,在初始化Logger时加锁,是为了确保在多goroutine环境下Logger的初始化和使用都是安全的。这种做法虽然可能在某些情况下看起来是多余的,但它为代码的健壮性和未来的可维护性提供了保障。

回到顶部