编程 Zig 0.14 深度实战:当系统编程遇见无隐藏控制流——从 Comptime 元编程到 C 无缝互操作的生产级完全指南(2026)

2026-06-17 05:28:14 +0800 CST views 10

Zig 0.14 深度实战:当系统编程遇见"无隐藏控制流"--从 Comptime 元编程到 C 无缝互操作的生产级完全指南(2026)

作者: 程序员茄子
日期: 2026-06-17
标签: Zig, 系统编程, Comptime, C互操作, 内存安全, 编译期元编程, 性能优化


目录

  1. 前言:C语言的"中年危机"与 Zig 的救赎之路
  2. Zig 设计哲学:为什么"无隐藏控制流"改变一切
  3. 核心特性深度剖析
  4. Zig 0.14 新特性实战
  5. 架构分析:Zig 编译器的自举之路
  6. 代码实战:从零构建生产级项目
  7. 性能对比:Zig vs C vs Rust
  8. 生产落地:大厂案例与最佳实践
  9. 未来展望:Zig 在 2026-2030 的技术路线图
  10. 总结:为什么 2026 是学习 Zig 的最佳时机

1. 前言:C语言的"中年危机"与 Zig 的救赎之路

1.1 C语言的辉煌与困境

作为系统编程的"老大哥",C语言已经统治了整整 50年(1972-2026).从 UNIX 内核到 Linux 操作系统,从嵌入式设备到超级计算机,C语言的身影无处不在.

但是,2026年的C语言开发者面临着无法回避的痛点:

// 典型的C语言"陷阱"代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void unsafe_copy(char *dest, const char *src, size_t n) {
    // 问题1:没有边界检查,缓冲区溢出风险
    strcpy(dest, src);
    
    // 问题2:内存泄漏隐患
    char *buffer = malloc(1024);
    // 如果这里提前返回,buffer 就泄漏了
    
    // 问题3:空指针解引用
    if (src != NULL) {
        // 但函数文档没说 src 能不能为 NULL
    }
    
    // 问题4:错误处理混乱
    FILE *fp = fopen("file.txt", "r");
    if (fp == NULL) {
        return; // 调用者不知道失败了
    }
}

// 问题5:宏的调试地狱
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(1, 2); // 看起来简单...
int y = MAX(f(), g()); // 糟糕!f() 或 g() 可能被调用两次!

C语言的核心问题总结:

问题表现后果
没有模块系统所有符号默认全局可见命名冲突,封装性差
内存安全漏洞手动管理内存 + 无边界检查缓冲区溢出,UAF,重复释放
错误处理混乱返回 -1NULL,无类型检查调用者容易忽略错误
宏系统的缺陷简单文本替换,无类型信息调试困难,副作用隐蔽
未定义行为(UB)有符号整数溢出,空指针访问安全漏洞,优化后的诡异行为
构建系统碎片化Make,CMake,Autotools,Meson...学习曲线陡峭,跨平台困难

1.2 为什么 Rust 不是银弹?

Rust 凭借所有权系统借用检查器,在内存安全方面取得了革命性突破.但是:

// Rust 的学习曲线:新手常被编译器"教育"
use std::fs::File;
use std::io::{self, Read};

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?; // 问题1:? 操作符的语义需要理解
    let mut contents = String::new();
    
    // 问题2:生命周期标注让很多开发者头疼
    // 问题3:异步 Rust 的复杂度(async/await + runtime)
    // 问题4:编译时间长(大型项目可达数十分钟)
    // 问题5:Rust 的"隐藏控制流"--? 操作符实际上是个隐式 return
    
    file.read_to_string(&mut contents)?;
    Ok(contents)
}

// 生命周期:Rust 最劝退的特性之一
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 这个 'a 是什么?为什么需要它?新手:

Rust 的"激进"路线:

  • 优点:内存安全保证,零成本抽象,强大的类型系统
  • 缺点:学习曲线陡峭,编译时间长,"对抗编译器"的体验

1.3 Zig 的"第三条路线"

Zig 由 Andrew Kelley 于 2015年发起,设计目标是:

"A programming language that can replace C"
一门可以替代 C 的编程语言.

