跳到主要内容

91 篇博文 含有标签「后端」

后端开发技术文章

查看所有标签

解决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 启动项目。

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 的利用率,一次偷走一半。

Gradio 配合 FastAPI 搭建交互式Web应用与REST API服务

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

虚拟环境准备

进入项目文件夹,创建虚拟环境

python -m venv venv

激活环境

source venv/bin/activate  # 在Windows上使用 venv\Scripts\activate.bat

安装依赖

pip install gradio fastapi uvicorn

编写核心代码

在一个 Python 文件(例如core_logic.py)中编写你的核心逻辑。

def login(username, password):
if username == "admin" and password == "123123":
return "Login successful!"
else:
return "Login failed. Please check your credentials."

使用 gradio 创建页面

在另一个 Python 文件(例如gradio_app.py)中创建 Gradio 界面。

import gradio as gr
from core_logic import login

iface = gr.Interface(
fn=login,
inputs=["text", "password"],
outputs="text"
)

if __name__ == "__main__":
iface.launch()

创建 FastAPI 应用

在另一个 Python 文件(例如fastapi_app.py)中创建 FastAPI 应用

from fastapi import FastAPI
from pydantic import BaseModel
from core_logic import login

app = FastAPI()

class LoginData(BaseModel):
username: str
password: str

@app.post("/login/")
async def api_login(data: LoginData):
return {"message": login(data.username, data.password)}

if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=7861)

运行应用

为 Gradio 界面运行gradio_app.py,并通过浏览器访问 Gradio 界面。

python gradio_app.py

为 FastAPI 应用运行fastapi_app.py,并通过浏览器或 API 客户端访问 FastAPI 提供的接口。

python fastapi_app.py

PS:这只是一个模板,如果想继续添加 UI 界面或者是接口需要你自己继续开发

Go 的数组为什么是值类型

· 阅读需 2 分钟
素明诚
Full stack development
	a := [3]int{1, 2, 3}
b := a // a 的值被复制到 b
b[0] = 42 // 修改 b 不会影响 a
fmt.Println(a) // 输出:[1 2 3]
fmt.Println(b) // 输出:[42 2 3]

性能

  • 这可能是一个重要因素。在栈上分配内存通常比在堆上分配和管理内存更快,特别是对于小数组。此外,值类型数组可以避免额外的垃圾收集开销。

语言简单性和一致性

  • Go 语言的设计者倾向于保持语言的简单性和一致性。将数组设计为值类型可以使语法更简单,更容易理解,特别是对于新手程序员。

明确的语义

  • 数组作为值类型提供了明确的语义,使得程序员可以很容易地理解代码的行为,特别是在赋值和参数传递时。

切片的引入

  • 通过引入切片作为引用类型,Go 语言为动态数组提供了一个强大而灵活的解决方案,同时保留了数组的简单性和效率。

数据安全

  • 值类型数组在多线程环境中提供了更好的数据安全性,因为它们不会被意外共享。

避免隐藏的性能陷阱

  • 在某些语言中,由于数组是引用类型,可能会引入隐藏的性能陷阱,例如意外的内存分配和垃圾收集。将数组作为值类型可以避免这些陷阱。

遗产和历史原因

  • Go 语言的设计可能受到其设计者以往经验和其他语言的影响,特别是 C 语言,其中数组也是值类型。

使用现有的Gradio接口创建一个FastAPI应用

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

确保已经安装了

pip install fastapi gradio uvicorn

创建你的 Gradio 和 FastAPI 应用

import gradio as gr
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
import uvicorn

# 定义你的模型功能
def model_function(input_text: str):
text_length = len(input_text)
return f"The text has {text_length} characters."

# 创建你的Gradio接口
gradio_iface = gr.Interface(fn=model_function, inputs="text", outputs="text")

# 创建你的FastAPI应用
app = FastAPI()

@app.get("/", response_class=HTMLResponse)
async def read_root():
# 返回 Gradio 界面的 HTML
return gradio_iface._get_html()

@app.get("/predict/")
def predict(input_text: str):
# 直接调用Gradio接口的模型功能
return {"result": gradio_iface.process({"text": input_text})["text"]}

# 如果是直接运行此脚本,启动Uvicorn服务器
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)

现在,你可以运行此脚本,然后访问 http://localhost:8000 来查看和交互 Gradio 界面,或者访问 http://localhost:8000/docs 来查看和交互 FastAPI 自动生成的 Swagger 文档。