跳到主要内容

Koa 的 ctx 是什么如何使用

· 阅读需 2 分钟
素明诚
Full stack development

ctx 在 Koa 中是上下文(Context)的缩写,它是一个封装了 Node.js 的 requestresponse 对象的对象,用于在中间件之间传递信息和状态。下面是一个简单的例子来说明这个过程

const Koa = require('koa');
const app = new Koa();

// 第一个中间件
app.use(async (ctx, next) => {
// 设置一个用户信息对象到 ctx.state,这个是中间件间共享的状态对象
ctx.state.user = { name: '张三', age: 30 };
await next(); // 调用下一个中间件
});

// 第二个中间件
app.use(async (ctx, next) => {
// 读取上一个中间件设置的用户信息
const user = ctx.state.user;
// 做一些操作,比如权限检查
if (user.name === '张三') {
await next(); // 用户名匹配,继续下一个中间件
} else {
ctx.status = 403; // 用户名不匹配,返回403 Forbidden
}
});

// 第三个中间件
app.use(async (ctx) => {
// 再次读取用户信息,用于生成响应
const user = ctx.state.user;
// 设置响应体为用户信息
ctx.body = `欢迎 ${user.name},您的年龄是 ${user.age}岁`;
});

app.listen(3000);

第一个中间件在 ctx.state 对象上设置了 user 对象。

第二个中间件检查了 ctx.state.user,并根据用户信息决定是否继续执行下一步。

第三个中间件使用了 ctx.state.user 来构建响应。

Git 放弃当前分支使用远程分支

· 阅读需 1 分钟
素明诚
Full stack development

用 dev 分支举例子想要放弃本地 dev 分支上所有的更改,并且完全同步远程 dev 分支的代码,您可以删除本地的 dev 分支然后重新创建它,以匹配远程分支的状态。以下是步骤

首先,确保您没有在 dev 分支上,因为您不能删除当前检出的分支

git checkout main  # 切换到main分支或任何其他分支

删除本地的 dev 分支

git branch -D dev

拉取远程 dev 分支并在本地创建一个新的 dev 分支

git fetch origin
git checkout -b dev origin/dev

-b 标志告诉 Git 创建一个新的分支并立即切换到这个新分支。origin/dev 指定了新分支应该跟踪的远程分支

这样做的好处是您不需要处理任何合并冲突,因为您直接采用了远程分支的状态。适用于你的同事在处理完冲突进行了硬重置,你可以直接先保存一份代码,直接使用远程分支,进行修改。

解决Node版本问题fsjs45 primordials

· 阅读需 1 分钟
素明诚
Full stack development

这个错误通常是因为使用了不兼容的 Node.js 和 Gulp 版本,尤其是在 Node.js 版本高于 12 时使用 Gulp 3 时出现。要解决这个问题,可以尝试以下两种方法:

降级 Node.js

  • 检查你的 Node.js 版本,如果超过了 12,并且你在使用 Gulp 3,你需要降级到 Node.js 版本 12。

使用 npm-shrinkwrap.json

  • 删除 node_modules 文件夹。
  • 在你的 package.json 文件所在目录创建一个名为 npm-shrinkwrap.json 的新文件。
  • 在 npm-shrinkwrap.json
    文件中添加以下内容:
{
"dependencies": {
"graceful-fs": {
"version": "4.2.2"
}
}
}
  • 运行 npm install 更新你的依赖。
  • 最后,运行 gulp 启动项目。

systemd 的基本使用

· 阅读需 2 分钟
素明诚
Full stack development

服务控制

  • 启动服务(可简写)
sudo systemctl start apache2.service
sudo systemctl start apache2
  • 停止服务
sudo systemctl stop apache2.service
  • 重新启动服务
sudo systemctl restart apache2.service
  • 查看服务状态
systemctl status apache2.service

服务开机自启设置

  • 启用服务开机自启
sudo systemctl enable apache2.service
  • 禁用服务开机自启
sudo systemctl disable apache2.service

系统状态查看

  • 查看服务的网络连接
ss -tnlp | grep apache2
  • 查看所有日志
journalctl -u apache2.service
  • 查看最新日志
journalctl -u apache2.service -e
  • 列出所有运行的服务
systemctl list-units --type=service
  • 查看系统日志
journalctl

定时器和计划任务

  • 创建一个文件 /etc/systemd/system/backup.service 来定义要执行的任务:
plaintextCopy code[Unit]
Description=Daily Backup

