当前位置: 首页 > news >正文

Go 死锁全解析:4个条件+5个场景+6个解决方案

在 Go 并发开发中,你是否遇到过这样的窘境:程序突然卡住不动,日志停止输出,CPU 占用趋近于 0,重启后又恢复正常?这大概率是死锁在作祟。死锁就像两个人在狭窄走廊里对峙,都想让对方先让步,结果陷入永恒僵局——Go 中的 goroutine 若因“互相等待资源”陷入这种状态,整个流程便会彻底停滞。

本文将用“原理+代码+实战”的方式,带你彻底搞懂 Go 死锁的4 个必要条件、拆解5 个高频死锁场景、给出6 个可落地的避坑方法,让你不仅能快速定位死锁,更能从根源避免它。

一、先搞懂:死锁到底是什么?

死锁的本质是“多个 goroutine 因互相等待对方持有的资源,导致永远无法推进”。我们用一个生活例子类比:

你和朋友去餐厅吃饭,需要“筷子+勺子”才能用餐。你手里握着唯一的筷子(持有资源),等着朋友的勺子(等待资源);朋友手里握着唯一的勺子(持有资源),等着你的筷子(等待资源)。两人都不释放自己的资源,最终谁也吃不上饭——这就是死锁。

对应到 Go 程序中:

  • “你和朋友” = 多个 goroutine;
  • “筷子/勺子” = 程序资源(如锁、通道、文件句柄);
  • “互相等待” = goroutine 间的资源依赖循环。

死锁的核心特征很明显:

  • 程序卡住,无任何业务逻辑推进;
  • 日志无输出(或卡在某个步骤);
  • CPU、内存占用极低(goroutine 都在等待,无实际工作)。

二、死锁的 4 个必要条件:少一个都不会发生

死锁并非随机出现,它必须同时满足4 个经典条件(计算机科学通用理论,完全适用于 Go)。只要破坏其中任意一个条件,死锁就不会发生。我们逐个拆解,结合代码理解:

条件 1:互斥条件 —— 资源只能“独占使用”

定义:某个资源同一时间只能被一个 goroutine 占用,其他 goroutine 要使用,必须等待当前占用者释放。

就像厕所隔间一次只能容纳一人,Go 中的 sync.Mutex(互斥锁)是典型的“互斥资源”——一个 goroutine 加锁后,其他 goroutine 必须等待其解锁才能再次加锁。

代码示例(满足互斥条件,但未触发死锁):

package mainimport ("sync""time"
)func main() {var mu sync.Mutex // 互斥锁(互斥资源)// goroutine1 先占用锁go func() {mu.Lock()         // 加锁:独占资源defer mu.Unlock() // 退出时释放资源(关键)time.Sleep(3 * time.Second) // 模拟业务耗时println("goroutine1:释放锁")}()// 主 goroutine 等待 1 秒后尝试获取锁time.Sleep(1 * time.Second)mu.Lock()         // 此时 goroutine1 未解锁,主 goroutine 阻塞defer mu.Unlock()println("主 goroutine:获取到锁")
}

说明:此例仅满足“互斥条件”,但不会死锁——主 goroutine 虽等待锁,但未持有任何其他资源,待 goroutine1 释放锁后,主 goroutine 即可正常获取。

条件 2:持有并等待条件 —— 拿着资源等其他资源

定义:一个 goroutine 已持有至少一个资源,仍在等待其他 goroutine 持有的资源,且不释放自己已持有的资源。

回到餐厅例子:你握着筷子(已持有资源),还在等朋友的勺子(等待资源),且不把筷子交给朋友——这就是“持有并等待”。

代码示例(满足条件 1+2,仍未死锁):

package mainimport ("sync""time"
)func main() {var mu1, mu2 sync.Mutex // 两个互斥资源(筷子+勺子)// goroutine1:持有 mu1,等待 mu2go func() {mu1.Lock()         // 已持有 mu1defer mu1.Unlock()time.Sleep(1 * time.Second) // 模拟占用 mu1mu2.Lock()         // 等待 mu2(此时 mu2 未被占用)defer mu2.Unlock()println("goroutine1:获取到两个锁")}()// 主 goroutine:不持有任何资源,直接获取 mu2time.Sleep(2 * time.Second) // 等 goroutine1 释放 mu1mu2.Lock()defer mu2.Unlock()println("主 goroutine:获取到 mu2")
}

说明:goroutine1 虽“持有并等待”,但主 goroutine 未持有任何资源,仅单纯获取 mu2——两者无资源依赖冲突,因此不会死锁。