Zig 的核心设计哲学:

  1. No hidden control flow(无隐藏控制流)

    • try,defer,errdefer 都是显式的
    • 没有 ? 操作符那样的"隐式 return"
  2. No hidden allocations(无隐藏内存分配)

    • 标准库函数不会悄悄调用 malloc
    • 内存分配器(Allocator)必须显式传入
  3. Comptime(编译期计算)

    • 比 C 宏强大,比 C++ 模板直观
    • 编译期执行任意代码,生成类型安全的代码
  4. Seamless C interop(无缝 C 互操作)

    • 直接 #include <c_header.h>,无需绑定层
    • 可以逐步从 C 迁移到 Zig
  5. Optional safety(可选的安全性)

    • Debug 模式:完整的越界检查,内存泄漏检测
    • ReleaseFast 模式:零开销,和 C 一样的性能

2. Zig 设计哲学:为什么"无隐藏控制流"改变一切

2.1 什么是"隐藏控制流"?

**隐藏控制流(Hidden Control Flow)**指的是:代码表面上看起来是顺序执行,但实际上某些语法结构会隐式地跳转,返回或抛出异常.

2.1.1 C++ 的例子

// C++ 的"隐藏控制流"陷阱
#include <iostream>
#include <vector>

int main() {
    std::vector<int> vec = {1, 2, 3};
    
    // 看起来只是"调用构造函数",但实际上可能抛异常
    // 如果 vec[10] 越界,std::vector::at() 会抛出 std::out_of_range
    try {
        int x = vec.at(10); // 隐藏控制流:可能 throw
        std::cout << x << stdline;
    } catch (const std::out_of_range& e) {
        std::cout << "Error: " << e.what() << std::endl;
    }
    
    // 更隐蔽的例子:操作符重载
    std::string s = "Hello";
    s += " World"; // 看起来是简单的拼接,但实际上可能抛异常(内存分配失败)
    
    // 析构函数:隐藏的清理逻辑
    // 你看到 vec 离开作用域,但没看到 ~vector() 被调用
    return 0;
}

2.1.2 Rust 的例子

// Rust 的 ? 操作符:方便的语法糖,但是"隐藏控制流"
use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    // ? 操作符:如果 read_to_string 返回 Err,这个函数会立即返回 Err
    // 你看不到显式的 return,但控制流确实跳转了
    let content = fs::read_to_string("config.toml")?;
    
    // 上面一行等价于:
    // let content = match fs::read_to_string("config.toml") {
    //     Ok(s) => s,
    //     Err(e) => return Err(e), // 隐藏在这里!
    // };
    
    Ok(content)
}

// 更复杂的例子:? 在 Option 上也能用
fn find_user_id(users: &[String], name: &str) -> Option<usize> {
    let idx = users.iter().position(|u| u == name)?; // 如果 None,立即返回 None
    Some(idx)
}

2.1.3 Zig 的解决方案

// Zig 的"显式控制流"
const std = @import("std");

fn readConfig(allocator: std.mem.Allocator) ![]u8 {
    // ![]u8 是 []u8!error{FileNotFound, OutOfMemory} 的语法糖
    
    // Zig 的 try:显式地"展开"错误联合
    // 如果 readFile 返回错误,try 会立即返回该错误
    // 关键:你能看到 try 关键字,控制流没有隐藏!
    const content = try readFile(allocator, "config.toml");
    
    // 等价写法(不使用 try):
    const content_explicit = readFile(allocator, "config.toml") catch |err| {
        std.debug.print("Failed to read config: {}\n", .{err});
        return err;
    };
    
    return content;
}

// Zig 的 defer:显式的资源清理
fn exampleDefer() void {
    const file = std.fs.cwd().openFile("data.txt", .{}) catch unreachable;
    defer file.close(); // 函数返回时一定会执行,你能看到这个声明
    
    // 使用 file...
    
    // 不需要手动调用 file.close(),但 defer 是显式声明的
}

关键差异总结:

语言隐藏控制流示例Zig 的做法
C++异常,析构函数,操作符重载vec.at(10) 可能 throw无异常,无析构函数,无操作符重载
Rust? 操作符read_to_string()? 隐式 returntry readFile() 显式展开
Godefer类似 ZigZig 的 defer 更灵活(支持 errdefer)
Zig-所有控制流跳转都是显式的