[Service]
ExecStart=/usr/local/bin/backup.sh
  • 创建一个文件 /etc/systemd/system/backup.timer 来定义定时器:
plaintextCopy code[Unit]
Description=Runs backup every day at 2am

[Timer]
OnCalendar=*-*-* 020000
Unit=backup.service

[Install]
WantedBy=timers.target
  • 启动并启用定时器:
sudo systemctl start backup.timer
sudo systemctl enable backup.timer

创建和管理自定义服务

  • 以下是一个简单的自定义服务单元文件的例子,该文件应保存为 /etc/systemd/system/custom.service
plaintextCopy code[Unit]
Description=Custom Service

[Service]
ExecStart=/usr/local/bin/custom.sh
Restart=always

[Install]
WantedBy=multi-user.target
  • 在创建了单元文件后,可以通过以下命令启动、停止或重新启动服务:
sudo systemctl start custom.service
sudo systemctl stop custom.service
sudo systemctl restart custom.service

Go 为什么不推荐使用锁

· 阅读需 3 分钟
素明诚
Full stack development

Go 语言的设计者通过引入通道(channels)和协程(goroutines)提供了一种新的并发模型,它旨在简化并发编程并避免常见的并发问题。虽然 Go 语言中也可以使用互斥锁(mutexes)和其他传统的并发控制机制,但通道提供了一种更高级、更直观的方式来处理并发。以下是为什么 Go 社区通常推荐首先考虑使用通道而不是锁的几个原因:

更简单、更直观

通道提供了一种简单直观的方式来在协程之间发送数据和信号。通过使用通道,开发人员可以避免直接管理锁和共享内存,降低了编写并发代码的复杂性。

避免死锁和竞争条件

通道通过串行化对共享数据的访问来避免竞争条件和死锁,这是传统的锁和共享内存模型中常见的问题。虽然使用通道仍然可能遇到死锁(例如,循环依赖),但通常更容易识别和解决。

提高代码的可读性和维护性

使用通道使得并发逻辑更清晰、更易于理解和维护。通道的发送和接收操作明确地表达了协程之间的通信和同步意图。

方便的错误处理和资源清理

通道提供了一种方便的方式来传播错误和通知协程结束,从而可以进行适当的资源清理和错误处理。

促进了“共享内存通过通信”(Share Memory By Communicating, SMC)的哲学

Go 语言推广的“不要通过共享内存来通信,而是通过通信来共享内存”的哲学,通过通道实现了这种哲学,它鼓励开发人员使用通信而不是共享内存来组织并发逻辑。

更容易测试和调试

使用通道通常使得并发代码更容易测试和调试,因为它简化了协程之间的交互和同步。

优雅的关闭和协程同步

通过关闭通道和使用select语句,开发人员可以优雅地控制协程的关闭和同步,避免了复杂的锁逻辑和条件变量。

虽然通道是处理并发的推荐方式,但在某些情况下,使用锁可能仍然是必要的,特别是在需要精细控制并发访问的场景中。重要的是理解通道和锁的不同,以及它们各自的适用场景,以便能够选择最适合特定问题的解决方案。

Go 的 select 语句随机选择 case 分支

· 阅读需 1 分钟
素明诚
Full stack development

公平性

  • 通过随机选择 case 分支,select 语句确保每个通道都有公平的机会被处理,避免了某些通道可能因为固定的顺序或优先级而被忽略或延迟处理。

避免饥饿

  • 如果 select 语句总是优先处理某个通道,可能会导致其他通道饥饿,即它们的数据延迟处理或永不处理。随机选择有助于避免这种情况,确保所有通道都能得到处理。

简化逻辑

  • select 语句的随机选择行为简化了并发逻辑,因为程序员不需要考虑如何设计复杂的优先级或调度算法来确保公平处理通道。

负载均衡

  • 在某些情况下,随机选择可以作为一种简单的负载均衡机制,确保处理在多个通道或协程之间均匀分布。

提高响应性

  • 随机选择有助于提高系统的响应性,因为它减少了通道处理的确定性延迟,使系统能够更快地响应不同的事件和条件。

Go 通道为什么分为阻塞和非阻塞

· 阅读需 5 分钟
素明诚
Full stack development

通道(channel)的阻塞和非阻塞行为是 Go 语言并发编程模型的重要组成部分。这两种行为可以帮助程序员更灵活地控制 goroutine 之间的交互和同步。下面分别解释了阻塞和非阻塞通道的机制及其用途

