news 2026/9/25 2:54:55

从零上手 gMock:C++ 单元测试中的 Mock 对象与交互验证完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零上手 gMock:C++ 单元测试中的 Mock 对象与交互验证完全指南
  • 性能剖析
  • 内存管理
  • 开发工具

【免费下载链接】gperftools

Main gperftools repository

项目地址:https://gitcode.com/gh_mirrors/gp/gperftools
点击查看免费下载

gMock(Google Mock)是随 googletest 一起分发的 C++ Mock 框架,它让你无需手写桩代码,就能通过简单宏生成 mock 类、用类英文的 DSL 语法设定期望,并自动验证被测代码与外部依赖之间的调用交互。本文以 gperftools 仓库中随附的 gMock 源码与测试基础设施为佐证,带你完整掌握 mock 的写法、期望的设定、次数约束、返回值行为与调用顺序控制,最终在单元测试中实现"快、稳、准"的依赖隔离。

gMock 是什么:Mock 对象与 Fake 对象的本质区别

当你编写原型或测试时,往往不能(也不应该)完全依赖真实对象。mock 对象实现了与真实对象相同的接口(因此可以被当作真实对象使用),但允许你在运行时指定它"应当如何被使用":哪些方法会被调用、以什么顺序调用、调用多少次、传入什么参数、返回什么结果等等。

在测试驱动开发(TDD)社区中,fake与mock是两个经常被混淆但含义截然不同的概念:

  • Fake(假对象):拥有可工作的实现,但通常走一些捷径(例如让操作更廉价),因此不适合用于生产环境。一个内存文件系统就是典型的 fake。
  • Mock(模拟对象):预先用expectations(期望)编程的对象,这些期望构成了它"预期会收到哪些调用"的规格说明。

最核心的一点:mock 允许你验证它自身与被测代码之间的交互。fake 与 mock 的差异在你真正开始使用 mock 后会愈发清晰。

gMock正是这样一套用于创建并使用 mock 类的库(官方文档戏称它为 "framework" 以显得更酷)。它对 C++ 的意义,相当于 jMock / EasyMock 对 Java 的意义。使用 gMock 的典型三步流程是:

  1. 用几个简单宏描述你要 mock 的接口,宏会自动展开为 mock 类的实现;
  2. 创建若干 mock 对象,用直观的语法设定其期望与行为;
  3. 运行使用这些 mock 对象的被测代码——一旦违反期望,gMock 会立即捕获错误。

在 gperftools 仓库中,gMock 与 googletest 以 vendor 形式内置在 vendor/googletest 目录下,主头文件 gmock.h 的注释明确说明了它所支持的ON_CALL与EXPECT_CALL语法形态,可作为后续章节的语法权威参考。

为什么需要 gMock:手工 mock 的三大痛点

虽然 mock 对象能帮助测试移除不必要的依赖、让测试又快又可靠,但在 C++ 中手工编写 mock 极其痛苦:

  • 必须有人实现 mock,这项工作通常冗长且易错,难怪开发者宁愿绕远路去避开它;
  • 手工 mock 的质量难以预测——可能见到打磨精致的实现,也可能见到赶工拼凑、带各种临时限制的版本;
  • 从一个 mock 上学到的知识无法迁移到下一个 mock。

反观 Java 和 Python 社区,jMock、EasyMock 等成熟框架让 mock 的创建完全自动化,mock 因此在那些社区里被证明是高效且被广泛采纳的技术——趁手的工具完全能改变结果。gMock 正是为 C++ 程序员打造的,它受 jMock 与 EasyMock 启发,但针对 C++ 的特性做了专门设计。如果你的项目遇到以下问题,gMock 就是你的帮手:

  • 受困于欠佳的设计,希望早点多做原型验证,但 C++ 的原型开发远谈不上"快速";
  • 测试依赖太多库或昂贵资源(如数据库)而运行缓慢;
  • 测试依赖不可靠的资源(如网络)而脆弱易碎;
  • 想测试代码对故障(如文件校验和错误)的处理,却很难人为制造故障;
  • 需要确认模块与其他模块的交互方式是否正确,但交互难以观察,只能事后观察副作用,十分笨拙;
  • 想"mock 掉"依赖,但它们还没有 mock 实现,而你又不信任那些手写的 mock。

因此官方建议把 gMock 用作两用工具:设计工具(尽早、频繁地试验接口设计,更多迭代带来更好设计)与测试工具(削减测试的外部依赖、探测模块与协作者之间的交互)。