条件 3:不可剥夺条件 —— 资源不能“强行抢走”

定义:一个 goroutine 持有的资源,不能被其他 goroutine 强行剥夺,只能由持有方主动释放。

比如你握着筷子,别人不能直接从你手中抢走,只能等你主动放下;Go 中的互斥锁也遵循此规则——一个 goroutine 加锁后,其他 goroutine 无法强行解锁,只能等待持有方调用 Unlock()

为什么这是死锁条件:若资源可被强行剥夺(如系统强制回收 goroutine 持有的锁),就能打破“互相等待”的僵局。但 Go 中无此机制,因此“不可剥夺条件”默认满足。

条件 4:循环等待条件 —— 互相等待对方的资源

定义:多个 goroutine 形成闭环,每个 goroutine 都在等待下一个 goroutine 持有的资源。

例如:goroutineA 持有资源1、等资源2;goroutineB 持有资源2、等资源1——两者形成循环,永远无法获取所需资源。

代码示例(4 个条件全满足,触发死锁):

package mainimport ("sync""time"
)func main() {var mu1, mu2 sync.Mutex // 两个互斥资源// goroutine1:持有 mu1,等待 mu2(满足条件2)go func() {mu1.Lock()defer mu1.Unlock()time.Sleep(1 * time.Second) // 确保先持有 mu1mu2.Lock() // 等待 mu2(此时 mu2 已被 goroutine2 持有)defer mu2.Unlock()println("goroutine1:获取到两个锁(不会执行)")}()// goroutine2:持有 mu2,等待 mu1(满足条件2)go func() {mu2.Lock()defer mu2.Unlock()time.Sleep(1 * time.Second) // 确保先持有 mu2mu1.Lock() // 等待 mu1(此时 mu1 已被 goroutine1 持有)defer mu1.Unlock()println("goroutine2:获取到两个锁(不会执行)")}()select {} // 主 goroutine 阻塞,防止程序退出(死锁状态)
}

运行结果:程序卡住,无任何日志输出。此时 4 个条件全满足:

  1. 互斥:mu1、mu2 均为互斥锁;
  2. 持有并等待:两个 goroutine 均“握一锁等一锁”;
  3. 不可剥夺:锁无法被强行回收;
  4. 循环等待:goroutine1 等 mu2(goroutine2 持有),goroutine2 等 mu1(goroutine1 持有)。

这是 Go 中最典型的死锁场景,也是实际开发中最容易踩的坑。

三、Go 5 个高频死锁场景:原理+代码+修复

理解死锁的 4 个条件后,我们结合实际开发场景,拆解 5 个最常遇到的死锁案例,每个案例都包含“死锁代码+原因分析+修复方案”。

场景 1:无缓冲通道“只发不收”或“只收不发”

原理:无缓冲通道(make(chan T))的核心特性是“发送方与接收方必须同时就绪”——发送方会阻塞到有接收方接收,接收方会阻塞到有发送方发送。若只有发送无接收(或反之),goroutine 会永远阻塞,触发死锁。

死锁代码(只发不收):

package mainfunc main() {ch := make(chan int) // 无缓冲通道ch <- 10 // 主 goroutine 发送数据,但无接收方 → 永远阻塞println("数据发送成功(不会执行)")
}

为什么死锁:无缓冲通道缺乏“临时存储区”,发送方必须等待接收方响应。此例中仅存在发送方,主 goroutine 阻塞在 ch <- 10,满足“循环等待”(主 goroutine 等接收方,但接收方不存在)。

修复方案:启动 goroutine 作为接收方,确保“发收同时就绪”:

package mainfunc main() {ch := make(chan int)// 启动接收方 goroutinego func() {data := <-ch // 接收数据println("接收方:收到数据", data) // 输出:接收方:收到数据 10}()ch <- 10 // 此时有接收方,发送后不阻塞println("发送方:数据发送成功") // 输出:发送方:数据发送成功
}

场景 2:goroutine 互相等待通道数据

原理:两个 goroutine 互相向对方的通道发送数据,且都“先发送、再接收”——双方均阻塞在“发送”步骤,形成循环等待。

死锁代码

package mainfunc main() {ch1 := make(chan int)ch2 := make(chan int)// goroutine1:先发 ch2,再收 ch1go func() {ch2 <- 20 // 阻塞:等 goroutine2 接收 ch2data := <-ch1 // 永远执行不到println("goroutine1:收到", data)}()// goroutine2:先发 ch1,再收 ch2go func() {ch1 <- 10 // 阻塞:等 goroutine1 接收 ch1data := <-ch2 // 永远执行不到println("goroutine2:收到", data)}()select {} // 主 goroutine 阻塞
}

