Golanggoroutine池实现与资源管理技巧

Goroutine池通过限制并发数防止资源耗尽,提升系统稳定性与性能可预测性,适用于高并发场景下的资源控制与任务调度。

golanggoroutine池实现与资源管理技巧

Golang中的goroutine池,说到底,就是一种更精细的并发控制手段。我们都知道goroutine轻量,创建销毁成本极低,但“低”不代表“无”。当并发量冲到极致,或者任务本身对外部资源(比如数据库连接、文件句柄、下游API调用)有严格限制时,无限制地创建goroutine就可能带来性能瓶颈,甚至系统崩溃。所以,goroutine池的核心价值在于,它提供了一个可控的并发上限,让系统在处理大量并发任务时,能保持稳定、可预测的性能表现,避免资源耗尽。它本质上是一种用空间(一个固定大小的goroutine集合)换时间(更稳定的执行和更低的资源争抢)的策略。

解决方案

实现一个goroutine池,最常见也最直观的方式是利用Go的通道(channel)机制。我们可以创建一个固定数量的worker goroutine,它们都监听同一个任务通道。当有新任务到来时,将其发送到任务通道;空闲的worker会从通道中取出任务并执行。这样,无论外部提交多少任务,同时运行的worker数量始终保持在预设的上限。

一个基础的实现通常包含以下几个部分:

任务通道(Task Channel):这是一个缓冲通道,用来接收待处理的任务。任务可以是任何可执行的函数,通常定义为一个

func()

类型。工作者(Worker Goroutines):固定数量的goroutine,它们会持续从任务通道中读取任务并执行。管理结构(Pool Struct):封装任务通道、工作者数量以及一些控制池生命周期的机制(如

sync.WaitGroup

用于等待所有任务完成,或者

context.Context

用于取消)。

以下是一个简化的代码骨架:

立即学习“go语言免费学习笔记(深入)”;

package mainimport (    "fmt"    "sync"    "time")// WorkerPool 定义了goroutine池的结构type WorkerPool struct {    taskQueue chan func() // 任务队列    workerNum int         // 工作者数量    wg        sync.WaitGroup // 用于等待所有任务完成    quit      chan struct{} // 退出信号}// NewWorkerPool 创建一个新的goroutine池func NewWorkerPool(workerNum int) *WorkerPool {    if workerNum <= 0 {        workerNum = 1 // 至少一个工作者    }    return &WorkerPool{        taskQueue: make(chan func()),        workerNum: workerNum,        quit:      make(chan struct{}),    }}// Start 启动goroutine池func (p *WorkerPool) Start() {    for i := 0; i < p.workerNum; i++ {        p.wg.Add(1)        go p.worker(i)    }}// worker 是实际执行任务的goroutinefunc (p *WorkerPool) worker(id int) {    defer p.wg.Done()    for {        select {        case task, ok := <-p.taskQueue:            if !ok { // 任务队列已关闭                fmt.Printf("Worker %d: Task queue closed, exiting.n", id)                return            }            fmt.Printf("Worker %d: Starting task.n", id)            task() // 执行任务            fmt.Printf("Worker %d: Finished task.n", id)        case <-p.quit: // 收到退出信号            fmt.Printf("Worker %d: Received quit signal, exiting.n", id)            return        }    }}// Submit 提交一个任务到goroutine池func (p *WorkerPool) Submit(task func()) {    p.taskQueue <- task}// Shutdown 关闭goroutine池,等待所有任务完成func (p *WorkerPool) Shutdown() {    close(p.taskQueue) // 关闭任务队列,通知所有worker不再接收新任务    // 发送退出信号给所有worker,这在某些情况下可能需要,但通常关闭taskQueue就足够了    // for i := 0; i < p.workerNum; i++ {    //  p.quit <- struct{}{}    // }    p.wg.Wait() // 等待所有worker完成    close(p.quit) // 关闭退出信号通道    fmt.Println("Worker pool shutdown complete.")}func main() {    pool := NewWorkerPool(3) // 创建一个包含3个worker的goroutine池    pool.Start()    // 提交一些任务    for i := 0; i < 10; i++ {        taskID := i        pool.Submit(func() {            time.Sleep(time.Duration(taskID%3+1) * time.Second) // 模拟耗时任务            fmt.Printf("Task %d processed.n", taskID)        })    }    time.Sleep(2 * time.Second) // 给一些任务处理时间    pool.Shutdown() // 关闭池    fmt.Println("Main goroutine finished.")}