快速起步:gMock 随 googletest 一起分发

gMock 与 googletest 捆绑发布。在 gperftools 仓库中,二者的集成体现在构建系统层面:根目录 CMakeLists.txt 将 vendor 下的 googletest 源码编译为静态库gtest(包含gtest_main.cc、gtest.cc等源文件),并对外暴露vendor/googletest/googletest/include头文件路径;仓库内的大量单测(如src/tests/addressmap_unittest.cc、src/tests/page_heap_test.cc等)均以target_link_libraries(..., gtest)的方式链接该测试框架。

在实际工程中使用时,只需在你的构建中引入 googletest 源码,并在测试代码中#include <gmock/gmock.h>即可开始。

实战案例:为"海龟绘图"接口编写 Mock

假设你在开发一个依赖类 LOGO 语言 API 的绘图程序,如何测试它"画对了"?直接运行并与黄金截图比对是昂贵且脆弱的(换一块抗锯齿更好的显卡,就得更新全部黄金图片)。正确做法是运用依赖注入:不让应用程序直接调用系统 API,而是把 API 包装进一个接口(如Turtle),并面向该接口编程:

class Turtle { ... virtual ~Turtle() {} virtual void PenUp() = 0; virtual void PenDown() = 0; virtual void Forward(int distance) = 0; virtual void Turn(int degrees) = 0; virtual void GoTo(int x, int y) = 0; virtual int GetX() const = 0; virtual int GetY() const = 0; };

注意Turtle的析构函数必须是虚函数——对所有打算被继承的类都是如此,否则通过基类指针 delete 派生对象时不会调用派生类析构函数,导致内存泄漏等程序状态损坏。

PenUp()/PenDown()控制移动是否留下痕迹,Forward()、Turn()、GoTo()控制移动,GetX()/GetY()返回当前位置。程序在正常路径使用真实实现,测试时则换成 mock 实现,从而轻松检查程序调用了哪些绘图原语、以什么参数、按什么顺序调用。这样的测试更健壮(不会因新机器抗锯齿方式不同而失败)、更易读易维护(测试意图写在代码里而非二进制图片中),并且快得多。

如何定义 Mock 类:MOCK_METHOD 宏

如果幸运,你要用的 mock 可能已有人实现;否则按以下步骤即可把写 mock 变成一场"游戏":

  • 从Turtle派生一个MockTurtle类;
  • 挑一个Turtle的virtual函数(用模板 mock 非虚方法也是可行的,详见 Cook Book,但要麻烦得多);
  • 在子类的public:区段写下MOCK_METHOD();宏;
  • 把函数签名剪切粘贴进宏中,加上两个逗号——一个在返回类型与函数名之间,另一个在函数名与参数列表之间;
  • 若 mock 的是 const 方法,加第 4 个参数(const)(括号必须保留);
  • 由于是在覆写虚方法,建议加override关键字:const 方法的第 4 个参数写作(const, override),非 const 方法写作(override)(非强制);
  • 重复直到所有想 mock 的虚函数完成(抽象类中的所有纯虚方法必须被 mock 或覆写)。

完成后大致如下:

#include <gmock/gmock.h> // 引入 gMock class MockTurtle : public Turtle { public: ... MOCK_METHOD(void, PenUp, (), (override)); MOCK_METHOD(void, PenDown, (), (override)); MOCK_METHOD(void, Forward, (int distance), (override)); MOCK_METHOD(void, Turn, (int degrees), (override)); MOCK_METHOD(void, GoTo, (int x, int y), (override)); MOCK_METHOD(int, GetX, (), (const, override)); MOCK_METHOD(int, GetY, (), (const, override)); };

无需在其他地方定义这些 mock 方法——MOCK_METHOD宏会替你生成全部定义。在仓库源码中,该宏的定义位于 gmock-function-mocker.h,它基于返回类型、方法名与参数列表展开出一整套 mock 桩代码;gmock.h头文件顶部注释也给出了ON_CALL/EXPECT_CALL的完整语法骨架(见 gmock.h)。

Mock 类放哪里:所有权决定放置策略

定义 mock 类时需决定放置位置。一些人放在_test.cc里,当被 mock 的接口(如Foo)由同一个人或团队维护时没问题;否则Foo一旦变更,你的测试就可能编译失败——你无法指望Foo的维护者去修复每个使用Foo的测试。

