为什么我要杀掉 syn:Rust 编译速度之战与 unsynn 的诞生

本文是对 The virtue of unsynn的整理与翻译


内容结构概览

markdown 复制代码
一、传言与真相:我真的想杀掉 serde 吗
    - facet 项目引发的 Reddit 讨论
    - 澄清:不是 serde,而是 syn
    - "free of syn" 运动的由来

二、syn 有多慢:用数字说话
    - M4 Pro 上的冷构建基准测试(facet vs syn@1 vs syn@2)
    - 单线程 -j1 模拟 CI 环境:3x vs 12x,差距扩大到约 4 倍
    - GitHub Actions 免费层实测数据

三、先搞清楚我们在比较什么:苹果 vs 核潜艇
    - syn 是什么:解析完整 Rust AST 的库
    - syn 能做什么:用一段代码解析自己
    - Rust 的声明式宏(macro_rules!)能做什么,边界在哪里

四、声明式宏 vs proc macro:从代码到构建时间
    - 用 print_fn_name 做对比示例
    - 声明式宏的局限性(pub fn 就能让它崩溃)
    - proc macro + syn 版本的实现
    - 宏展开结果对比:两者完全相同
    - 用 -Zunpretty=expanded 验证展开结果

五、构建时间差距有多大(微项目层面)
    - 冷构建:decl 111ms vs syn 1524ms,差 13.71 倍
    - 开启 opt-level=3 后:差距扩大到 36.76 倍
    - 两种常见反驳:proc macro 本来就慢 / 谁关心冷构建

六、不用 syn 也能写 proc macro:纯手工版
    - 直接操作 TokenStream 的完整实现代码
    - 性能:manual 205ms,仍比 syn 快 6.8 倍
    - 代价:人体工程学很差

七、unsynn:介于手工和 syn 之间的中间地带
    - unsynn 是什么(名字来自德语 unsinn,意为胡说八道)
    - keyword! 宏的用法与展开详情
    - unsynn! 宏中的核心组合子:Many / Cons / Except
    - 完整的 proc macro 实现代码
    - ToTokens 实现为什么需要手写
    - 为什么还需要 quote 和 proc_macro2(可测试性问题)

八、unsynn 的构建时间
    - 冷构建四路对比:decl 166ms / manual 267ms / unsynn 718ms / syn 1574ms
    - unsynn 比 syn 快 2.2 倍,比声明式宏慢 4.3 倍

九、热构建才是真正的战场
    - 用一万一千行 AI 生成的"垃圾代码"填满函数体
    - 热构建测试:decl≈197ms / manual≈204ms / unsynn≈208ms / syn≈304ms
    - syn 的真正问题:每次编译都要重新解析完整 AST,而 proc macro 调用结果不会被缓存
    - 100ms 被 syn 额外消耗,每次 cargo 认为有变化就要重付

十、宏观视角:syn 在真实项目里的影响
    - beardist 工具中 syn 出现 8 次的依赖树
    - 用因果剖析(coz)技术分析 syn 对关键路径的影响
    - jiff 不在关键路径上,使它更快没有意义
    - 魔法加速 syn 可以减少约 10% 的构建时间
    - 指向 facet 项目的背景

十一、小结

一、传言与真相

Reddit 上关于 facet(作者正在开发的 Rust 反射框架)的讨论线程里,开始流传一个传言:fasterthanlime 想要干掉 serde------Rust 社区最受欢迎的序列化/反序列化框架,为 Rust 的成功做出了巨大贡献。

作者的回应是:

那是绝对的、百分之百的真实。

然后他马上说:

开玩笑的。大部分是。

因为你没法真的"杀掉"一个 Rust crate。如果你扎它一下,它不会流血------作者对此深有体会,因为他正在积极尝试干掉的其实是另一个 crate:syn ,并为此发起了 free of syn 运动。


二、syn 到底有多慢

作者先拿出一组数据,但故意不解释在对比什么:

M4 Pro,并行构建(默认):

sql 复制代码
Benchmark 1: facet@0.x
  Time (mean ± σ):      1.241 s ±  0.003 s
  Range (min ... max):    1.236 s ...  1.244 s    10 runs

Benchmark 2: syn@2
  Time (mean ± σ):      2.679 s ±  0.035 s
  Range (min ... max):    2.643 s ...  2.742 s    10 runs