阻塞通道(Blocking Channels)

  • 默认情况下,通道是阻塞的。当一个 goroutine 尝试从一个空的通道中读取数据时,它会被阻塞,直到另一个 goroutine 向通道写入数据为止。同样地,当一个 goroutine 尝试向一个已满的通道写入数据时(在带缓冲的通道的情况下),它会被阻塞,直到另一个 goroutine 从通道读取数据为止。
  • 这种阻塞机制提供了一种自然而强大的方式来同步并发操作。例如,它可以用于等待一个长时间运行的操作完成,或者协调多个 goroutine 的执行顺序。

非阻塞通道(Non-blocking Channels)

  • 非阻塞通道操作允许 goroutine 在尝试读取或写入通道时不被阻塞。在 Go 中,可以通过使用 select 语句和 default 子句来实现非阻塞通道操作。
  • 这种非阻塞机制可以用于实现更复杂的并发逻辑,例如超时、尝试操作或多路复用。它允许 goroutine 在通道操作无法立即进行时继续执行其他任务。
ch := make(chan int)

// 非阻塞读取示例
select {
case value := <-ch:
fmt.Println("Received value:", value)
default:
fmt.Println("No value received")
}

// 非阻塞写入示例
select {
case ch <- 1:
fmt.Println("Sent value")
default:
fmt.Println("No value sent")
}

select 语句尝试从 ch 通道读取值或向 ch 通道写入值。如果通道操作可以立即进行,则 select 会随机进入执行相应的 case 子句。如果通道操作无法立即进行,则 select 会执行 default 子句,实现非阻塞操作。

为什么要设计有缓冲区和非缓冲区呢?

答案就在同步和异步

无缓冲区通道(Unbuffered Channels)

  • 同步通信 无缓冲区通道提供了一种强同步的通信机制。当一个协程尝试向通道发送数据时,它会被阻塞,直到另一个协程从通道接收数据。同样,当一个协程尝试从通道接收数据时,它会被阻塞,直到另一个协程向通道发送数据。
  • 确保数据接收 通过无缓冲区通道发送数据时,发送协程会等待直到数据被接收,这确保了数据确实被接收并处理。
  • 简单和清晰 无缓冲区通道简化了协程间的通信,使得代码易于理解和维护。

有缓冲区通道(Buffered Channels)

  • 异步通信 有缓冲区通道允许异步的通信。协程可以向通道发送数据而不等待接收,只要通道的缓冲区还有空间。同样,协程可以从通道接收数据,只要缓冲区中有数据。
  • 提高性能 有缓冲区通道可以帮助提高程序的性能,因为协程可以继续执行,而不是等待其他协程。这对于需要高吞吐量或低延迟的应用程序特别有用。
  • 流量控制 有缓冲区通道的大小可以用作一种流量控制机制,以防止快速发送者溢出慢速接收者。

阻塞通道的使用场景

场景: 数据处理管道

在一个数据处理管道中,一个协程生成数据,另一个协程处理数据。阻塞通道确保生产者和消费者能够同步执行,避免生产者过快地产生数据,导致内存溢出。

package main

import (
"fmt"
"time"
)

func producer(ch chan int) {
for i := 0; i < 10; i++ {
fmt.Println("Produced:", i)
ch <- i // 此处会阻塞,直到消费者准备好接收
}
close(ch)
}

func consumer(ch chan int) {
for value := range ch {
fmt.Println("Consumed:", value)
time.Sleep(time.Millisecond * 100) // 模拟处理时间
}
}

func main() {
ch := make(chan int)
go producer(ch)
consumer(ch)
}

非阻塞通道的使用场景及示例

场景: 事件通知

在一个事件驱动的程序中,可能需要一个非阻塞的方式来检查是否有新事件,同时继续执行其他任务。

package main

import (
"fmt"
"time"
)

func eventProducer(ch chan int) {
for i := 0; i < 10; i++ {
ch <- i
time.Sleep(time.Millisecond * 50) // 模拟事件产生的间隔
}
close(ch)
}

func main() {
ch := make(chan int, 10) // 创建带缓冲的通道
go eventProducer(ch)

for {
select {
case event, ok := <-ch:
if !ok {
fmt.Println("Channel is closed, no more events.")
return
}
fmt.Println("Received event:", event)
default:
fmt.Println("No new event, doing other work.")
time.Sleep(time.Millisecond * 100) // 模拟其他工作
}
}
}