一般原则:不要 mock 你不拥有的类。若必须 mock 他人所有的类,则把 mock 类定义在Foo的 Bazel 包中(通常是同目录或testing子目录),放在.h头文件里并构建为testonly=True的cc_library,供所有人从测试中引用。这样Foo变化时只需改一份MockFoo,且只需修复依赖变更方法的测试。

另一种做法是引入薄适配层FooAdaptor,面向新接口编程。因为FooAdaptor归你所有,吸收Foo的变化更容易;虽然初期工作量更大,但精心挑选的适配器接口可以让代码更好写、更可读,长期来看是净收益。

在测试中使用 Mock:五步工作流

拿到 mock 类后,典型工作流如下:

  1. 从testing命名空间导入 gMock 名字,以便不加限定地使用(每个文件只需一次;命名空间是好习惯);
  2. 创建若干 mock 对象;
  3. 设定期望:方法将被调用多少次?带什么参数?应该做什么?
  4. 运行使用 mock 的代码;可选地配合 googletest 断言检查结果。若 mock 方法被调用次数超过预期或参数错误,会立即报错;
  5. mock 析构时,gMock 自动检查其上的全部期望是否已满足。

示例:

#include "path/to/mock-turtle.h" #include <gmock/gmock.h> #include <gtest/gtest.h> using ::testing::AtLeast; // #1 TEST(PainterTest, CanDrawSomething) { MockTurtle turtle; // #2 EXPECT_CALL(turtle, PenDown()) // #3 .Times(AtLeast(1)); Painter painter(&turtle); // #4 EXPECT_TRUE(painter.DrawCircle(0, 0, 10)); // #5 }

该测试验证PenDown()至少被调用一次;若painter没调用它,测试会失败并输出类似信息:

path/to/my_test.cc:119: Failure Actual function call count doesn't match this expectation: Actually: never called; Expected: called at least once. Stack trace: ...

Tip 1:若在 Emacs 缓冲区内运行测试,可在失败行号上按<Enter>直接跳转到失败的期望处。

Tip 2:如果 mock 对象从未被删除,最终的期望校验就不会发生。因此当你在堆上分配 mock 时,建议开启堆检查器(若使用gtest_main库则自动获得该能力)。

期望必须在调用之前设定

重要规则:gMock 要求期望先于mock 函数被调用之前设定,否则行为未定义。不要把EXPECT_CALL()与对 mock 函数的调用交替进行,也不要在把 mock 传给某个 API 之后再对其设定期望。

也就是说,EXPECT_CALL()应被解读为"期待未来会发生一次调用",而非"一次调用已经发生"。这样设计的原因:提前设定期望能让 gMock 在违规刚发生时(上下文、堆栈等仍然可用)立即上报,极大方便调试。

设定期望:让交互规格精确到"刚刚好"

成功使用 mock 的关键是设定恰到好处的期望:太严则测试会因无关变更而失败,太松则让 bug 溜走。gMock 提供了让你做到"刚刚好"的全部手段。

一般语法

EXPECT_CALL()宏用于在 mock 方法上设定期望,通用语法为:

EXPECT_CALL(mock_object, method(matchers)) .Times(cardinality) .WillOnce(action) .WillRepeatedly(action);

宏有两个参数:先是 mock 对象,然后是方法及其参数。注意二者之间用**逗号(,)**而非句点(.)分隔(这是出于技术原因的必要设计)。若方法未被重载,宏也可以不带 matcher 调用:

EXPECT_CALL(mock_object, non-overloaded-method) .Times(cardinality) .WillOnce(action) .WillRepeatedly(action);

这种形式允许测试作者直接表达"以任意参数调用"而不必写出参数个数或类型;为避免歧义,它仅可用于非重载方法。两种形式后都可以接若干可选clauses(子句),下文逐一介绍。

该语法刻意让期望读起来像英文。例如:

using ::testing::Return; ... EXPECT_CALL(turtle, GetX()) .Times(5) .WillOnce(Return(100)) .WillOnce(Return(150)) .WillRepeatedly(Return(200));

意思是turtle.GetX()将被调用 5 次:第 1 次返回 100,第 2 次返回 150,之后每次都返回 200。有人把这种风格称为领域特定语言(DSL)。

为什么用宏?一是让期望易于识别(无论是用grep还是人眼),二是让 gMock 能在失败信息中包含期望所在的源文件位置,方便调试。这正是EXPECT_CALL宏被设计出来的双重目的,在 gmock-spec-builders.h 等实现文件中可以看到它围绕"期望(expectation)"与"匹配(matching)"的完整状态机。