Benchmark 3: syn@1
  Time (mean ± σ):      2.885 s ±  0.077 s
  Range (min ... max):    2.780 s ...  3.001 s    10 runs

Summary
  facet@0.x ran
    2.16 ± 0.03 times faster than syn@2
    2.32 ± 0.06 times faster than syn@1

在 M4 Pro 这样的高端机器上,最慢的跑完不到三秒,看起来还好。

但如果用 -j1(单线程,模拟 CI 环境):

sql 复制代码
Benchmark 1: facet@0.x
  Time (mean ± σ):      3.225 s ±  0.035 s

Benchmark 2: syn@2
  Time (mean ± σ):     12.200 s ±  0.106 s

Benchmark 3: syn@1
  Time (mean ± σ):     12.162 s ±  0.028 s

Summary
  facet@0.x ran
    3.77 ± 0.04 times faster than syn@1
    3.78 ± 0.05 times faster than syn@2

12 秒。这段时间里,作者说他有时间泡好一杯红茶,思考人生。

GitHub Actions 免费层实测(2025 年 4 月 17 日):

sql 复制代码
Benchmark 1: facet@0.x
  Time (mean ± σ):      2.768 s ±  0.030 s

Benchmark 2: syn@2
  Time (mean ± σ):      9.352 s ±  0.039 s

Benchmark 3: syn@1
  Time (mean ± σ):      9.302 s ±  0.031 s

Summary
  facet@0.x ran
    3.36 ± 0.04 times faster than syn@1
    3.38 ± 0.04 times faster than syn@2

但等等,facetsyn 根本不是同一类东西,对比它们的构建时间意义是什么?


三、苹果与核潜艇

作者承认:facetsyn 不等价。就像在对比苹果和核潜艇。

syn 到底是什么?

syn 是一个 Rust 代码解析库,它可以把 Rust 源代码解析成完整的抽象语法树(AST)。

下面这段代码可以解析它自己:

rust 复制代码
fn main() {
    eprintln!("{:?}", syn::parse_file(include_str!("main.rs")).unwrap());
}

运行后输出的是整个文件的完整 AST 表示,包含每一个 token 的位置信息:

less 复制代码
File { shebang: None, attrs: [], items: [Item::Fn { attrs: [], vis: Visibility::Inherited,
sig: Signature { constness: None, asyncness: None, unsafety: None, ...

这在编写 proc macro(过程宏)时非常有用,因为你可以完整地访问和操作 Rust 代码的所有语法结构。


四、声明式宏能做什么,边界在哪里

Rust 有两种宏:声明式宏macro_rules!)和过程宏(proc macro)。

用声明式宏写一个"打印函数名"的宏:

rust 复制代码
macro_rules! print_fn_name {
    (fn $name:ident ($($args:tt),*) { $($body:tt)* }) => {
        fn $name($($args),*) {
            println!("Function name: {}", stringify!($name));
            $($body)*
        }
    };
}

print_fn_name! {
    fn main() {
        println!("Hello, world!")
    }
}

输出:

javascript 复制代码
Function name: main
Hello, world!

它在编译期操作 token,而不是像 C 的预处理器那样在文本层面工作。括号分组的概念是内置的,不用担心括号的内容。

但声明式宏的局限性马上出现------只要加一个 pub

rust 复制代码
print_fn_name! {
    pub fn main() {        // 这行有问题
        println!("Hello, world!")
    }
}
rust 复制代码
error: no rules expected keyword `pub`
  --> src/main.rs:11:5
note: while trying to match keyword `fn`

可以修复,在模式里加 $vis:vis

rust 复制代码
macro_rules! print_fn_name {
    ($vis:vis fn $name:ident ($($args:tt),*) { $($body:tt)* }) => {
        $vis fn $name($($args),*) {
            println!("Function name: {}", stringify!($name));
            $($body)*
        }
    };
}

但你会不断遇到下一个语法细节,不断修补。

用 syn 写 proc macro,就没有这个问题,它帮你解析完所有的 Rust 语法。


五、用 syn 写 proc macro

创建一个 proc macro crate:

bash 复制代码
cargo new --lib ouroboros-proc-macro

Cargo.toml 中添加:

toml 复制代码
[lib]
proc-macro = true

加入依赖 syn@2 -F fullquote,然后实现宏:

rust 复制代码
use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro_attribute]
pub fn print_fn_name(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input: syn::ItemFn = parse_macro_input!(item);
    let fn_vis = &input.vis;
    let fn_sig = &input.sig;
    let fn_name = &input.sig.ident;
    let fn_block = &input.block;

    let expanded = quote! {
        #fn_vis #fn_sig {
            println!("Function name: {}", stringify!(#fn_name));
            #fn_block
        }
    };
    TokenStream::from(expanded)
}

调用侧变得简洁:

rust 复制代码
use ouroboros_proc_macro::print_fn_name;

#[print_fn_name]
pub fn main() {
    println!("Hello, world!")
}

宏展开结果验证

-Zunpretty=expanded 可以查看两种宏的展开结果,它们完全相同

proc macro 版展开:

rust 复制代码
pub fn main() {
    {
        ::std::io::_print(format_args!("Function name: {0}\n", "main"));
    };
    {
        {
            ::std::io::_print(format_args!("Hello, world!\n"));
        }
    }
}

声明式宏版展开:

rust 复制代码
macro_rules! print_fn_name { /* ... */ }
pub fn main() {
    {
        ::std::io::_print(format_args!("Function name: {0}\n", "main"));
    };
    {
        ::std::io::_print(format_args!("Hello, world!\n"));
    }
}

唯一的区别是声明式宏版本还保留了宏的定义本身。

两者的本质区别不是功能,而是人体工程学syn 帮你解析了整个 Rust AST,你不用操心任何语法细节。


六、构建时间差距:冷构建(微项目)

先做基准测试,比较四种方案的冷构建时间:

less 复制代码
Benchmark 1: decl(声明式宏)
  Time (mean ± σ):     111.1 ms ±   2.1 ms

Benchmark 2: syn(proc macro + syn)
  Time (mean ± σ):      1.524 s ±  0.030 s

Summary
  decl ran
   13.71 ± 0.37 times faster than syn

声明式宏比 syn proc macro 快 13.71 倍

如果给 proc macro 开启优化(在 .config/cargo.toml 加):

toml 复制代码
[profile.dev.build-override]
opt-level = 3

结果是......更糟:

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     112.6 ms ±   2.1 ms

Benchmark 2: syn
  Time (mean ± σ):      4.140 s ±  0.070 s

Summary
  decl ran
   36.76 ± 0.93 times faster than syn

差距扩大到 36.76 倍 。优化的是 syn 本身的代码,让 syn 跑得更快了,但编译 syn 本身花的时间也更长了。


七、两种反驳与作者的回应

面对这些数据,文章预测了两种常见反驳:

反驳一:proc macro 本来就贵,这不是 syn 的问题。

作者的回应:不对,可以不用 syn 也能写 proc macro。见下节。

反驳二:谁关心冷构建?大家都做了缓存,热构建才是日常。

作者的回应:热构建同样有问题。见后续章节。


八、不用 syn 也能写 proc macro:纯手工版

直接操作 proc_macro 的原始 API:

rust 复制代码
// in `ouroboros-manual-macro/src/lib.rs`

use proc_macro::{Delimiter, Group, Ident, Literal, Punct, Spacing, Span, TokenStream, TokenTree};

#[proc_macro_attribute]
pub fn print_fn_name(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let mut tokens = item.into_iter();
    let mut output = Vec::new();

    // 1. 把 token 传过去,直到遇到 "fn"
    for token in &mut tokens {
        let is_fn = matches!(&token, TokenTree::Ident(ident) if ident.to_string() == "fn");
        output.push(token.clone());
        if is_fn {
            break;
        }
    }

    // 2. 下一个必须是函数名标识符
    let fn_name_ident = match tokens.next() {
        Some(TokenTree::Ident(ident)) => ident,
        _ => panic!("Expected function name after fn"),
    };
    let fn_name_str = fn_name_ident.to_string();
    output.push(TokenTree::Ident(fn_name_ident.clone()));

    // 3. 继续传,直到遇到函数体 { ... }
    for token in tokens {
        if let TokenTree::Group(group) = &token {
            if group.delimiter() == Delimiter::Brace {
                output.push(TokenTree::Group(Group::new(
                    Delimiter::Brace,
                    TokenStream::from_iter(
                        [
                            TokenTree::Ident(Ident::new("println", Span::call_site())),
                            TokenTree::Punct(Punct::new('!', Spacing::Alone)),
                            TokenTree::Group(Group::new(
                                Delimiter::Parenthesis,
                                TokenStream::from_iter([TokenTree::Literal(Literal::string(
                                    &format!("Function name: {fn_name_str}"),
                                ))]),
                            )),
                            TokenTree::Punct(Punct::new(';', Spacing::Alone)),
                        ]
                        .into_iter()
                        .chain(group.stream()),
                    ),
                )));
                continue;
            }
        }
        output.push(token);
    }

    output.into_iter().collect()
}

很繁琐,但能用,而且快:

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     108.5 ms ±   1.8 ms

Benchmark 2: manual(手工版 proc macro)
  Time (mean ± σ):     205.8 ms ±   1.3 ms

Benchmark 3: syn
  Time (mean ± σ):      1.500 s ±  0.035 s

Summary
  decl ran
    1.90 ± 0.03 times faster than manual
   13.82 ± 0.40 times faster than syn

手工版比声明式宏慢 1.9 倍,但比 syn 快 7.3 倍

问题是人体工程学太差了。如果只有这两个极端(声明式宏 vs 直接操作 TokenStream),选择会很难。

这就是 unsynn 出现的原因。


九、unsynn:介于手工和 syn 之间

unsynn 这个名字来自德语 "unsinn",意为"胡说八道/无稽之谈"。

它的思路很清晰:只解析你需要的部分,不解析你不需要的部分

第一步:定义关键字

rust 复制代码
use unsynn::*;

keyword! {
    KFn = "fn";
}

keyword! 是一个声明式宏,展开后生成:

rust 复制代码
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
struct KFn;

impl unsynn::Parser for KFn {
    fn parser(tokens: &mut unsynn::TokenIter) -> Result<Self> {
        use unsynn::Parse;
        unsynn::CachedIdent::parse_with(tokens, |ident, tokens| {
            if ident == "fn" {
                Ok(KFn)
            } else {
                unsynn::Error::other::<KFn>(
                    tokens,
                    format!(
                        "keyword {:?} expected, got {:?} at {:?}",
                        "fn",
                        ident.as_str(),
                        ident.span().start()
                    ),
                )
            }
        })
    }
}

impl unsynn::ToTokens for KFn {
    fn to_tokens(&self, tokens: &mut TokenStream) {
        unsynn::Ident::new("fn", unsynn::Span::call_site()).to_tokens(tokens);
    }
}

impl AsRef<str> for KFn {
    fn as_ref(&self) -> &str {
        &"fn"
    }
}

第二步:声明要解析的结构

rust 复制代码
unsynn! {
    struct UntilFn {
        items: Many<Cons<Except<KFn>, TokenTree>>,
    }

    struct UntilBody {
        items: Many<Cons<Except<BraceGroup>, TokenTree>>,
    }

    struct Body {
        items: BraceGroup,
    }

    struct FunctionDecl {
        until_fn: UntilFn, _fn: KFn, name: Ident,
        until_body: UntilBody, body: Body
    }
}

核心组合子说明:

  • Many:一个或多个,类似正则表达式中的 +
  • Cons:这个,然后那个------两件事依次出现
  • Except:向前看,确保某个东西匹配。不消耗 token,只做检查
  • KFn:刚定义的 fn 关键字------不是字符串字面量,是真正的关键字 token
  • TokenTree:不只是单个 token,而是一整棵树。任何括号括起来的表达式都是一个 TokenTree

关键设计思路:我们不解析不需要解析的东西 。我们只是跳过 token,直到看到 fn 关键字,然后拿到标识符,跳过直到函数体,拿到函数体。

unsynn! 宏里:

  • struct = "一系列东西的序列"(类似带命名字段的 Cons<...>
  • enum = "替代选项"
  • Option<T> 也可以用

第三步:proc macro 入口

rust 复制代码
#[proc_macro_attribute]
pub fn print_fn_name(
    _attr: proc_macro::TokenStream,
    item: proc_macro::TokenStream,
) -> proc_macro::TokenStream {
    let item = TokenStream::from(item);
    let mut i = item.to_token_iter();
    let fdecl = i.parse::<FunctionDecl>().unwrap();

    let FunctionDecl {
        until_fn,
        _fn,
        name,
        until_body,
        body,
    } = fdecl;

    let fmt_string = format!("Function name: {}", name);
    quote::quote! {
        #until_fn fn #name #until_body {
            println!(#fmt_string);
            #body
        }
    }
    .into()
}

首先把输入 TokenStreamproc_macro 版本转换为 proc_macro2 版本(这是因为 proc_macro API 目前只对 proc macro crate 可用,如果想写单元测试需要抽象层),然后解析为 FunctionDecl

解构之后,用 quote! 把各个字段插值到生成的 token stream 中,同时保留 span 信息。

为了让 quote! 能插值,还需要为自定义类型实现 quote::ToTokens

rust 复制代码
impl quote::ToTokens for UntilFn {
    fn to_tokens(&self, tokens: &mut unsynn::TokenStream) {
        self.items.to_tokens(tokens)
    }
}

impl quote::ToTokens for UntilBody {
    fn to_tokens(&self, tokens: &mut unsynn::TokenStream) {
        self.items.to_tokens(tokens)
    }
}

impl quote::ToTokens for Body {
    fn to_tokens(&self, tokens: &mut unsynn::TokenStream) {
        tokens.extend(self.items.0.stream())
    }
}

基本上是把实现转发给已有的实现,因为 unsynn 自带 ToTokens trait,和 quote::ToTokens 本质上是同一个东西。

为什么还需要 quote 和 proc_macro2

unsynnquote 共享 proc_macro2 依赖,这是一种"必要之恶"。proc_macro API 目前还不能用于非 proc macro crate,所以如果想写单元测试,就需要某种抽象层。这个问题有 tracking issue 正在推进,但在文章写作时还有 PR 等待有人认领。


十、unsynn 的冷构建时间

四路对比:

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     166.2 ms ±   4.0 ms

Benchmark 2: manual
  Time (mean ± σ):     267.9 ms ±   3.0 ms

Benchmark 3: syn
  Time (mean ± σ):      1.574 s ±  0.004 s

Benchmark 4: unsynn
  Time (mean ± σ):     718.0 ms ±   1.9 ms

Summary
  decl ran
    1.61 ± 0.04 times faster than manual
    4.32 ± 0.10 times faster than unsynn
    9.47 ± 0.23 times faster than syn

unsynnsyn2.19 倍,比声明式宏慢 4.32 倍。

它比 syn 轻,也比 syn 做的事少------而后者本来就是重点。


十一、热构建才是真正的战场

很多人的反驳:"我不在乎冷构建,我有缓存,热构建才是日常。"

好,测热构建。

作者用 GPT-4.1 生成了大量嵌套的"垃圾代码",然后把同一个块复制 100 次,填满 main 函数体(约一万一千行代码)。

热构建的测量方式:touch main.rs,然后 cargo build。依赖本身不重新构建,但 proc macro 会重新处理整个函数体。

less 复制代码
Benchmark 1: decl
  Time (mean ± σ):     197.7 ms ±   3.6 ms

Benchmark 2: manual
  Time (mean ± σ):     204.0 ms ±   7.6 ms

Benchmark 3: syn
  Time (mean ± σ):     304.8 ms ±   3.1 ms

Benchmark 4: unsynn
  Time (mean ± σ):     208.6 ms ±   3.8 ms

Summary
  decl ran
    1.03 ± 0.04 times faster than manual
    1.05 ± 0.03 times faster than unsynn
    1.54 ± 0.03 times faster than syn

200ms 是解析编译这些代码的基本成本(假设声明式宏几乎免费)。调用一个已构建好的 proc macro 大约需要 10ms。syn 解析一万一千行"AI 垃圾代码"额外消耗了约 100ms

这才是作者不喜欢 syn 的真正原因

syn 不允许你"少做一点"。它总是解析完整的 Rust AST,即使你的需求远比这要简单。

而且,目前 proc macro 调用的结果不会被缓存 。只要 cargo 认为某个文件可能发生了变化,就会重新运行所有 proc macro。这意味着:任何一个使用了 syn 的 proc macro,每次 cargo 认为需要重新编译时,都要重新解析所有代码,生成所有代码,无论实际内容有没有变化。

这就是为什么我们需要 proc macro 尽可能少做工作,而不是把 syn 的完整 AST 解析带到每一次编译循环里。


十二、syn 在真实项目中的影响规模

微项目的测试说明了原理,但真实项目里 syn 的影响更大。

作者的内部工具 beardist 的依赖树里,syn 出现了 8 次

scss 复制代码
beardist on  main via Rust v1.86.0
❯ cargo tree -i syn --depth 1
syn v2.0.100
├── clap_derive v4.5.32 (proc-macro)
├── displaydoc v0.2.5 (proc-macro)
├── icu_provider_macros v1.5.0 (proc-macro)
├── serde_derive v1.0.219 (proc-macro)
├── synstructure v0.13.1
├── yoke-derive v0.7.5 (proc-macro)
├── zerofrom-derive v0.1.6 (proc-macro)
└── zerovec-derive v0.10.3 (proc-macro)

要把 syn 从这个依赖树里移除,意味着需要替换序列化框架、命令行解析、reqwest(通过 url crate 依赖 syn),工作量巨大。

用因果剖析(Causal Profiling)量化影响

直接测量 syn 的影响不容易,但可以用 coz 因果剖析器的技术来模拟:

  1. 先运行一次构建,其中每个 crate 的构建时间都人为加倍
  2. 再运行一次,其中除 syn 之外的每个 crate 构建时间加倍

两次构建之间的差异,就是"假如 syn 构建速度翻倍会带来多大改进"的虚拟加速。

这个技术来自因果剖析论文 COZ: Finding Code that Counts with Causal Profiling,核心思路是:通过人为减慢其他所有东西来模拟某个组件被加速的效果。

测试结果中几个有意思的发现:

  • jiff 虽然占了构建时间的相当一部分,但它不在关键路径上------让它更快对冷构建没有任何帮助,因为它会和其他事情并行完成
  • serde_derive 在这个特定项目里,即使魔法加速,也没有可测量的效果
  • syn 相关的 proc macro 依赖serde_deriveclap_derive 等)移到更早完成后,整体构建时间减少了约 10%

这是真实项目中 syn 对关键路径的量化影响。


十三、小结

这篇文章的完整论证链条:

ruby 复制代码
proc macro 使用 syn 解析 Rust 代码
  ↓
syn 解析完整 AST,即使你只需要其中很小的一部分
  ↓
proc macro 调用结果不被缓存
  ↓
每次 cargo 认为需要重新编译,syn 就要重新工作一遍
  ↓
在微项目上:冷构建慢 10--37 倍
  ↓
热构建:额外 100ms 每次
  ↓
在真实项目中:syn 在关键路径上,减少 10% 构建时间

替代方案的取舍对比:

方案 冷构建 热构建 人体工程学
声明式宏 最快(~111ms) 最快 受限,难以处理复杂语法
手工 proc macro 较快(~205ms) 很差,大量模板代码
unsynn 中等(~718ms) 较好,只解析需要的
syn 最慢(~1574ms) 最慢(+100ms) 最好,完整 AST 访问

作者的立场:

syn 是一个令人叹为观止的工具,它解析整个 Rust AST 的能力是真实的工程成就。但它不允许你"少做一点",而且 proc macro 缺乏缓存机制,让这个成本在每次编译中都要重新付出。

unsynn 是在"完整 AST 解析"和"直接操作原始 TokenStream"之间的中间地带:只描述你需要解析的结构,让其他部分透明地通过。

而 "free of syn" 运动,则是在推动整个 Rust 生态重新审视:当你只需要提取一个函数名、一个字段列表、一个枚举变体时,是否真的需要引入一个能解析整个 Rust 语言的完整解析器?


参考链接

相关推荐
huzhongqiang3 小时前
扩展 Python 事件机制:支持等待事件消失
后端·python
_Evan_Yao4 小时前
大学自学能力怎么练?慕课、B站、书籍资源清单
后端·学习
SimonKing4 小时前
从惊艳到踩坑:AI结对编程的真实复盘
java·后端·程序员
咖啡八杯4 小时前
GoF设计模式——原型模式
java·后端·设计模式·原型模式
IT_陈寒4 小时前
Python多线程居然不加速?这个坑我踩得明明白白
前端·人工智能·后端
ZC跨境爬虫4 小时前
模块化烹饪小程序开发日记 Day3:(Flask后端初始化、数据库配置与自定义日志系统搭建)
前端·javascript·数据库·后端·python·flask
青云计划5 小时前
渐进式发布
java·后端
多敲代码防脱发5 小时前
Spring进阶(Aware接口)
java·后端·spring