这个例子展示了一个最基础的池实现。

Submit

方法将任务放入通道,如果通道已满,

Submit

调用会阻塞,直到有worker取出任务,这是一种隐式的流量控制。

Shutdown

方法通过关闭任务通道来优雅地通知所有worker退出,并使用

WaitGroup

等待它们完成。

为什么我们需要Goroutine池,它能解决哪些实际问题?

我个人觉得,goroutine池的出现,很大程度上是对“goroutine很便宜”这句话的补充和校正。没错,goroutine启动和销毁的开销确实比线程小很多,但“便宜”不等于“免费”,更不等于“无限”。当你的系统并发量达到某个临界点时,即使是轻量级的goroutine,也可能带来一系列问题,而goroutine池就是用来解决这些问题的:

资源耗尽与系统稳定性:这是最直接的痛点。想象一下,一个高并发的服务,突然涌入成千上万的请求,每个请求都可能启动一个goroutine去处理。如果这些goroutine都去争抢有限的资源(比如数据库连接池的连接、文件句柄、网络带宽),很快就会导致资源枯竭。内存可能飙升,CPU上下文切换开销巨大,甚至系统因为无法分配新资源而崩溃。goroutine池通过限制并发执行的上限,就像给水龙头装了个限流阀,确保系统始终在可承受的范围内运行。性能可预测性:没有池的情况下,系统负载高低起伏,性能表现也可能忽好忽坏。有了池,你可以设定一个合理的并发数,让系统在面对突发流量时,能保持一个相对稳定的响应时间,而不是直接“躺平”。它把“尽力而为”变成了“尽力而为,但别超负荷”。外部服务限流:很多时候,我们调用的外部服务(比如第三方API、数据库、缓存)都有自己的QPS(每秒查询数)或并发连接数限制。如果我们的服务一股脑地发起大量请求,很容易触发对方的限流机制,导致请求失败甚至IP被封。通过goroutine池,我们可以精确控制对这些外部服务的并发访问,成为一个“好公民”,避免被惩罚。避免“goroutine爆炸”:这是一种形象的说法,指代因为无限创建goroutine而导致的内存占用暴增、调度器负担加重等问题。特别是在一些递归处理、批处理任务中,如果逻辑设计不当,很容易无意中创建出天文数字的goroutine。池化机制从根本上避免了这种失控。

举个例子,我曾经手头有个数据同步任务,需要从一个系统拉取大量数据,然后经过一系列处理后写入另一个系统。如果直接为每条数据启动一个goroutine,在数据量大的时候,内存占用会迅速突破GB级别,而且数据库连接池也会被瞬间打爆。引入goroutine池后,我将处理数据的并发数限制在几十个,内存占用稳定了,数据库也表示“压力不大”,整个任务运行得又快又稳。这让我意识到,并非所有场景都适合无限制的并发,适度的限制反而是性能和稳定性的保障。

如何设计一个高效且健壮的Golang Goroutine池?

设计一个真正高效且健壮的goroutine池,不只是把上面的基础骨架搭起来那么简单,还需要考虑很多细节,确保它能在各种复杂场景下稳定运行。这就像盖房子,地基打好后,还要考虑抗震、防水、采光等等。

任务提交机制:阻塞还是非阻塞?

我上面给的例子是阻塞式提交:当任务通道满时,

Submit

调用会一直等待,直到有worker取出任务。这种方式的优点是简单,能自然地实现流量控制,防止任务堆积过多。缺点是如果池子长期饱和,提交任务的goroutine可能会长时间阻塞。非阻塞提交:可以通过

select

语句结合

default

分支来实现。如果任务通道满,

Submit

不会阻塞,而是立即返回一个错误或者丢弃任务。这适用于对实时性要求高、可以容忍少量任务丢失的场景。带超时提交:在阻塞提交的基础上,加入

context.WithTimeout

time.After

,如果一定时间内任务无法提交,则放弃。这提供了一种折衷方案。

优雅关闭与任务完成等待

sync.WaitGroup

:这是最常见的做法。在启动每个worker时

wg.Add(1)

,worker退出时

wg.Done()

,关闭池时

wg.Wait()

。这样可以确保所有worker都处理完当前任务并退出后,池才真正关闭。

context.Context

:对于更复杂的场景,

context.Context