2.2 "无隐藏内存分配":为什么它很重要?

2.2.1 C++ 的隐藏分配

// C++ 标准库中到处都是隐藏的 malloc
#include <string>
#include <vector>
#include <iostream>

int main() {
    std::string s = "Hello"; // 堆分配(可能)
    s += " World"; // 可能触发 realloc
    
    std::vector<int> v = {1, 2, 3}; // 堆分配
    v.push_back(4); // 可能触发 realloc
    
    // 问题:在实时系统或内存受限环境中,你无法控制这些分配
    // 如果内存不足,std::bad_alloc 会抛出(又是隐藏控制流)
}

2.2.2 Zig 的显式分配器

const std = @import("std");

// Zig 标准库的函数:分配器必须显式传入
fn processData(allocator: std.mem.Allocator, input: []const u8) ![]u8 {
    // 你可以选择:
    // 1. std.heap.page_allocator - 直接 mmap,慢但简单
    // 2. std.heap.GeneralPurposeAllocator - 通用分配器,带泄漏检测
    // 3. std.heap.ArenaAllocator - 批量释放,适合临时对象
    // 4. std.heap.c_allocator - 使用 C 的 malloc/free
    
    const output = try allocator.dupe(u8, input); // 明确使用 allocator
    return output;
}

// 调用者可以控制用什么分配器
pub fn main() !void {
    // 使用通用分配器(Debug 模式检测内存泄漏)
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    const result = try processData(allocator, "Hello");
    defer allocator.free(result);
    
    std.debug.print("Result: {s}\n", .{result});
}

为什么这很重要?

  1. 性能可预测:在实时系统(游戏,嵌入式,高频交易)中,你不能承受意外的内存分配
  2. 内存使用可控:你可以为不同场景选择不同的分配器(池分配器,栈分配器,伙伴分配器)
  3. 无 GC 暂停:Zig 没有垃圾回收,内存管理完全由你控制(但通过工具辅助避免错误)

3. 核心特性深度剖析

3.1 Comptime:编译期元编程的终极形态

Comptime(编译期计算)是 Zig 最强大的特性之一,它让你可以在编译期执行任意代码,生成类型安全的,零开销的运行时代码.

3.1.1 C 宏 vs C++ 模板 vs Zig Comptime

C 宏的问题:

// C 宏:简单文本替换,无类型检查
#define SQUARE(x) ((x) * (x))

int main() {
    int a = 5;
    int b = SQUARE(a++); // 糟糕!a++ 被展开两次,a 增加了 2 次
    // 展开后:((a++) * (a++)) - 未定义行为!
    
    // 另一个问题:宏不能调试
    // 编译器错误指向宏展开后的代码,你看到的是 (a++) * (a++) 而不是 SQUARE(a++)
}

C++ 模板的问题:

// C++ 模板:强大的元编程,但语法晦涩
#include <iostream>
#include <type_traits>

// 编译期计算:需要借助 constexpr + 模板元编程
template<typename T>
constexpr T square(T x) {
    return x * x;
}

// 更复杂的例子:编译期斐波那契
template<int N>
struct Fibonacci {
    static constexpr int value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};

template<>
struct Fibonacci<0> {
    static constexpr int value = 0;
};

template<>
struct Fibonacci<1> {
    static constexpr int value = 1;
};

int main() {
    std::cout << Fibonacci<10>::value << std::endl; // 55
    // 问题:错误信息极其难懂,编译时间长
}

Zig Comptime 的解决方案:

const std = @import("std");

// Comptime 函数:看起来就像普通函数,但在编译期执行
fn square(comptime x: anytype) @TypeOf(x) {
    return x * x;
}

// 调用:编译期计算
const result = comptime square(5); // 编译期计算出 25,运行时零开销

// 更强大的例子:编译期生成类型
fn Vector(comptime T: type, comptime dim: usize) type {
    return struct {
        data: [dim]T,
        
        fn add(self: @This(), other: @This()) @This() {
            var result: @This() = undefined;
            for (&result.data, self.data, other.data) |*r, s, o| {
                r.* = s + o;
            }
            return result;
        }
        
        fn dot(self: @This(), other: @This()) T {
            var sum: T = 0;
            for (self.data, other.data) |s, o| {
                sum += s * o;
            }
            return sum;
        }
    };
}