为什么死锁:goroutine1 阻塞在 ch2 <- 20(等 goroutine2 收),goroutine2 阻塞在 ch1 <- 10(等 goroutine1 收)——双方均“持有发送动作,等待对方接收”,形成循环等待。

修复方案:调整“发送/接收”顺序(先接收、再发送),或使用带缓冲通道:

package mainimport "time"func main() {ch1 := make(chan int)ch2 := make(chan int)// goroutine1:先收 ch1,再发 ch2go func() {data := <-ch1 // 先等 goroutine2 发println("goroutine1:收到", data) // 输出:goroutine1:收到 10ch2 <- 20 // 再发送(此时 goroutine2 已准备接收)}()// goroutine2:先发 ch1,再收 ch2go func() {ch1 <- 10 // 发送(goroutine1 已准备接收)data := <-ch2 // 再等 goroutine1 发println("goroutine2:收到", data) // 输出:goroutine2:收到 20}()time.Sleep(1 * time.Second) // 等 goroutine 执行完
}

场景 3:忘记释放锁(Mutex/RWMutex)

原理sync.Mutex 加锁后,若因“提前 return”“panic”等原因未调用 Unlock(),锁会被永远持有,其他 goroutine 尝试加锁时会永远阻塞,触发死锁。

死锁代码(提前 return 导致未解锁):

package mainimport ("sync""time"
)func main() {var mu sync.Mutexvar count int// goroutine1:加锁后提前 return,未解锁go func() {mu.Lock()if count > 0 { // 假设 count=0,不满足;若满足则直接 returnreturn // 错误:此处 return 会跳过 Unlock()}count++mu.Unlock() // 仅 count<=0 时才解锁println("goroutine1:执行完成")}()// goroutine2:等待锁,但永远等不到time.Sleep(1 * time.Second)mu.Lock() // 阻塞,程序卡住defer mu.Unlock()count++println("goroutine2:执行完成(不会执行)")
}

为什么死锁:若 goroutine1 因 count>0 提前 return,会跳过 mu.Unlock(),导致锁永远被持有。goroutine2 尝试加锁时会永远阻塞,满足“持有并等待”(goroutine2 等锁,锁被 goroutine1 永久占用)。

修复方案:用 defer 确保解锁(defer 会在函数退出前执行,无论是否 return):

go func() {mu.Lock()defer mu.Unlock() // 关键:加锁后立即 defer 解锁if count > 0 {return // 即使 return,defer 也会执行解锁}count++println("goroutine1:执行完成")
}()

场景 4:sync.WaitGroup 计数不匹配

原理sync.WaitGroup 用于等待多个 goroutine 完成,核心逻辑是“Add(n) 设预期计数 → 每个 goroutine 执行完调用 Done()(计数-1)→ 主 goroutine 调用 Wait() 等计数归 0”。若“计数多了”(Add 多、Done 少)或“计数少了”(Add 少、Done 多),Wait() 会永远阻塞。

死锁代码(计数多了):

package mainimport ("sync""time"
)func main() {var wg sync.WaitGroupwg.Add(3) // 错误:预期 3 个 goroutine,但只启动 2 个// goroutine1:执行完调用 Done()go func() {defer wg.Done()time.Sleep(1 * time.Second)println("goroutine1:完成")}()// goroutine2:执行完调用 Done()go func() {defer wg.Done()time.Sleep(1 * time.Second)println("goroutine2:完成")}()wg.Wait() // 计数停在 1(3-2=1),永远阻塞println("所有 goroutine 完成(不会执行)")
}

为什么死锁wg.Add(3) 表示需等待 3 个 Done(),但实际仅调用 2 次,wg.Wait() 会永远等待第 3 个 Done(),主 goroutine 陷入阻塞。

修复方案:确保 Add(n)n 与 goroutine 数量一致,或“每个 goroutine 启动前 Add(1)”(更安全):

package mainimport ("sync""time"
)func main() {var wg sync.WaitGroup// 更安全的写法:每个 goroutine 启动前 Add(1)go func() {wg.Add(1)defer wg.Done()time.Sleep(1 * time.Second)println("goroutine1:完成")}()go func() {wg.Add(1)defer wg.Done()time.Sleep(1 * time.Second)println("goroutine2:完成")}()wg.Wait() // 计数归 0,正常退出println("所有 goroutine 完成") // 输出:所有 goroutine 完成
}