在上面的示例中,select 语句的 default 分支提供了一种非阻塞的方式来检查通道,同时允许程序在没有新事件时执行其他任务。

syncWaitGroup 使用场景

· 阅读需 2 分钟
素明诚
Full stack development

sync.WaitGroup 它主要用于等待一组协程(goroutines)的执行完成。

  1. 并行处理:当你有一组独立的任务需要并行执行,而主协程需要等待所有任务完成时,你可以使用 sync.WaitGroup
  2. 错误处理和资源清理:如果你想确保在程序的主协程中,在所有的工作协程完成执行后,再执行一些错误处理或资源清理工作,sync.WaitGroup 是很有用的。
  3. 实现并发控制:通过使用 sync.WaitGroup,你可以确保在启动新的协程之前,之前的协程已经完成了它们的工作。
  4. 数据的并行处理:当你需要在不同的协程中并行处理数据,并且需要在所有处理完成后再进行下一步操作时,sync.WaitGroup 是非常有用的。
package main

import (
"fmt"
"sync"
)

func worker(wg *sync.WaitGroup, id int) {
defer wg.Done() // 在协程结束时调用 Done 方法
fmt.Printf("Worker %d starting\n", id)
// ... 这里是协程的工作代码 ...
fmt.Printf("Worker %d done\n", id)
}

func main() {
var wg sync.WaitGroup // 创建一个 WaitGroup 实例

for i := 1; i <= 5; i++ {
wg.Add(1) // 为每个启动的协程增加计数
go worker(&wg, i) // 启动协程
}

wg.Wait() // 等待所有协程完成
fmt.Println("All workers done")
}

在这个例子中,我们创建了一个 sync.WaitGroup 实例,并在启动每个协程时通过 Add 方法增加计数。每个工作协程在完成时调用 Done 方法来减少计数。主协程通过调用 Wait 方法等待所有工作协程完成执行。这样,我们可以确保所有工作协程完成后,再执行后续的代码。

协程Goroutine和线程之间的区别和关系

· 阅读需 4 分钟
素明诚
Full stack development

在 Windows 操作系统中,线程是内核级别的,由操作系统管理,每个线程对应一个执行路径;

协程是用户级别的,通常运行在一个线程之上,按照程序员通过代码定义的顺序执行;

Goroutine 是特定于 Go 语言的协程,由 Go 运行时管理

从 Windows 内核角度来说,它们就是普通的用户空间线程上的任务。

线程(Thread)

在 Windows 中,线程是能够在系统中执行的最小单位,是进程的一部分,共享进程的资源,但有自己的独立执行序列(或控制流)。

线程是由 Windows 内核管理和调度的,能够被操作系统调度器分配到不同的处理器核心上运行。

线程有自己的堆栈空间,用于存储局部变量和函数调用,但它们共享其所属进程的内存和资源。

线程之间的切换涉及到保存和加载寄存器,更新虚拟内存映射等,这在系统级别上是有开销的。

协程(Coroutine)

协程是用户级别的,意味着它们完全在用户空间中运行,Windows 内核并不直接管理或调度协程。

协程在单个线程中“多任务”,可以想象成在一个线程中有多个任务轮流执行,但在任何时刻都只有一个在工作。

协程的调度是程序员控制的,通常是通过库实现的。

协程之间的上下文切换非常轻量,因为不需要内核介入,只涉及到变量的状态变化。

Goroutine(Go 中的协程)

Goroutine 是 Go 语言特有的,是对协程概念的一种实现,但具有一些独特的属性,尤其是它们之间的通信方式。

在 Windows 上运行的 Go 程序会启动一些线程,Go 运行时(runtime)会在这些线程上调度许多 Goroutine。

尽管在技术上,每个 Goroutine 都是一个独立的执行路径,但它们实际上是在这些线程之上运行的。

如果你还没明白,可以看下面这个例子

线程(Thread):想象线程就像是公司的员工。每个员工都有自己的任务和责任,但他们共享公司的资源(例如办公室、打印机等)。员工(线程)的上下班(开始和结束线程)以及工作调度(线程切换)由公司管理层(操作系统)控制,想想就挺残忍的。

如果公司要新增一个员工或者安排员工之间的工作,这需要管理层的直接参与,也会涉及到较多的人力和物力资源(也就是说,线程的创建和上下文切换成本相对较高)。