// 使用:编译期生成类型
const Vec3f = Vector(f32, 3); // 3D 向量,f32 类型
const Vec4d = Vector(f64, 4); // 4D 向量,f64 类型

pub fn main() void {
    const v1 = Vec3f{ .data = .{1.0, 2.0, 3.0} };
    const v2 = Vec3f{ .data = .{4.0, 5.0, 6.0} };
    
    const v3 = v1.add(v2); // 编译期生成 add 函数,零开销
    std.debug.print("v3 = [{d}, {d}, {d}]\n", .{v3.data[0], v3.data[1], v3.data[2]});
    // 输出:v3 = [5.0, 7.0, 9.0]
}

3.1.2 Comptime 分支:根据类型定制行为

// 编译期分支:根据类型选择不同的实现
fn printValue(value: anytype) void {
    // @TypeOf(value) 在编译期可知
    if (comptime @TypeOf(value) == i32) {
        std.debug.print("i32: {d}\n", .{value});
    } else if (comptime @TypeOf(value) == f64) {
        std.debug.print("f64: {d:.2}\n", .{value});
    } else if (comptime @typeInfo(@TypeOf(value)) == .Pointer) {
        std.debug.print("Pointer: {s}\n", .{value});
    } else {
        @compileError("Unsupported type");
    }
}

// 更实用的例子:序列化的 compile-time 多态
fn serialize(writer: anytype, value: anytype) !void {
    const T = @TypeOf(value);
    
    // 编译期判断类型,生成最优化的序列化代码
    switch (@typeInfo(T)) {
        .Int => |info| {
            // 整数:根据位数选择序列化方式
            if (info.bits <= 32) {
                try writer.writeIntNative(u32, @intCast(value));
            } else {
                try writer.writeIntNative(u64, @intCast(value));
            }
        },
        .Float => {
            // 浮点数:序列化为 IEEE 754 二进制表示
            try writer.writeAll(std.mem.asBytes(&value));
        },
        .Pointer => |info| {
            if (info.child == u8) {
                // 字符串:先写长度,再写内容
                try writer.writeIntNative(u32, @intCast(value.len));
                try writer.writeAll(value);
            }
        },
        else => @compileError("Unsupported type for serialization"),
    }
}

3.1.3 Comptime 字符串操作:零开销的 DSL

// 编译期字符串操作:实现类型安全的 SQL 查询构建器
fn Query(comptime fields: []const []const u8) type {
    // fields 在编译期可知,我们可以生成类型安全的查询
    var struct_fields: [fields.len]std.builtin.Type.StructField = undefined;
    
    for (&struct_fields, fields) |*field, name| {
        field.* = .{
            .name = name,
            .type = []const u8, // 简化处理:所有字段都是字符串
            .default_value = null,
            .is_comptime = false,
            .alignment = 1,
        };
    }
    
    return @Type(.{ .Struct = .{
        .layout = .Auto,
        .fields = &struct_fields,
        .decls = &.{},
        .is_tuple = false,
    }});
}

// 使用:编译期生成类型
const User = Query(&.{"id", "name", "email"});

pub fn main() void {
    const user = User{
        .id = "1",
        .name = "Alice",
        .email = "alice@example.com",
    };
    
    std.debug.print("User: id={s}, name={s}, email={s}\n",
        .{user.id, user.name, user.email});
}

Comptime 的性能优势:

// 基准测试:Comptime vs 运行时泛型
const std = @import("std");