Matcher:我们期待什么参数?

当 mock 函数带参数时,可以指定期望的参数值:

// 期待乌龟向前移动 100 个单位。 EXPECT_CALL(turtle, Forward(100));

但经常你不想太具体——过度指定会导致测试脆弱并掩盖意图。官方建议只指定必要的内容。如果不在意某个参数的值,写_表示"任意值":

using ::testing::_; ... // 期待乌龟跳到 x=50 这条线上的某处。 EXPECT_CALL(turtle, GoTo(50, _));

_就是所谓的matcher(匹配器)的一个实例。matcher 类似谓词,能测试参数是否符合期望;_是"任意值"的便捷写法。上例中的100、50其实也是 matcher——隐式等价于Eq(100)、Eq(50),即参数必须(用operator==)等于给定值。gMock 为常见类型内置了大量 matcher,也支持自定义 matcher;例如:

using ::testing::Ge; ... // 期待乌龟至少向前移动 100。 EXPECT_CALL(turtle, Forward(Ge(100)));

如果你对所有参数都不在意,与其逐个写_,不如直接省略参数列表:

// 期待乌龟向前移动。 EXPECT_CALL(turtle, Forward); // 期待乌龟跳到某处。 EXPECT_CALL(turtle, GoTo);

这适用于所有非重载方法;若方法被重载,则需要通过指定参数个数乃至参数类型来帮助 gMock 解析期望的是哪个重载。

Cardinality:将被调用多少次?

Times()是EXPECT_CALL()后可以指定的第一个子句,其参数称为cardinality(基数),表示调用应发生的次数。它允许你重复一条期望而无需写很多遍;更重要的是,cardinality 可以像 matcher 一样是"模糊的",从而精确表达测试意图。

一个有趣的特殊情况是Times(0):表示该函数根本不应被以给定参数调用,一旦被(错误地)调用,gMock 就会上报 googletest 失败。上文已见过AtLeast(n)这种模糊基数;内置基数的完整清单见 gmock_cheat_sheet.md。在源码层面,基数类型定义于 gmock-cardinalities.h。

Times()子句可以省略。省略时 gMock 会自行推断基数,规则很好记:

  • 若EXPECT_CALL()中既无WillOnce()也无WillRepeatedly(),推断的基数为Times(1);
  • 若有n个WillOnce()但没有WillRepeatedly()(n≥ 1),基数为Times(n);
  • 若有n个WillOnce()且有一个WillRepeatedly()(n≥ 0),基数为Times(AtLeast(n))。

小测验:如果一个函数被期望调用两次、实际却被调用四次,会发生什么?

Action:它应该做什么?

mock 对象并没有真正的工作实现,需要用户告诉它在方法被调用时做什么。

首先,若 mock 函数的返回类型是内建类型或指针,它自带默认动作(void函数直接返回,bool函数返回false,其他函数返回 0);在 C++11 及以上,返回类型可默认构造的 mock 函数默认返回默认构造的值。如果你什么都不说,就用这个行为。

其次,若函数没有默认动作、或默认动作不合需求,可以用一串WillOnce()子句加一个可选的WillRepeatedly()来指定每次期望匹配时要执行的动作。例如:

using ::testing::Return; ... EXPECT_CALL(turtle, GetX()) .WillOnce(Return(100)) .WillOnce(Return(200)) .WillOnce(Return(300));

表示turtle.GetX()将被调用恰好三次(因未显式写Times(),gMock 从WillOnce()的数量推断),依次返回 100、200、300。

using ::testing::Return; ... EXPECT_CALL(turtle, GetY()) .WillOnce(Return(100)) .WillOnce(Return(200)) .WillRepeatedly(Return(300));

表示turtle.GetY()将被调用至少两次,前两次分别返回 100、200,从第三次起返回 300。

当然,若显式写了Times(),gMock 不会自行推断。若指定的次数多于WillOnce()子句的个数,则所有WillOnce()用尽之后,gMock 每次都执行默认动作(除非还有WillRepeatedly())。

除了Return(),WillOnce()里还能做什么?可以用ReturnRef(variable)返回引用,或调用预定义函数等,详见 gmock_cook_book.md。

重要注意:EXPECT_CALL()语句只对 action 子句求值一次,即使该 action 会被执行多次。因此必须当心副作用——下面的代码很可能不符合预期:

using ::testing::Return; ... int n = 100; EXPECT_CALL(turtle, GetX()) .Times(4) .WillRepeatedly(Return(n++));

它不会依次返回 100、101、102……而是每次都返回 100,因为n++只被求值了一次。同理,Return(new Foo)会在EXPECT_CALL()执行时创建一次Foo对象,之后每次都返回同一个指针。若希望副作用每次发生,需要定义自定义 action(见 Cook Book)。

再来一次测验,猜猜下面代码的含义:

using ::testing::Return; ... EXPECT_CALL(turtle, GetY()) .Times(4) .WillOnce(Return(100));

显然turtle.GetY()被期望调用四次。但如果你以为每次都返回 100,那就错了!每调用一次函数就会消耗一个WillOnce()子句,之后执行默认动作。正确答案是:第一次返回 100,从第二次起返回 0——返回 0 正是int函数的默认动作。

多条期望并存:反向查找与"新规则覆盖旧规则"

实际测试中通常会在多个 mock 方法(可能来自多个 mock 对象)上设定多条期望。默认情况下,当 mock 方法被调用时,gMock 会按定义的反向顺序搜索期望,在找到一条与参数匹配的活跃期望时停止(可理解为"新规则覆盖旧规则")。如果匹配的期望已不能再接受调用,就会得到上界违规(upper-bound-violated)失败。示例:

using ::testing::_; ... EXPECT_CALL(turtle, Forward(_)); // #1 EXPECT_CALL(turtle, Forward(10)) // #2 .Times(2);

若Forward(10)连续被调用三次,第三次就会报错,因为最后匹配的期望 #2 已饱和;但如果第三次调用的是Forward(20),则没问题——此时 #1 成为匹配期望。

为什么反向搜索?这允许用户在 mock 对象的构造函数或测试夹具的 setup 阶段设置默认期望,然后在测试体中写更具体的期望来定制 mock。因此,如果同一方法有两条期望,应把 matcher 更具体的那条放在后面,否则更具体的规则会被后面更宽泛的规则遮蔽。

Tip:常见做法是先为某个方法写一条兜底期望,配Times(AnyNumber())(省略参数;若重载则对全部参数用_)。这让对该方法的任何调用都被视为预期。对于完全没有提及的方法("无趣"调用)不是必需的,但对于"既有部分期望、其他调用也无所谓"的方法非常有用。更深入的概念见 Understanding Uninteresting vs Unexpected Calls。

有序与无序调用:InSequence 控制严格顺序

默认情况下,即使前面的期望尚未满足,一条期望也可以匹配调用——即调用不必按期望指定的顺序发生。若希望所有期望调用严格按顺序发生,gMock 提供InSequence:

using ::testing::InSequence; ... TEST(FooTest, DrawsLineSegment) { ... { InSequence seq; EXPECT_CALL(turtle, PenDown()); EXPECT_CALL(turtle, Forward(100)); EXPECT_CALL(turtle, PenUp()); } Foo(); }

创建一个InSequence类型对象后,其作用域内的所有期望都被放入一条sequence,必须依次发生。由于实际工作由该对象的构造与析构完成,它的名字无关紧要。本例验证Foo()按书写顺序调用这三个函数,乱序即报错。

如果你只关心部分调用的相对顺序(任意偏序),答案也是肯定的——细节见 gmock_cook_book.md。

期望默认是"粘性"的:RetiresOnSaturation

来个小测验:如何测试乌龟被要求回到原点恰好两次(忽略它收到的其他指令)?参考答案:

using ::testing::_; using ::testing::AnyNumber; ... EXPECT_CALL(turtle, GoTo(_, _)) // #1 .Times(AnyNumber()); EXPECT_CALL(turtle, GoTo(0, 0)) // #2 .Times(2);

假设turtle.GoTo(0, 0)被调用三次:第三次时 gMock 发现参数匹配期望 #2(记住总是选择最后匹配的期望),而该期望只允许两次,于是立即报错。这就是上节"多条期望"规则的直接应用。

这个例子揭示了重要规则:gMock 中的期望默认是"粘性"(sticky)的——即使已经达到调用上界,它们仍然保持活跃。这与许多其他 mock 框架的行为不同(之所以这样设计,是因为 gMock 认为该规则让常见情况更容易表达和理解)。

再看你是否真正理解了:下面代码表达什么?

using ::testing::Return; ... for (int i = n; i > 0; i--) { EXPECT_CALL(turtle, GetX()) .WillOnce(Return(10*i)); }