场景 5:循环等待多个锁(最经典场景)

原理:多个 goroutine 按“不同顺序”申请多个锁,形成循环等待——这是“死锁 4 个条件”的典型实践,常见于“多资源协同”场景(如同时操作“用户”和“订单”数据时申请双锁)。

死锁代码(不同顺序申请锁):

package mainimport ("sync""time"
)func main() {var userLock sync.Mutex  // 用户锁var orderLock sync.Mutex // 订单锁// goroutine1:先申请 userLock,再申请 orderLockgo func() {userLock.Lock()defer userLock.Unlock()time.Sleep(1 * time.Second) // 确保先持有 userLockorderLock.Lock() // 等待 orderLock(已被 goroutine2 持有)defer orderLock.Unlock()println("goroutine1:处理用户+订单(不会执行)")}()// goroutine2:先申请 orderLock,再申请 userLock(顺序相反)go func() {orderLock.Lock()defer orderLock.Unlock()time.Sleep(1 * time.Second) // 确保先持有 orderLockuserLock.Lock() // 等待 userLock(已被 goroutine1 持有)defer userLock.Unlock()println("goroutine2:处理订单+用户(不会执行)")}()select {} // 主 goroutine 阻塞
}

为什么死锁:goroutine1 持有 userLock 等 orderLock,goroutine2 持有 orderLock 等 userLock——按不同顺序申请锁,形成循环等待,4 个死锁条件全满足。

修复方案:所有 goroutine 按“固定顺序”申请锁(如“先用户锁、后订单锁”),破坏“循环等待”条件:

package mainimport ("sync""time"
)func main() {var userLock sync.Mutex  // 用户锁var orderLock sync.Mutex // 订单锁// goroutine1:固定顺序:userLock → orderLockgo func() {userLock.Lock()defer userLock.Unlock()time.Sleep(1 * time.Second)orderLock.Lock()defer orderLock.Unlock()println("goroutine1:处理用户+订单") // 正常执行}()// goroutine2:同一固定顺序:userLock → orderLock(关键修改)go func() {userLock.Lock() // 先申请 userLock(与 goroutine1 一致)defer userLock.Unlock()time.Sleep(1 * time.Second)orderLock.Lock() // 再申请 orderLockdefer orderLock.Unlock()println("goroutine2:处理订单+用户") // 正常执行}()time.Sleep(3 * time.Second)
}

四、6 个避坑方法:从根源破坏死锁条件

死锁的本质是“4 个条件同时满足”,因此避坑的核心思路是“针对性破坏其中一个或多个条件”。以下 6 个方法可直接落地,覆盖绝大多数场景:

方法 1:按固定顺序申请资源(破坏“循环等待”)

这是解决“多锁死锁”最有效的方法——所有 goroutine 申请多个资源时,严格遵循同一固定顺序(如“按资源 ID 从小到大”“按锁变量名排序”)。

例如处理“用户+订单”时,无论哪个 goroutine,都先申请“用户锁”,再申请“订单锁”——这样永远不会形成循环等待。

代码示例

// 统一规则:所有 goroutine 均按「userLock → orderLock」顺序申请
func process(userLock, orderLock *sync.Mutex) {userLock.Lock()         // 固定第一步:用户锁defer userLock.Unlock()time.Sleep(1 * time.Second)orderLock.Lock()        // 固定第二步:订单锁defer orderLock.Unlock()println("安全处理用户+订单数据")
}func main() {var userLock, orderLock sync.Mutexgo process(&userLock, &orderLock)go process(&userLock, &orderLock) // 同一顺序,无死锁风险time.Sleep(3 * time.Second)
}

方法 2:用带缓冲通道(破坏“持有并等待”)

无缓冲通道需“发收同时就绪”,容易因“等待对方”触发死锁;而带缓冲通道(make(chan T, n))有“临时存储区”——发送方在缓冲区未满时不会阻塞,接收方在缓冲区非空时不会阻塞,可打破“必须等待对方”的限制。

代码示例(修复“互相等待通道”场景):

package mainimport "time"func main() {ch1 := make(chan int, 1) // 带缓冲通道(缓冲区大小 1)ch2 := make(chan int, 1)// goroutine1:先发 ch2,再收 ch1go func() {ch2 <- 20 // 缓冲区未满,直接发送(不阻塞)data := <-ch1println("goroutine1:收到", data) // 输出:goroutine1:收到 10}()// goroutine2:先发 ch1,再收 ch2go func() {ch1 <- 10 // 缓冲区未满,直接发送(不阻塞)data := <-ch2println("goroutine2:收到", data) // 输出:goroutine2:收到 20}()time.Sleep(1 * time.Second)
}