// 方案1:Comptime 多态
fn maxComptime(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

// 方案2:运行时多态(使用 anytype)
fn maxRuntime(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    return if (a > b) a else b;
}

// 方案3:C 的 void* + 函数指针(类型不安全)
// 省略...

pub fn benchmark() void {
    const N = 1000000;
    
    // Comptime:编译期生成 i32 专用的 max 函数,零开销
    var sum1: i32 = 0;
    for (0..N) |i| {
        sum1 += maxComptime(i32, @intCast(i), 0);
    }
    
    // Runtime:编译器可能内联,但可能生成泛型代码
    var sum2: i32 = 0;
    for (0..N) |i| {
        sum2 += maxRuntime(@intCast(i), 0);
    }
    
    std.debug.print("Comptime: {d}, Runtime: {d}\n", .{sum1, sum2});
}

3.2 错误处理:错误联合类型与 try 表达式

3.2.1 传统错误处理的痛点

C 语言:返回值 + errno

// C 的错误处理:容易忽略返回值
#include <stdio.h>
#include <stdlib.h>

int process_file(const char *path) {
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        return -1; // 调用者需要知道 -1 代表什么错误
    }
    
    // 问题:错误码没有类型,不同函数可能用不同的错误码约定
    char *buffer = malloc(1024);
    if (buffer == NULL) {
        fclose(fp);
        return -2; // 又是不同的错误码...
    }
    
    // 如果这里提前返回,需要记住释放资源
    if (fread(buffer, 1, 1024, fp) != 1024) {
        free(buffer);
        fclose(fp);
        return -3;
    }
    
    // 成功路径也要记得释放
    free(buffer);
    fclose(fp);
    return 0;
}

C++:异常

// C++ 的异常:隐藏控制流 + 性能开销
#include <iostream>
#include <fstream>
#include <stdexcept>

void process_file(const std::string& path) {
    std::ifstream file(path);
    
    if (!file.is_open()) {
        throw std::runtime_error("Failed to open file"); // 隐藏控制流!
    }
    
    // 问题1:你看不到哪些函数会抛异常(除非查文档)
    // 问题2:异常影响性能(栈展开,RAII)
    // 问题3:在实时系统中,异常的延迟不可预测
    
    std::string line;
    while (std::getline(file, line)) {
        // 如果 getline 抛异常,控制流跳转到哪里?
        std::cout << line << std::endl;
    }
}

3.2.2 Zig 的错误联合类型

const std = @import("std");

// 定义错误集合
const FileError = error {
    FileNotFound,
    PermissionDenied,
    OutOfMemory,
};

// 错误联合类型:T!E 是 E!T 的语法糖
// 这里:processFile 可能返回 []u8,也可能返回 FileError
fn processFile(allocator: std.mem.Allocator, path: []const u8) FileError![]u8 {
    // 打开文件
    const file = std.fs.cwd().openFile(path, .{}) catch |err| {
        // catch:显式处理错误
        std.debug.print("Failed to open {s}: {}\n", .{path, err});
        return err; // 返回错误
    };
    defer file.close(); // defer:确保文件关闭
    
    // 读取文件内容
    const content = file.readToEndAlloc(allocator, 1024 * 1024) catch |err| {
        std.debug.print("Failed to read {s}: {}\n", .{path, err});
        return err;
    };
    
    return content; // 成功:返回内容
}

// 调用者:必须处理错误
pub fn main() void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    
    // 方式1:使用 try(错误时自动返回)
    const content1 = processFile(allocator, "example.txt") catch |err| {
        std.debug.print("Error: {}\n", .{err});
        return;
    };
    defer allocator.free(content1);
    
    // 方式2:使用 if (result) |value| else |err|(更明确)
    const result = processFile(allocator, "data.txt");
    if (result) |content2| {
        defer allocator.free(content2);
        std.debug.print("Content: {s}\n", .{content2});
    } else |err| {
        std.debug.print("Failed: {}\n", .{err});
    }
}

3.2.3 trycatch 的深入理解

// try 的本质:语法糖
fn example1() !void {
    const content = try readFile("data.txt"); // try:错误时自动 return
    // 等价于:
    const content_explicit = readFile("data.txt") catch |err| return err;
}

// catch 的高级用法:错误恢复
fn example2(allocator: std.mem.Allocator) !void {
    // 如果文件不存在,使用默认值
    const content = readFile(allocator, "config.toml") catch |err| blk: {
        if (err == error.FileNotFound) {
            // 错误恢复:返回默认配置
            break :blk try allocator.dupe(u8, "key = 'default'");
        } else {
            // 其他错误:继续返回
            return err;
        }
    };
    
    // 使用 content...
}