可以用来传递取消信号。当池需要关闭时,可以取消顶层

Context

,worker在处理任务时会定期检查

Context

Done()

通道,如果收到信号就提前退出。这对于那些可能长时间运行、需要中断的任务尤其有用。

错误处理与任务结果返回

默认的

func()

任务无法直接返回错误或结果。如果任务需要返回结果,你需要修改任务的类型,例如

func() (interface{}, error)

,并在提交任务时,将一个带有结果通道的结构体传递进去。一个常见的模式是,任务的定义是一个带有结果通道的闭包,或者池提供一个

SubmitWithResult

方法,返回一个

chan Result

池的容量与性能调优

池的大小(

workerNum

)不是越大越好。它应该根据你的任务类型来决定:I/O密集型任务(如网络请求、数据库查询):这类任务大部分时间在等待I/O,CPU利用率不高。可以适当增大池的容量,通常可以设置为

2 * runtime.NumCPU() + N

,甚至更高,因为很多goroutine在等待时并不占用CPU。CPU密集型任务(如复杂计算、图像处理):这类任务会长时间占用CPU。池的容量最好接近或等于

runtime.NumCPU()

,避免过多的上下文切换开销。实际应用中,池的大小往往需要通过压力测试和监控来确定最佳值。

监控与可观测性

一个健壮的池应该能够暴露其内部状态,例如:当前任务队列的长度(

len(p.taskQueue)

)。已完成任务的数量。正在执行任务的worker数量。任务的平均执行时间。这些指标对于判断池是否饱和、是否存在瓶颈至关重要。

// 一个更健壮的WorkerPool结构示例,包含结果和错误处理type Result struct {    Value interface{}    Err   error}type Task func(ctx context.Context) Resulttype RobustWorkerPool struct {    taskQueue   chan Task    resultsChan chan Result // 用于收集任务结果    workerNum   int    wg          sync.WaitGroup    ctx         context.Context    cancel      context.CancelFunc}func NewRobustWorkerPool(workerNum int, resultBufferSize int) *RobustWorkerPool {    ctx, cancel := context.WithCancel(context.Background())    if workerNum <= 0 {        workerNum = 1    }    if resultBufferSize < workerNum {        resultBufferSize = workerNum // 至少能缓冲与worker数量相同的任务结果    }    return &RobustWorkerPool{        taskQueue:   make(chan Task),        resultsChan: make(chan Result, resultBufferSize),        workerNum:   workerNum,        ctx:         ctx,        cancel:      cancel,    }}func (p *RobustWorkerPool) Start() {    for i := 0; i < p.workerNum; i++ {        p.wg.Add(1)        go p.worker(i)    }}func (p *RobustWorkerPool) worker(id int) {    defer p.wg.Done()    for {        select {        case task, ok := <-p.taskQueue:            if !ok {                return // 任务队列已关闭            }            res := task(p.ctx) // 执行任务,传递上下文            select {            case p.resultsChan <- res: // 将结果发送到结果通道            case <-p.ctx.Done(): // 如果池已关闭,则放弃结果                fmt.Printf("Worker %d: Pool shutting down, discarding result.n", id)                return            }        case <-p.ctx.Done(): // 收到取消信号            return        }    }}func (p *RobustWorkerPool) Submit(task Task) error {    select {    case p.taskQueue <- task:        return nil    case <-p.ctx.Done():        return p.ctx.Err() // 池已关闭    default: // 非阻塞提交,如果通道满则报错        return fmt.Errorf("task queue is full")    }}func (p *RobustWorkerPool) GetResults() <-chan Result {    return p.resultsChan}func (p *RobustWorkerPool) Shutdown() {    p.cancel()          // 发送取消信号给所有worker    close(p.taskQueue) // 关闭任务队列,确保所有待处理任务被取出    p.wg.Wait()         // 等待所有worker完成    close(p.resultsChan) // 关闭结果通道    fmt.Println("Robust Worker pool shutdown complete.")}// 示例用法func mainRobustPool() {    pool := NewRobustWorkerPool(2, 5) // 2个worker,结果通道缓冲5个    pool.Start()    // 提交一些任务    for i := 0; i < 7; i++ { // 提交7个任务,但池只有2个worker        taskID := i        err := pool.Submit(func(ctx context.Context) Result {            select {            case <-ctx.Done():                return Result{nil, fmt.Errorf("task %d cancelled", taskID)}            case <-time.After(time.Duration(taskID%3+1) * time.Second): // 模拟耗时                return Result{fmt.Sprintf("Processed %d", taskID), nil}            }        })        if err != nil {            fmt.Printf("Failed to submit task %d: %vn", taskID, err)        }    }    // 收集结果    go func() {        for res := range pool.GetResults() {            if res.Err != nil {                fmt.Printf("Task error: %vn", res.Err)            } else {                fmt.Printf("Task result: %vn", res.Value)            }        }        fmt.Println("Result collector finished.")    }()    time.Sleep(5 * time.Second)    pool.Shutdown()    fmt.Println("Main robust pool goroutine finished.")}