协程(Coroutine):现在想象协程就像是在家工作的自由职业者。他们使用自己的电脑和办公设备(拥有自己的堆栈和局部变量),并且自己决定什么时候工作、什么时候休息(编程者控制)。

他们可以随时暂停工作去喝杯咖啡或是散步(yield 或等待),然后再回来继续工作。所有这些活动的安排都不需要外部管理层的参与(用户级的调度),并且几乎不需要额外的资源(低成本的任务切换)。

Goroutine(Go 中的协程):Goroutine 就像是使用特殊工作方法的自由职业者团队。他们不仅可以自己安排工作时间(用户态调度),还使用一种特殊的通信方式 —— 他们不会直接交谈(共享内存),而是通过写信(传递消息)来沟通(Go 语言的通道机制)。这种工作方式使他们的合作更加高效和有序(并发编程更容易实现和管理)。

特性/机制线程 (Thread)协程 (Coroutine)Goroutine (Go 语言)
调度操作系统调度用户程序调度Go 运行时环境调度
上下文切换开销较大开销较小开销较小
内存占用相对较高相对较低相对较低
创建和销毁开销较高较低较低
执行模型并发/并行执行协作式并发并发/并行执行
资源共享可共享进程资源通常独立的栈和局部变量可共享内存
应用场景广泛的并发/并行处理协作式多任务,事件驱动简化并发编程,网络服务

解析 Go 语言的 GMP 并发模型

· 阅读需 3 分钟
素明诚
Full stack development

GMP 并发模型

Goroutine (G)

Goroutine 是 Go 语言中的轻量级线程,每个 Goroutine 代表一个并发执行的任务。它们的创建和销毁成本很低,且运行时系统会自动处理它们的调度和执行。

Machine (M)

Machine 是操作系统级的线程,是真正执行代码的实体。每个 M 都绑定到一个操作系统线程,它执行与之关联的 Goroutine 的代码。

Processor (P)

Processor 是逻辑处理器,它负责调度 Goroutine 到 M 上执行。每个 P 有自己的本地运行队列,存放等待执行的 Goroutine。

GMP 模型的核心工作细节

Goroutine 调度

Go 的运行时系统维护了全局和本地的运行队列。全局运行队列包含所有新创建和被阻塞后重新就绪的 Goroutine。每个 P 的本地运行队列包含了即将由该 P 调度的 Goroutine。只有在本地运行队列装满后(256 个 Goroutine)才会把新来的 Goroutine 放入本地运行队列

当 Goroutine 需要执行系统调用时,它可能会被阻塞。在这种情况下,与 Goroutine 关联的线程(M)会被解除绑定,并可能会为其他未阻塞的 Goroutine 创建一个新的 M。

Go 的运行时系统还管理一个定时器堆,用于处理定时事件。Goroutine 可以创建定时器,而运行时系统将确保在指定的时间后将它们唤醒。

Go 的运行时系统包含一个网络轮询器,它可以在非阻塞模式下检查网络 I/O。如果一个 Goroutine 在等待网络 I/O,它会被放入网络轮询器,直到 I/O 准备好为止。

线程创建和销毁

当所有的 M 都在执行 Goroutine,并且还有更多的 Goroutine 等待执行时,运行时系统可能会创建新的 M。一旦 M 完成了 Goroutine 的执行并且没有更多的 Goroutine 等待执行,M 可能会被销毁或放回线程池以备后用。

例如当线程池中的线程数量超过了一定的阈值或者线程已经存在了很长时间没有被重用时,运行时系统可能会选择销毁 M 以释放资源。

抢占式调度

Go 运行时系统实现了抢占式调度,以确保 Goroutine 公平地获得执行时间。如果一个 Goroutine 长时间占用 CPU,这个时间大概是 10ms,超过 10ms,运行时系统会抢占它,将它放回运行队列,并允许其他 Goroutine 执行。

网络和系统调用

当 Goroutine 阻塞在网络或系统调用时,它的 M 会解除与 P 的绑定,使 P 可以调度和执行其他 Goroutine。一旦系统调用完成,Goroutine 会再次被放入运行队列,等待被调度执行。

垃圾回收的协助

Go 的垃圾回收是并发执行的,它与 GMP 模型紧密集成。Goroutine 会在必要时协助运行时系统进行垃圾回收。

Work stealing

当一个 P 的本地运行队列为空时,它会尝试从其他 P 的本地运行队列中“偷取” Goroutine,以保持 CPU 的利用率,一次偷走一半。