如果你以为turtle.GetX()将被调用n次并依次返回 10、20、30……那就错了!因为期望是粘性的,第二次调用turtle.GetX()时,最后(最新)那条EXPECT_CALL()就会匹配并立刻触发"上界违规"错误——这段代码几乎没用。

正确的做法是显式声明期望不粘,即饱和后立即retire(退役):

using ::testing::Return; ... for (int i = n; i > 0; i--) { EXPECT_CALL(turtle, GetX()) .WillOnce(Return(10*i)) .RetiresOnSaturation(); }

还有更好的做法:既然这里期望按特定顺序发生、action 也按顺序排列,应该用 sequence 显式表达顺序:

using ::testing::InSequence; using ::testing::Return; ... { InSequence s; for (int i = 1; i <= n; i++) { EXPECT_CALL(turtle, GetX()) .WillOnce(Return(10*i)) .RetiresOnSaturation(); } }

顺便一提,期望可能"不粘"的另一种情形是:它处于某个 sequence 中——一旦 sequence 中排在其后的期望被使用,它便自动退役,从此不再匹配任何调用。

无趣调用(Uninteresting Calls):不想管的就别管

mock 对象可能有很多方法,并非都值得关心——例如某些测试中并不在意GetX()/GetY()被调用了多少次。在 gMock 中,如果对某个方法不感兴趣,什么都不说即可。若该方法被调用,测试输出中会出现一条警告,但不会导致失败。这被称为 "naggy"(唠叨)行为;如需改变,可参考 The Nice, the Strict, and the Naggy。在源码中,NiceMock、StrictMock、NaggyMock等混合类(mixin)定义于 gmock-nice-strict.h,分别对应"无趣调用不警告 / 无趣调用即失败 / 无趣调用发警告"三种默认策略,测试时可据此灵活切换。

小结:gMock 心法速记

  • mock 对象 = 同一接口 + 运行时指定的调用期望,用来验证交互而非实现;
  • 用MOCK_METHOD一行声明一个 mock 方法,宏自动生成实现;只 mock 你拥有的接口,或通过适配层隔离变化;
  • EXPECT_CALL必须在调用发生前设定;matcher 控制参数(_表示任意)、Times()控制次数(可省略并由 gMock 推断)、WillOnce()/WillRepeatedly()控制返回值与副作用;
  • 多条期望反向匹配、"新规则覆盖旧规则",因此更具体的期望要写在后面;
  • 期望默认粘性,需要时用RetiresOnSaturation()退役;InSequence强制严格调用顺序;
  • 不关心的调用保持沉默即可(naggy 行为),或用 NiceMock / StrictMock 调整策略。

掌握以上要点后,你就能像 gperftools 的测试套件(见 src/tests 下大量基于 gtest 的单测)一样,用 googletest + gMock 这套组合写出既快又稳、交互意图清晰可读的 C++ 单元测试。

  • 性能剖析
  • 内存管理
  • 开发工具

【免费下载链接】gperftools

Main gperftools repository

项目地址:https://gitcode.com/gh_mirrors/gp/gperftools
点击查看免费下载

相关推荐

上一篇:终极指南:Inpaint-Anything如何通过AI图像修复技术革新医疗影像分析
下一篇:C++ HTTP请求终极指南:5种HttpVersion协议版本控制策略详解

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 2:54:37

ESP32上跑WebAssembly:从固件原理到运行时实践的完整解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 2:53:29

CodeGuide 本地任务消息组件:基于门牌号分片扫描的动态任务补偿处理,兜住 HTTP/MQ 通知的最终一致性

文档教程后端 【免费下载链接】CodeGuide :books: 本代码库是作者小傅哥多年从事一线互联网 Java 开发的学习历程技术汇总&#xff0c;旨在为大家提供一个清晰详细的学习教程&#xff0c;侧重点更倾向编写Java核心内容。如果本仓库能为您提供帮助&#xff0c;请给予支持(关注、…

作者头像 李华
网站建设 2026/9/25 2:50:47

百托帮GEO服务在全国市场的表现如何

顺应流量迁徙趋势&#xff0c;锚定行业发展使命随着数字经济的深度渗透&#xff0c;线上获客已经成为企业经营发展的核心命题。从早期的搜索引擎营销&#xff0c;到短视频时代的内容种草&#xff0c;再到当下AI搜索的异军突起&#xff0c;用户获取信息与商业服务的路径正在发生…

作者头像 李华