这个

RobustWorkerPool

的例子加入了

context.Context

用于取消,并且通过

resultsChan

来异步收集任务结果,同时

Submit

方法也变成了非阻塞的,如果队列满会返回错误。这在实际项目中会更有用。

Goroutine池在使用中常见的陷阱与资源管理技巧有哪些?

即使设计得再精妙,goroutine池在使用中依然有一些“坑”和需要注意的资源管理细节。我踩过一些,所以深知这些地方的重要性。

死锁与任务依赖:这是最隐蔽也最麻烦的问题之一。如果池中的任务A需要等待池中的任务B完成,而池的容量又不足以同时容纳A和B,那么就可能发生死锁。任务A提交后占用了一个worker,然后等待任务B。如果任务B也需要提交到同一个池,但此时池已满,B就无法提交,A也就永远等不到B,导致整个池阻塞。技巧:避免在同一个池内的任务之间创建循环依赖。如果任务有依赖关系,考虑使用不同的池,或者将依赖任务作为子任务在当前goroutine中直接执行(如果它不耗时且不会阻塞),或者使用

sync.Once

sync.Cond

等更高级的并发原语来协调。任务饥饿(Task Starvation):如果池中的任务队列是先进先出(FIFO)的,那么一些耗时较长的任务可能会导致后续的短任务长时间得不到执行,即使有空闲的worker。技巧:对于有不同优先级或时效性要求的任务,可能需要实现多个任务队列,或者使用优先级队列。当然,这会增加池实现的复杂性。资源泄露:虽然goroutine本身不会造成内存泄露(Go的GC会回收),但如果goroutine持有的外部资源(如文件句柄、数据库连接、网络连接)没有正确关闭或释放,就会导致资源泄露。即使goroutine池限制了goroutine数量,如果每个任务都泄露资源,最终系统还是会崩溃。技巧:在每个任务内部,务必确保所有打开的资源都在

defer

语句中正确关闭。对于数据库连接池这类资源,goroutine池应该与连接池协同工作,而不是替代连接池。任务从连接池获取连接,使用后归还。Context传播与取消:在微服务架构中,

context.Context

用于传递请求ID、超时和取消信号。当任务进入goroutine池时,原始的

Context

如何有效地传递到池内的worker中,并能响应取消信号,是一个关键点。技巧:任务的定义应该接受一个

context.Context

参数。在提交任务时,将原始请求的

Context

传递给任务。worker在执行任务时,如果任务耗时,应定期检查

ctx.Done()

,以便及时响应取消信号。池大小的动态调整:虽然我们说池的大小是固定的,但在某些极端场景下,如果负载变化巨大,固定的池大小可能不够灵活。技巧:可以考虑实现一个“弹性”的goroutine池,根据任务队列的长度、CPU利用率等指标,动态地增加或减少worker的数量。但这会显著增加实现的复杂性,通常只在对性能和资源利用率有极高要求的场景下才考虑。与外部资源池的协同:goroutine池和数据库连接池、HTTP客户端连接池等是不同层面的概念。goroutine池管理的是计算并发,而外部资源池管理的是特定资源的并发访问。

以上就是Golanggoroutine池实现与资源管理技巧的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1403252.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月15日 19:25:42
下一篇 2025年12月15日 19:25:58