// 多个错误的处理
fn example3() void {
    const result = someFunction() catch |err| switch (err) {
        error.OutOfMemory => {
            std.debug.print("OOM, try again later\n", .{});
            return;
        },
        error.FileNotFound => {
            std.debug.print("File not found, check path\n", .{});
            return;
        },
        else => {
            std.debug.print("Unknown error: {}\n", .{err});
            return;
        },
    };
    
    // 使用 result...
}

3.2.4 defererrdefer:资源管理的艺术

const std = @import("std");

// defer:无论函数如何退出,都会执行
fn deferExample() void {
    const file = std.fs.cwd().openFile("test.txt", .{}) catch unreachable;
    defer file.close(); // 函数返回前执行
    
    const buffer = std.heap.page_allocator.alloc(u8, 1024) catch unreachable;
    defer std.heap.page_allocator.free(buffer); // 释放内存
    
    // 使用 file 和 buffer...
    
    // 不需要手动调用 file.close() 和 free(buffer)
    // defer 确保资源释放(即使提前 return 或 发生错误)
}

// errdefer:只在错误时执行(类似 defer,但只在该函数返回错误时执行)
fn errdeferExample(allocator: std.mem.Allocator) !void {
    const resource1 = try allocator.alloc(u8, 1024);
    errdefer allocator.free(resource1); // 只在错误时释放
    
    const resource2 = try allocator.alloc(u8, 2048);
    errdefer allocator.free(resource2); // 只在错误时释放
    
    // 如果这里返回错误,resource1 和 resource2 会被释放
    // 如果成功,调用者负责释放
    
    if (someCondition()) {
        return error.SomeError; // errdefer 触发:释放 resource1 和 resource2
    }
    
    // 成功:返回资源,调用者负责释放
    // (实际中应该封装成结构体,实现 deinit() 方法)
}

// 实际应用:实现 File 的自动关闭
const AutoCloseFile = struct {
    file: std.fs.File,
    
    fn open(path: []const u8) !AutoCloseFile {
        const file = try std.fs.cwd().openFile(path, .{});
        return AutoCloseFile{ .file = file };
    }
    
    fn close(self: *AutoCloseFile) void {
        self.file.close();
    }
    
    // 使用 defer 自动关闭
    fn withFile(comptime func: fn (std.fs.File) void) !void {
        var file = try open("data.txt");
        defer file.close();
        
        func(file.file);
    }
};

3.3 内存管理:手动控制 + 可选安全检查

3.3.1 Zig 的内存安全策略

Zig 采用了分层安全模型:

构建模式内存安全检查性能使用场景
Debug完整检查(越界, use-after-free,double-free)开发和测试
ReleaseSafe部分检查(关键路径)中等预发布环境
ReleaseFast无检查最快生产环境
const std = @import("std");

// Debug 模式:完整的越界检查
fn debugExample() void {
    var array: [10]u8 = undefined;
    
    // Debug 模式:这行会触发运行时 panic
    // array[10] = 42; // 越界!
    
    // Debug 模式:检测 use-after-free
    const ptr = &array[0];
    // 如果 array 离开作用域,ptr 变成悬垂指针
    // Zig 的 Debug 模式会检测到(通过堆栈痕迹)
}

// ReleaseFast 模式:零开销
fn releaseFastExample() void {
    var array: [10]u8 = undefined;
    
    // ReleaseFast 模式:不检查越界,和 C 一样快
    array[10] = 42; // 未定义行为!但不会 panic
}

3.3.2 标准库中的内存分配器

const std = @import("std");

---

## 本文未完成加载

> 由于文章篇幅过长(原稿 58,444 字符),本文为节选版本。
> 完整版请访问程序员茄子官网获取。
复制全文 生成海报 Zig 系统编程 Comptime C互操作 内存安全

推荐文章

PHP 的生成器,用过的都说好!
2024-11-18 04:43:02 +0800 CST
55个常用的JavaScript代码段
2024-11-18 22:38:45 +0800 CST
Vue3 结合 Driver.js 实现新手指引
2024-11-18 19:30:14 +0800 CST
开源AI反混淆JS代码:HumanifyJS
2024-11-19 02:30:40 +0800 CST
Golang - 使用 GoFakeIt 生成 Mock 数据
2024-11-18 15:51:22 +0800 CST
Gai:AI 原生的 Go Web 全栈框架
2026-05-21 16:19:43 +0800 CST
程序员茄子在线接单