方法 3:用 context 设置超时(破坏“持有并等待”)

若 goroutine 持有资源并等待其他资源时,能在“超时后主动释放资源”,就不会陷入永久等待。Go 的 context.WithTimeout 可实现此逻辑:超时后,goroutine 主动解锁或放弃等待,释放已持有的资源。

代码示例(超时释放锁):

package mainimport ("context""sync""time"
)func main() {var mu1, mu2 sync.Mutex// goroutine1:持有 mu1,超时后释放go func() {mu1.Lock()defer mu1.Unlock()// 创建 1 秒超时的 contextctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)defer cancel()select {case <-ctx.Done():// 超时:主动放弃等待,释放 mu1(defer 执行)println("goroutine1:等待 mu2 超时,释放 mu1")returndefault:// 模拟业务逻辑time.Sleep(500 * time.Millisecond)println("goroutine1:正常执行")}}()// goroutine2:持有 mu2,超时后释放go func() {mu2.Lock()defer mu2.Unlock()ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)defer cancel()select {case <-ctx.Done():println("goroutine2:等待 mu1 超时,释放 mu2")returndefault:mu1.Lock()defer mu1.Unlock()println("goroutine2:获取到两个锁")}}()time.Sleep(2 * time.Second)
}

方法 4:避免 goroutine 泄漏(防止“资源永久占用”)

goroutine 泄漏(启动后永远不退出)会导致其持有的资源(锁、通道)永久不释放,其他 goroutine 会永远等待这些资源,最终触发死锁。常见泄漏场景:

  • goroutine 内有无限循环,且无退出机制;
  • 通道接收方永远等不到数据(发送方已退出)。

避免方法

  1. 给每个 goroutine 加退出机制(用 context 或退出通道);
  2. sync.WaitGroup 确保所有 goroutine 正常退出;
  3. 避免无退出条件的无限循环。

代码示例(用 context 避免泄漏):

package mainimport ("context""time"
)func main() {// 创建可取消的 context(主 goroutine 退出时触发)ctx, cancel := context.WithCancel(context.Background())defer cancel()// 启动 goroutine,带退出机制go func() {for {select {case <-ctx.Done():// 收到退出信号,主动退出(避免泄漏)println("goroutine:收到退出信号,正常退出")returndefault:// 模拟业务逻辑time.Sleep(500 * time.Millisecond)println("goroutine:处理业务")}}}()// 主 goroutine 运行 2 秒后退出time.Sleep(2 * time.Second)println("主 goroutine:退出")
}

方法 5:用工具提前检测死锁风险

Go 提供了多个工具,可在开发阶段发现死锁隐患,无需等到线上出问题:

工具功能使用命令
race 检测器检测数据竞争、部分死锁场景go run -race main.go
go vet静态分析常见错误(如通道只发不收)go vet main.go
pprof(goroutine 分析)排查已发生的死锁(查看阻塞 goroutine)1. `ps aux

示例:用 race 检测器检测死锁:

go run -race main.go

若存在死锁风险,终端会输出详细的警告信息,包括阻塞的 goroutine 和资源。

方法 6:减小资源持有时间(降低死锁概率)

goroutine 持有资源的时间越短,其他 goroutine 等待的时间就越短,死锁的概率也越低。实践建议:

  1. 锁的粒度要小:只在“修改共享数据”的代码段加锁,不要给整个函数加锁;
  2. 避免在锁内做耗时操作:如网络请求、文件 IO、time.Sleep 等;
  3. 不在锁内调用外部函数:外部函数可能隐藏加锁逻辑,导致嵌套锁或死锁。

代码示例(减小锁粒度):

package mainimport ("sync""time"
)// 不好的写法:整个函数加锁,持有时间长
func badDemo(mu *sync.Mutex, data []int) {mu.Lock()defer mu.Unlock()time.Sleep(1 * time.Second) // 耗时操作(不该在锁内)data[0] = 100 // 仅这行需要保护
}// 好的写法:仅保护关键代码段,持有时间短
func goodDemo(mu *sync.Mutex, data []int) {time.Sleep(1 * time.Second) // 耗时操作(锁外执行)mu.Lock()data[0] = 100 // 仅关键步骤加锁mu.Unlock()
}

五、总结:死锁避坑核心原则

死锁虽看似复杂,但只要记住“理解条件→识别场景→落地方法”的逻辑,就能轻松应对。核心原则可提炼为 5 点:

  1. 死锁条件是根因:死锁必须同时满足“互斥、持有并等待、不可剥夺、循环等待”,破坏任意一个即可避免;
  2. 通道使用有规矩:无缓冲通道确保“有发有收”,复杂场景用带缓冲通道;
  3. 锁的使用讲规范:按固定顺序申请锁,用 defer 确保解锁,减小锁粒度;
  4. 工具辅助早发现:开发阶段用 racego vet 检测风险,线上用 pprof 排查问题;
  5. goroutine 有退出机制:用 contextWaitGroup 避免泄漏,防止资源永久占用。

死锁是 Go 并发编程的“常见病”,但并非“不治之症”。只要在编写代码时保持对“资源竞争”的警惕,遵循上述原则,就能从根源上杜绝死锁,写出高效、稳定的并发程序。


文章转载自:

http://jMjT61Fp.gyqnp.cn
http://gQZvIhEo.gyqnp.cn
http://c8FQKipA.gyqnp.cn
http://OBsCY5pC.gyqnp.cn
http://zpCMudx8.gyqnp.cn
http://HHhpJLDh.gyqnp.cn
http://IfEihYLv.gyqnp.cn
http://eUFqvSUn.gyqnp.cn
http://N34BG2os.gyqnp.cn
http://iLupXff0.gyqnp.cn
http://wTw0sc0l.gyqnp.cn
http://rMIJxRvr.gyqnp.cn
http://WvnbR68G.gyqnp.cn
http://iYWTVQ2e.gyqnp.cn
http://JSnFRbQu.gyqnp.cn
http://ElHiP3GF.gyqnp.cn
http://d50c248G.gyqnp.cn
http://HkkADGzP.gyqnp.cn
http://ipLW4e9J.gyqnp.cn
http://5GG3seFW.gyqnp.cn
http://ULtwAz2m.gyqnp.cn
http://4eMc6WLE.gyqnp.cn
http://cUSmF0pG.gyqnp.cn
http://I3ZOmGJ8.gyqnp.cn
http://9jkW3zRE.gyqnp.cn
http://JXKVQMLV.gyqnp.cn
http://FZShbTAT.gyqnp.cn
http://rqCKZCow.gyqnp.cn
http://jlXnkaEz.gyqnp.cn
http://Ygs5cWOC.gyqnp.cn
http://www.dtcms.com/a/378856.html

相关文章:

  • Go语言快速入门教程(JAVA转go)——1 概述
  • 【leetcode】139. 单词拆分
  • 使用yocto工具链交叉编译lsof命令
  • vue项目的main.js规划设计与合理使用
  • FPGA入门-无源蜂鸣器驱动
  • 使用Langchain生成本地rag知识库并搭载大模型
  • [第一章] web入门—N1book靶场详细思路讲解
  • uniapp 文件查找失败:main.js
  • 第7篇、Kafka Streams 与 Connect:企业级实时数据处理架构实践指南
  • Linux redis 8.2.1源码编译
  • logging 模块升级版 loguru
  • 【Flask】实现一个前后端一体的项目-脚手架
  • 小说阅读系统Java源码 小说阅读软件开发 小说app小程序
  • 如何在 Debian 12 上安装 MySQL
  • GA-PNN: 基于遗传算法的光子神经网络硬件配置方法(未做完)
  • STM32基础篇--GPIO
  • 无人机遥控器射频模块技术解析
  • Docker 命令核心语法
  • 第五章:Python 数据结构:列表、元组与字典(一)
  • Python快速入门专业版(二十一):if语句基础:单分支、双分支与多分支(判断用户权限案例)
  • 学习笔记:JavaScript(4)——DOM节点
  • 软考中级习题与解答——第四章_软件工程(3)
  • 消息队列-kafka完结
  • SKywalking Agent配置+Oracle监控插件安装指南
  • Skywalking告警配置+简易邮件告警应用配置(保姆级)
  • 【matlab】YALMIP、GLPK安装资源及安装方法
  • modbus学习
  • 创建GLFW窗口,开启OpenGL之路
  • (网络原理)核心知识回顾 网络核心原理 get和post的理解 解析http 加密+请求和响应的一些关键字 Cookie和session 对密钥的理解
  • 如何提升研发文档的检索体验与效率