相关推荐

  • CSS mask属性无法获取图片:为什么我的图片不见了?

    CSS mask属性无法获取图片 在使用CSS mask属性时,可能会遇到无法获取指定照片的情况。这个问题通常表现为: 网络面板中没有请求图片:尽管CSS代码中指定了图片地址,但网络面板中却找不到图片的请求记录。 问题原因: 此问题的可能原因是浏览器的兼容性问题。某些较旧版本的浏览器可能不支持CSS…

    2025年12月24日
    900
  • Uniapp 中如何不拉伸不裁剪地展示图片?

    灵活展示图片:如何不拉伸不裁剪 在界面设计中,常常需要以原尺寸展示用户上传的图片。本文将介绍一种在 uniapp 框架中实现该功能的简单方法。 对于不同尺寸的图片,可以采用以下处理方式: 极端宽高比:撑满屏幕宽度或高度,再等比缩放居中。非极端宽高比:居中显示,若能撑满则撑满。 然而,如果需要不拉伸不…

    2025年12月24日
    400
  • 如何让小说网站控制台显示乱码,同时网页内容正常显示?

    如何在不影响用户界面的情况下实现控制台乱码? 当在小说网站上下载小说时,大家可能会遇到一个问题:网站上的文本在网页内正常显示,但是在控制台中却是乱码。如何实现此类操作,从而在不影响用户界面(UI)的情况下保持控制台乱码呢? 答案在于使用自定义字体。网站可以通过在服务器端配置自定义字体,并通过在客户端…

    2025年12月24日
    800
  • 如何在地图上轻松创建气泡信息框?

    地图上气泡信息框的巧妙生成 地图上气泡信息框是一种常用的交互功能,它简便易用,能够为用户提供额外信息。本文将探讨如何借助地图库的功能轻松创建这一功能。 利用地图库的原生功能 大多数地图库,如高德地图,都提供了现成的信息窗体和右键菜单功能。这些功能可以通过以下途径实现: 高德地图 JS API 参考文…

    2025年12月24日
    400
  • 如何使用 scroll-behavior 属性实现元素scrollLeft变化时的平滑动画?

    如何实现元素scrollleft变化时的平滑动画效果? 在许多网页应用中,滚动容器的水平滚动条(scrollleft)需要频繁使用。为了让滚动动作更加自然,你希望给scrollleft的变化添加动画效果。 解决方案:scroll-behavior 属性 要实现scrollleft变化时的平滑动画效果…

    2025年12月24日
    000
  • 如何为滚动元素添加平滑过渡,使滚动条滑动时更自然流畅?

    给滚动元素平滑过渡 如何在滚动条属性(scrollleft)发生改变时为元素添加平滑的过渡效果? 解决方案:scroll-behavior 属性 为滚动容器设置 scroll-behavior 属性可以实现平滑滚动。 html 代码: click the button to slide right!…

    2025年12月24日
    500
  • 为什么设置 `overflow: hidden` 会导致 `inline-block` 元素错位?

    overflow 导致 inline-block 元素错位解析 当多个 inline-block 元素并列排列时,可能会出现错位显示的问题。这通常是由于其中一个元素设置了 overflow 属性引起的。 问题现象 在不设置 overflow 属性时,元素按预期显示在同一水平线上: 不设置 overf…

    2025年12月24日 好文分享
    400
  • 网页使用本地字体:为什么 CSS 代码中明明指定了“荆南麦圆体”,页面却仍然显示“微软雅黑”?

    网页中使用本地字体 本文将解答如何将本地安装字体应用到网页中,避免使用 src 属性直接引入字体文件。 问题: 想要在网页上使用已安装的“荆南麦圆体”字体,但 css 代码中将其置于第一位的“font-family”属性,页面仍显示“微软雅黑”字体。 立即学习“前端免费学习笔记(深入)”; 答案: …

    2025年12月24日
    000
  • 如何选择元素个数不固定的指定类名子元素?

    灵活选择元素个数不固定的指定类名子元素 在网页布局中,有时需要选择特定类名的子元素,但这些元素的数量并不固定。例如,下面这段 html 代码中,activebar 和 item 元素的数量均不固定: *n *n 如果需要选择第一个 item元素,可以使用 css 选择器 :nth-child()。该…

    2025年12月24日
    200
  • 使用 SVG 如何实现自定义宽度、间距和半径的虚线边框?

    使用 svg 实现自定义虚线边框 如何实现一个具有自定义宽度、间距和半径的虚线边框是一个常见的前端开发问题。传统的解决方案通常涉及使用 border-image 引入切片图片,但是这种方法存在引入外部资源、性能低下的缺点。 为了避免上述问题,可以使用 svg(可缩放矢量图形)来创建纯代码实现。一种方…

    2025年12月24日
    100
  • 如何让“元素跟随文本高度,而不是撑高父容器?

    如何让 元素跟随文本高度,而不是撑高父容器 在页面布局中,经常遇到父容器高度被子元素撑开的问题。在图例所示的案例中,父容器被较高的图片撑开,而文本的高度没有被考虑。本问答将提供纯css解决方案,让图片跟随文本高度,确保父容器的高度不会被图片影响。 解决方法 为了解决这个问题,需要将图片从文档流中脱离…

    2025年12月24日
    000
  • 为什么我的特定 DIV 在 Edge 浏览器中无法显示?

    特定 DIV 无法显示:用户代理样式表的困扰 当你在 Edge 浏览器中打开项目中的某个 div 时,却发现它无法正常显示,仔细检查样式后,发现是由用户代理样式表中的 display none 引起的。但你疑问的是,为什么会出现这样的样式表,而且只针对特定的 div? 背后的原因 用户代理样式表是由…

    2025年12月24日
    200
  • inline-block元素错位了,是为什么?

    inline-block元素错位背后的原因 inline-block元素是一种特殊类型的块级元素,它可以与其他元素行内排列。但是,在某些情况下,inline-block元素可能会出现错位显示的问题。 错位的原因 当inline-block元素设置了overflow:hidden属性时,它会影响元素的…

    2025年12月24日
    000
  • 为什么 CSS mask 属性未请求指定图片?

    解决 css mask 属性未请求图片的问题 在使用 css mask 属性时,指定了图片地址,但网络面板显示未请求获取该图片,这可能是由于浏览器兼容性问题造成的。 问题 如下代码所示: 立即学习“前端免费学习笔记(深入)”; icon [data-icon=”cloud”] { –icon-cl…

    2025年12月24日
    200
  • 为什么使用 inline-block 元素时会错位?

    inline-block 元素错位成因剖析 在使用 inline-block 元素时,可能会遇到它们错位显示的问题。如代码 demo 所示,当设置了 overflow 属性时,a 标签就会错位下沉,而未设置时却不会。 问题根源: overflow:hidden 属性影响了 inline-block …

    2025年12月24日
    000
  • 如何利用 CSS 选中激活标签并影响相邻元素的样式?

    如何利用 css 选中激活标签并影响相邻元素? 为了实现激活标签影响相邻元素的样式需求,可以通过 :has 选择器来实现。以下是如何具体操作: 对于激活标签相邻后的元素,可以在 css 中使用以下代码进行设置: li:has(+li.active) { border-radius: 0 0 10px…

    2025年12月24日
    100
  • 为什么我的 CSS 元素放大效果无法正常生效?

    css 设置元素放大效果的疑问解答 原提问者在尝试给元素添加 10em 字体大小和过渡效果后,未能在进入页面时看到放大效果。探究发现,原提问者将 CSS 代码直接写在页面中,导致放大效果无法触发。 解决办法如下: 将 CSS 样式写在一个单独的文件中,并使用 标签引入该样式文件。这个操作与原提问者观…

    2025年12月24日
    000
  • 如何模拟Windows 10 设置界面中的鼠标悬浮放大效果?

    win10设置界面的鼠标移动显示周边的样式(探照灯效果)的实现方式 在windows设置界面的鼠标悬浮效果中,光标周围会显示一个放大区域。在前端开发中,可以通过多种方式实现类似的效果。 使用css 使用css的transform和box-shadow属性。通过将transform: scale(1.…

    2025年12月24日
    200
  • 为什么我的 em 和 transition 设置后元素没有放大?

    元素设置 em 和 transition 后不放大 一个 youtube 视频中展示了设置 em 和 transition 的元素在页面加载后会放大,但同样的代码在提问者电脑上没有达到预期效果。 可能原因: 问题在于 css 代码的位置。在视频中,css 被放置在单独的文件中并通过 link 标签引…

    2025年12月24日
    100
  • 为什么我的 Safari 自定义样式表在百度页面上失效了?

    为什么在 Safari 中自定义样式表未能正常工作? 在 Safari 的偏好设置中设置自定义样式表后,您对其进行测试却发现效果不同。在您自己的网页中,样式有效,而在百度页面中却失效。 造成这种情况的原因是,第一个访问的项目使用了文件协议,可以访问本地目录中的图片文件。而第二个访问的百度使用了 ht…

    2025年12月24日
    000

发表回复

登录后才能评论
关注微信