数据库系统的核心作用在于高效地存储和管理数据，并支持从海量信息中快速检索所需记录。传统的 SQL 关系型数据库在数据规模持续增长的情况下，容易遭遇单机性能瓶颈。目前提升性能的主流做法多依赖垂直扩展增强单机硬件能力，而非水平扩展增加节点以线性扩容。由于其架构限制，关系型数据库在弹性扩展性方面存在障碍，难以充分适应多节点分布式环境。同时为满足 ACID 事务特性，这类系统在支持分布式事务处理时也面临一定的技术挑战。相比之下，NoSQL 数据库更适用于大规模数据存储和高吞吐、低延迟的应用场景。由于 NoSQL 不受固定数据结构和关系的限制，具有更灵活的扩展能力，并且支持节点的动态扩展，从而更好地适应分布式环境。无论是关系型 SQL 数据库还是 NoSQL 数据库，它们都有一个共同的核心组件存储引擎，这是数据库设计与实现的关键核心部分。目前数据库存储引擎可以分为两大类，一类是基于内存的 （In-Memory）实现，例如 Redis 和 Memcached 这类的将数据全部存储在内存中；另外一种则是类似于 MySQL 的 InnoDB 和 LevelDB 这种将数据存储在磁盘中。两者都有各自的优势和劣势，基于内存的存储引擎通常存储的数据量较少，但其访问速度往往比基于磁盘的存储引擎快好几个数量级，而基于磁盘的存储引擎访问速度较慢，但存储数据量要比内存引擎多几个数量级。通常内存的价格要比磁盘的贵很多，这也是在选择存储硬件时要考虑的成本问题。存储引擎的开发者需要权衡读写性能和硬件成本的同时，重点在于如何高效存储数据和检索数据。数据库的存储引擎相当于在操作系统文件系统之上，建立的一套用于数据组织、存储与查询的逻辑管理层，最终实现是依赖于操作系统的文件系统 API 接口的，最终数据是被持久化到磁盘文件中保存，这个就对应着 ACID 中的 Durability 数据持久性。持久性（Durability）是存储引擎最为至关重要的功能实现，目前很多基于内存版本的 NoSQL 数据库例如 Memcached 和 Redis 在这方面做的就比较差，在 NoSQL 服务器运行过程中突然崩溃断电就会导致数据没有被持久化存储的情况，从而导致数据丢失影响到上层的业务程序。[scode type="yellow"]所谓的持久性就是如何将计算机运算的结果持久化存储在一些物理介质中。[/scode]存储是现代计算机最核心的功能之一，计算机中的程序和文件以数据块的形式保存在各种存储介质中，主要可分为持久化存储和非持久化存储两类。非持久化存储通常指内存（如 RAM），其特点是在断电或关机后，数据会被清除。尽管容量通常小于持久化存储，但其高速读写性能使其广泛应用于程序运行过程中的数据处理和临时缓存。相比之下，持久化存储如硬盘、固态硬盘、DVD、磁带等，能够在断电后保留数据，适用于长期保存程序和文件信息。[scode type="yellow"]鉴于数据可能存储在多种物理介质（如磁盘、SSD、光盘等）上，主流的操作系统需要提供一致的抽象接口，以屏蔽底层硬件差异，使用户能够高效、统一地进行文件的读写、创建与删除等操作。当前主流操作系统均实现了完整的文件系统，开发者可以通过如 POSIX 这样的标准 API 接口来访问和管理存储设备，而无需关心具体设备的物理实现方式。尽管操作系统的文件子系统通过标准如 POSIX 提供了对存储设备的访问能力但仍存在一些限制；例如存储性能受限于设备的物理读写速度，同时文件系统通常采用页缓存 Page Cache 或写缓冲机制，这意味着数据写入操作并不会立即持久化到磁盘，一旦系统发生故障或意外断电，可能会导致数据丢失，影响系统的可靠性。[/scode]基础架构综上所述，针对这些场景的问题 UrnaDB 采用了基于 Log-Structured Megre Tree 日志结构化文件系统的存储引擎实现，存储引擎会以 Append-Only Log 的方式将所有的数据操作写入到数据文件中。同时 UrnaDB 为了高速查询检索数据记录，存储引擎会将数据记录索引信息全部保存在内存中，从而实现高效快速的查询目标数据记录。这样的设计的好处是能以磁盘最大写入性能进行写入数据，并且还能减少读取磁盘索引所需要的时间，通过一次索引定位来读取数据记录，写入和查询流程图：其核心持久化机制基于预写日志 Write-Ahead Logging 简称 WAL ，在对数据执行任何操作前，都会先将操作记录写入 WAL 日志文件。WAL 文件不仅承担持久化的角色，也作为主要的数据存储载体。在数据库进程崩溃后，只需从 WAL 日志中顺序读取各条 Segment 记录，即可高效恢复内存中的索引结构。在 UrnaDB 中对这些 WAL 数据文件有一个统称为叫 Region 文件，这些文件有单个固定大小限制，当一个数据文件写满之后就会被关闭，会重新创建一个新的 Active 活跃的 Region 文件进行数据记录的进行写入。被关闭的 Region 文件会被视为冷数据文件，随着数据库不间断长时间运行 Region 文件会不断递增逐渐占用磁盘空间。此时数据库进程就需要对旧的 Region 文件执行压缩 Compaction 和定期清理，以降低存储压力并提高查崩溃数据恢复时的启动效率，压缩流程原理图：目前工业级的存储系统中，RaikDB 是一个采用类似模型实现的数据库产品，RaikDB 是基于 Amazon Dynamo 论文的设计理念构建而成。其 RaikDB 底层也是采用的顺序写入的日志式存储引擎，并将其命名为 Bitcask 存储模型，以提升写入性能和读写效率。该模型与 UrnaDB 所采用的存储引擎在设计理念上颇为相似。Bitcask 存储引擎在执行数据压缩 Compaction 时会生成 Hint 文件，其作用是在存储引擎重启时，辅助快速重建内存索引，从而避免每次启动都需全量扫描数据文件，显著提升系统的启动效率。然而 Hint 文件仅在 Compaction 过程中生成，所记录的只是当时内存索引的快照，无法反映实时状态。[scode type="yellow"]针对这一问题 UrnaDB 在存储引擎中进行了改进，将 Hint 文件机制替换为 Checkpoint 功能，系统会在固定时间间隔内，定期将当前内存索引持久化为 checkpoint 文件写入磁盘。当服务器进程发生异常崩溃，也可通过读取 checkpoint 快照快速恢复内存索引状态。在实际恢复流程中，UrnaDB 会优先加载最近一次的 checkpoint 快照，并从该时间点之后的 Region 文件中继续回放增量数据。通过这种方式，有效提升了系统启动恢复的速度，减少了启动耗时，同时兼顾了数据一致性与运行效率。[/scode]在该存储引擎设计的基础上，UrnaDB 构建了内置的 HTTP 服务器，并引入了多种抽象化的数据结构层。整体架构如下图所示：这些模块相互协作构成了一个完整的单机版 NoSQL 数据库，实现了从底层存储到服务接口的一体化设计。检查点技术[scode type="yellow"]由于 UrnaDB 数据库存储引擎采用将所有索引数据存储在内存中的设计，一旦数据库进程意外崩溃，内存中的索引信息将会丢失，未能持久化到磁盘。这样在下一次启动时，系统需要重新扫描所有数据文件，以重建内存中的索引结构，如果数据文件体量较大，将导致启动过程变得缓慢，启动耗时较长。[/scode]Checkpoint 功能机制会通过定期将内存中的索引状态保存到磁盘，在发生异常重启时即可快速加载 checkpoint 文件，从而大幅缩短数据库的启动时间。开启 checkpoint 功能可以在数据库崩溃后显著加快启动恢复的速度。然而 checkpoint 文件的生成时间间隔设置同样需要谨慎权衡。如果生成间隔过短，将导致 checkpoint 文件过于频繁地写入磁盘，占用大量 I/O 资源，可能影响数据库的整体性能；而如果间隔设置过长，崩溃恢复时仍需扫描大量 Region 数据记录，反而无法发挥 checkpoint 的优势，导致启动时间较长。因此合理配置 checkpoint 的生成频率，需在性能开销与数据恢复效率之间做出平衡，建议根据实际业务的数据写入频率和系统负载，动态调整该参数以获得最佳效果。磁盘垃圾回收器[scode type="yellow"]由于 UrnaDB 存储引擎采用顺序日志写入结构，随着系统运行时间的增长，每个 Region 区域对应的磁盘数据文件会不断增大。默认情况下 Region 文件中的每条数据记录未经过压缩，导致磁盘空间占用随数据量增加而持续上升。为了解决这一问题 UrnaDB 存储引擎内置了数据压缩算法，启用该压缩器功能后，可以显著减少磁盘文件的存储体积，从而有效降低存储成本。[/scode]在配置 Region 区域的垃圾回收功能时，需要综合考虑多种因素。若不启用该功能，随着系统长时间运行，磁盘空间占用将持续增长，甚至可能过高，影响系统稳定性。若启用垃圾回收，则必须合理设定其执行周期：周期过长：会导致空间回收不及时，磁盘占用上升，同时在压缩 Region 时耗时较长。周期过短：会导致垃圾回收过于频繁，增加系统资源开销，进而影响存储引擎的整体性能。在 UrnaDB 中垃圾回收器的执行周期同样依赖于 Time 触发，并且其执行过程会占用一定的系统资源。如果采用并行执行机制，这段时间对外部应用基本无感，系统可持续对外提供服务。这种设计理念与许多现代编程语言的垃圾回收器类似，UrnaDB 的 Region 区域划分为 Active Region 和 Read Region，其架构与 Java 虚拟机（JVM）中的垃圾回收机制具有相似性。当垃圾回收开始运行时，它不会对 Active Region 中的活跃数据进行操作，而是仅针对旧的 Read Region 区域执行垃圾分析与清理，从而避免对当前读写路径造成干扰，不会出现类似于编程语言虚拟机中的 Stop-The-World 世界时钟停止问题，减少压缩磁盘空间的抖动，保障数据库稳定性。时间就是一个问题[scode type="yellow"]在整个 NoSQL 数据库的设计过程中，许多复杂问题的根源其实都与物理世界中的 Time 有关。当前的 UrnaDB 实现为单机版本，因此主要面临的是本地磁盘 I/O 和线程调度带来的延迟问题。然而一旦在未来引入多个存储节点，发展为分布式架构，就必须应对网络延迟、时钟同步、事务一致性等与时间密切相关的挑战。因此无论是当前的 NoSQL 单机实现，还是面向未来的分布式扩展，如何合理建模和管理时间 Time 维度，始终是数据库架构设计中的核心研究问题之一。[/scode]在单机版本中还面临另一个挑战如何为数据记录提供过期时间（Time-To-Live）支持，尽管 TTL 是常见的需求，但其实现往往较为复杂，目前主流 NoSQL 数据库普遍采用惰性删除机制，即仅在数据被访问时才判断其是否过期并删除，这种方式依赖于外部读取请求的触发，可能导致过期数据长期滞留在存储中。因此如何合理权衡 Checkpoint 的执行频率与垃圾回收的触发时机？是目前 UrnaDB 数据库系统优化的重要考量。如果在数据高频读写期间执行垃圾回收操作如压缩磁盘空间，可能会带来短暂的读写延迟。同时存储引擎在数据迁移和压缩过程中，也可能导致磁盘空间临时占用上升，进而影响整体性能和资源利用。对此我更推荐采用主动回收机制，通过周期性运行的后台垃圾回收线程，专门处理过期数据的清理任务，该方式可以更好地控制存储空间的占用，提升系统的可预测性与稳定性。
在互联网上经常可以看到程序员因为编程语言的选择而争论不休，常常出现类似 “XXX vs XXXX” 的评测文章，同时也不排除一些语言布道师或商业公司在背后炒作某种语言的热度，来达到某种商业目的。大多数文章的评测重点通常是程序的执行效率和内存占用，很少从语言特性、语法或语义的角度进行深入分析和讲解，我这篇文章将从这个角度来讨论 Go 语言设计丑陋之处。在我编程生涯中使用过很多门编程语言，这其中就包括所谓的大道至简的 Go 语言，总体来说使用 Go 语言来编写逻辑是非常容易的，天生支持协程，采用 CSP 和 GMP 模型来设计，极度克制的语法和运行时并发模型，显著降低了高并发程序的工程门槛，使得开发高并发应用也非常简单。它的语法简单和 25 关键字非常容易掌握，能使程序员快速掌握并且编写代码逻辑，并不意味着 Go 的设计有多么好。反而在我使用 Go 编程过程中出现很多感觉很多丑陋的设计，而不像国内某些 Go 语言布道师吹崇它多么多么好，我个人感觉哪些很多布道师从 PHP 转换过来的，就没有写过其他高级的编程语言，例如 Kotlin 和 C# 、Rust 或者新版本的 Java 等... 当然作为某个编程语言的布道师可能说这门编程语言有多么多么好，这样才能忽悠到没有任何经验新人去学习这门编程语言。如果读者是 Go 语言的粉丝，这篇文章的内容会让你感到非常不适，乃至会让你感到作呕，但能让读者明白好的设计和相关问题在不同编程语言是如何解决的。空值 nil 和错误处理在计算机科学史上，很少有设计决策像 null 这样，既普遍存在又饱受诟病，它几乎出现在所有主流编程语言中，却也是无数程序崩溃、漏洞与安全事故的根源。这一现象并非偶然，与 C 语言的历史地位密切相关，由于 C 语言诞生于基础软件快速发展期，其设计选择在早期被广泛接受并固化下来，null 作为 “无对象” 未分配的内存的默认表示方式也随之成为事实标准。随后伴随着大量基础软件、操作系统内核、运行时库、编译器以及工具链都以 C 语言或其内存模型为基础构建，导致 null 这一设计被层层继承和放大，深度嵌入整个软件生态之中。众多编程语言实现自身编译器、虚拟机和运行时系统都是以 C 语言为依赖基础，null 不再只是某一门语言的设计细节，而演变为跨语言、跨系统的结构性问题，持续影响着后续数十年的语言设计与软件工程实践。Go 程序的错误处理，在编写逻辑复杂并且存在多种复杂异常的情况时会出现写出满屏 if err != nil，这个和 C 语言的一脉相承的，C 语言使用是错误码，Go 的错误处理只是添加一些上下文描述，并没有附加堆栈信息，例如下面的代码： if len(files) <= 0 { // 它这个错误设计要不停写 if err != nil err := lfs.createActiveRegion() if err != nil { return err } return nil }但一般程序员会自己优化掉，写成下面的这种方式：// return lfs.createActiveRegion()可以直接这么写，但是会丢弃错误堆栈处理的上下文，特别是在处理一些比较细节的错误分类的时候。Go 的错误处理机制中，错误值通过返回值 return 在调用栈中逐层向上冒泡传递，每个函数在检测到错误时，通常会将错误返回给上层调用者，由上层决定如何处理。这对于喜欢函数编程的程序员是一个很糟糕的设计，例如下面代码： sql2 := gsql.SelectAs([]string{"name", gsql.As("age", "年龄"), "id"}).From(UserInfo{}).ById(2) // SELECT name, age AS '年龄' FROM UserInfo WHERE id = 2 t.Log(sql2) sql3 := gsql.SelectAs(gsql.Alias(UserInfo{}, map[string]string{ "name": "名字", })).From(UserInfo{}).ById(1) // SELECT id, name AS '名字', age FROM UserInfo WHERE id = 1 t.Log(sql3)这段代码核心功能是我编写的一个用于 Go 的结构化查询语言代码生成器，类似于一个 SQL 在 GO 语言中 DSL 抽象实现，可以生成出 SQL 查询语句的字符串。但在调用这个 gsql 包提供的各种函数和 API 过程中会出现一个问题就是每个函数逻辑复杂有其他逻辑分支，可能会出现 error 的情况，因此按照 Go 的错误处理设计，每个函数必须返回其函数内部的 error 作为上下文，传递出去方便在出错时定位到问题。[scode type="yellow"]但为了满足写起来像函数编程链式调用的风格，又不得将这些函数的错误信息隐藏，包装起来，只有当调用完最后一个函数才能获取到具体的错误信息。每个函数在处理之前都需要检查基于上一个函数调用的结果的有没有出错，如果出错了就直接 return err ... 白白的创建一个并且运行新的栈帧...[/scode]这里列举一些其他编程语言我认可的设计例子，例如 C++ 和 Java 的设计，注这里的代码为 C++ 示例：try { // 任一步 throw 都会冒泡 auto c = A().b().c(); } catch(const std::runtime_error& e) { // 处理错误 }或者 Zig 的错误处理设计，Zig 也是类 C 的编程语言，它也是采用的 try ... catch ... 的方式来做错误处理，代码如下：// 任一步 try 都会将错误冒泡上一层 const a_val = try a(); const b_val = try a_val.b(); // 对 b_val.c() 进行错误处理 const c_val = b_val.c() catch |err| { ... }; // foo 函数有错误时为 x 变量设置默认值为 0 const x = foo() catch 0; // orelse 一个默认值 const value = p orelse defaultPtr;虽然不能函数级连调用，也是每一步来使用 try 来做错误处理，比 Go 每步都需要写 if err != nil 设计要好，Zig 每一步都有错误处理语义，try 失败就立刻返回当前函数，配合 catch 和 orelse 来为出错时设置默认值，不需要引入临时变量控制流是线性的，虽然不能 a().b().c() 这种函数级链式，但每一步的错误传播非常干净。[scode type="green"]这里我要指出是 Rust 的 ? + Result/Option 抽象，是我目前使用过语言里，对链式调用错误传播综合体验最好的，Rust 代码示例如下：[/scode]let x = a()?.b()?.c()?;链式调用发生未知的错误时 ? 只负责提前结束函数调用并且返回，在某些场景下上层的调用者在发生错误时还希望程序能继续正常运行，这时可以使用 unwrap_or 函数来设置默认值。// 函数式级联调用来处理错误 fn get_x() -> Option<T> { let x = a()?.b()?.c()?; Some(x) } // 在发生了错误的时，通过 unwrap_or 的设置默认值 let x = get_x().unwrap_or(default_value);在程序运行时，状态可能不确定，函数执行过程中可能抛出异常或返回 null ，导致无法获得预期的值。为此一些编程语言提供了默认值机制或语法糖，例如 Java 的 Optional<T> 、Kotlin 的可空类型，Kotlin 的代码如下：// 可为 null val name: String? = null // 非空类型，不能为 null val age: Int = 30 // name 为 null，则 length 也为 null，非法时使用 0 作为默认值 val length = name?.length ?: 0以及新版本的 ES6 也支持类似的默认值语法，JavaScript 代码如下：// ?? 预定义默认值 let value = someVariable ?? defaultValue;如果 someVariable 是 0、'...'、false、null、undefined、NaN ，就会使用默认值，someVariable 的非法的时，?? 可以让 defaultValue 成为 value 的值。同样 ES6 也是支持 object?.field 安全访问某个属性的值：// js 对象定义方式 var user = { name: "Alice", address: null }; let city = user.address?.city ?? "Unknown"; // "Unknown" console.log(city); 显然 JavaScript 作为一种动态类型语言，在运行时允许对已初始化的对象动态添加或删除字段，这就会导致对象的属性在运行过程中可能不确定，如果不对属性字段进行 null 或 undefined 的进行判断，就很容易在访问这些属性时导致程序崩溃或报错，不能达到预期执行路径的结果。[scode type="green"]因此 ES6 设计了可选链运算符 ?. 使用此运算符访问的对象或调用的函数是 null 或 undefined 则表达式会短路并计算为 undefined 而不是抛出错误。可选链运算符和 ?? 配合一起使用就能实现预定义默认值，从而安全地处理可能缺失的返回值，并使用预先定义的默认值保证程序能够正常运行，这是我认为最好的 PL 语法糖设计。[/scode]return 和 defer 冲突在多数编程语言 return 作用都是用来结束栈帧，结束函数的执行，并且如果函数签名中有返回值，那么还要顺带着返回值一起返回，但在 Go 语言还设计了 defer 关键字来辅助 return 来完成一些辅助清理工作，defer 关键字是用来延迟执行某个函数用途，并且保证在函数执行栈出现异常或者 return 时，能正常执行某个预定函数操作。在结合 defer 场景下就会出现问题，为了防止文件句柄打开忘记关闭情况下，在打开文件操作的代码逻辑后面就会紧跟上 defer 的操作，但 defer 并不能处理这个函数的调用返回值，return 和 defer 关键字的在关闭文件描述时发生干扰，例如下面的代码： file, err := os.Open(filePath) if err != nil { return fmt.Errorf("failed to open index file: %w", err) } defer file.Close()上面这段只以只读的方式打开了文件，所以只需要执行 defer file.Close() 操作就可以完成文件句柄关闭，但是下面这种情况在写入情况进行关闭，因为操作系统内核有缓冲区的存在，不一定能及时刷盘，所以在关闭文件的时候还要执行 file.Sync() 操作保证内核缓冲区的数据能及时刷盘再关闭，例如下面的代码：err := lfs.active.Sync() if err != nil { return fmt.Errorf("failed to sync active file: %w", err) } err = lfs.active.Close() if err != nil { return fmt.Errorf("failed to close active file: %w", err) }优化之后的代码，把两个操作合并为一个工具函数：defer utils.CloseFile(lfs.active)但 defer 并不能处理这个函数的调用返回值...这就导致执行会丢弃掉函数所传递出来的关键的错误信息...等价于下面的代码：// ??? _ = utils.CloseFile(lfs.active)上面的示例都是以 defer 来释放资源所面临的问题，下面的示例将开始对 defer 在配合 sync.Mutex 出错时一些代码示例的问题进行。在 Go 语言中编写并发程序防止资源竞争，通常都是使用 sync.Mutex 提供的互斥锁来保障临界区的资源。上锁的目的就是保证临界区的数据不被其他线程同时修改，但临界区范围需要程序员手动控制，也就是手动控制的上锁和解锁。Go 语言的互斥锁是不可重入的锁，意味着下一个锁进入临界区时，前一个持有锁的人必须解锁，通常这个解锁操作会配合 defer 来使用，例如下面代码：for _, imap := range lfs.indexs { // 上读锁，临界区结束时自动解锁 imap.mu.RLock() defer imap.mu.RUnlock() for inum, inode := range imap.index { bytes, err := serializedIndex(buf, inum, inode) if err != nil { return fmt.Errorf("failed to serialize index (inum: %d): %w", inum, err) } _, err = fd.Write(bytes) if err != nil { return fmt.Errorf("failed to write serialized index (inum: %d): %w", inum, err) } } }这是使用的好处是能在出错时主动释放锁，但不能控制临界区的范围，因为使用了 defer imap.mu.RUnlock() 来自动解锁，使用了 defer 需要整个函数嵌套调用栈执行完成才能解锁，并且被嵌套的 serializedIndex(...) 函数中也再次上锁，不可重入锁，会导致死锁。如果想控制锁的范围的，即锁的颗粒度，那么程序员就必须手动控制互斥锁主动释放和解锁，优化之后的代码：for _, imap := range lfs.indexs { // 上读锁 imap.mu.RLock() for inum, inode := range imap.index { bytes, err := serializedIndex(buf, inum, inode) if err != nil { // 出错提前解锁 imap.mu.RUnlock() return fmt.Errorf("failed to serialize index (inum: %d): %w", inum, err) } _, err = fd.Write(bytes) if err != nil { // 出错提前解锁 imap.mu.RUnlock() return fmt.Errorf("failed to write serialized index (inum: %d): %w", inum, err) } } // 循环结束后解锁 imap.mu.RUnlock() }优化之后的代码能控制锁的范围，但是又要在出错时手动释放互斥锁，那就要处理 if err != nil 的情况出现异常手动解锁，同样是一个很难用的设计。这里举几个其他语言的例子，另外一种就是 Java 7 之后的提供了 try-with-resources 语法，任何资源类只要实现 AutoCloseable 接口离开作用域时就会自动调用 close() 方法释放资源，代码如下：class MyResource implements AutoCloseable { public MyResource() { System.out.println("Resource acquired"); } public void doWork() { System.out.println("Working..."); } @Override public void close() { System.out.println("Resource released"); } } public class Main { public static void main(String[] args) { // 因为实现了 AutoCloseable 在 try 语句块会主动关闭资源 try (MyResource r = new MyResource()) { r.doWork(); } } }另外一个 Zig 语言同样使用 defer 在函数返回时做一些主动清理工作，同样会面临一些 defer mutex.unlock(); 只能等到整个 doSomething 函数执行完成时才能返回，但是在 Zig 允许使用 { ... } 定义作用域来缩小 defer 所清理资源的范围，解决了上面的 Go 语言每一步出做时需要在 if err != nil { ... } 中来手动控制处理每个错误分支的解锁逻辑。fn doSomething() !void { var mutex = std.Thread.Mutex.init(); { try mutex.lock(); // 只在这个作用域自动解锁 defer mutex.unlock(); try step1(); } // 离开作用域自动解锁 // 不需要锁 try step2(); }[scode type="green"]Zig 的一个特别设计是新增了 errdefer 关键字，与 defer 在函数退出时无论正常还是出错都执行不同，errdefer 仅在函数出错返回时触发，用于回滚、解锁或释放资源，而正常执行路径可以选择提前释放或交由调用方管理，例如上面 Go 语言手动错误分支解锁的逻辑，使用 Zig errdefer 关键字代码如下：[/scode]const std = @import("std"); fn doSomething() !void { var mutex = std.Thread.Mutex.init(); try mutex.lock(); // 正常路径手动解锁 errdefer mutex.unlock(); // 正常路径可以手动释放 try step1(); // 出错自动解锁 try step2(); // 出错自动解锁 // 正常路径手动解锁 mutex.unlock(); // 这两个函数不涉及到并发资源竞争不需要加锁 step3(); step4(); }如果使用的是 defer 那么要等整个 doSomething 正常函数返回才会自动解锁，就失去了对锁颗粒度范围的控制，例如上面的 step3() 和 step4() 函数不涉及到数据竞争就不需要加锁，如果采用 defer 则整个 doSomething 函数包括内部调用函数都上锁了。没有方法重载入，还有三目表达式，没有模式匹配，内置 init 函数和自定义 init 互相冲突并且自定义 var x = func() {}() 执行顺序和内置 init 函数执行顺序冲突，函数式编程没有 lambda 表达式的支持，范型不支持协变和逆变，标识符、Struct、变量、字段的访问控制和 JSON 正反序列化矛盾的冲突。另外构造器随意创建，不支持函数重载导致需要使用魔法编程可变参数进行实现类似于其他有重载功能语言，这对于一个设计友好 API 的人来说确实需要；另外接口太小，名称统一会导致被很多其他拥有相同字段接口所隐式的实现。协程泄漏和协程错误传播Go 语言最引以为傲的设计，是内建抢占式调度的 gorontine 以及与之配套的 runtime 并发模型，在 Go 语言里开启一个异步任务非常容易，只需要使用 go 关键字就使一个普通 func() 函数变成一个异步协程任务，代码如下：go func() { // ... }() // 👈 这个函数 return 了，goroutine 就死了goroutine 协程是轻量级的用户线程实现，并不意味着能无限得创建和泛用 go func() {...} 创建协程，Go 语言的 runtime 调度器只负责何时进行调度、栈管理、回收协程栈，根本就不会处理 goroutine 内部的状态变更和错误如何传播到上层，会导致协程内部错误被丢弃乃至不处理，最后会出现协程泄漏。[scode type="yellow"]原因就是 Go 语言在设计的时候，采用的所谓的 CSP 通过通信而非共享状态，goroutine 之间的状态同步与协作完全依赖于显式的通信机制如 channel 结构和 context 包。然而在实际使用 goroutine 处理异步并发任务时，Go 语言并未在语法或类型系统层面强制要求传入 channel 或 context 来管理 goroutine 及其子 goroutine 的生命周期，这使得并发结构的正确性完全依赖于开发者的经验和良好编码习惯。[/scode]一段典型使用 Go 语言实现非结构化并发代码示例如下：package main import ( "fmt" "time" ) func f(from string) { for i := 0; i < 3; i++ { go fmt.Println(from, ":", i) } } func main() { f("direct") go f("goroutine") go func(msg string) { go f(msg) }("going") time.Sleep(2 * time.Second) fmt.Println("done") }如上代码随意创建 goroutine 却没有对错误或异常退出路径进行统一管理，就很容易产生 goroutine 泄漏，这本质上属于一种非结构化并发的代码设计。如果想解决协程泄漏和错误传播的问题，就得使用 Go 所倡导的做法是采用 CSP 并发模型，通过 channel 或 context 将各个 goroutine 串联起来进行协同管理，这种方式要求开发者要在每个 goroutine 中显式编写对取消信号、错误返回和退出条件的处理逻辑，因此在每个 goroutine 协程里面都有一定的重复代码逻辑侵入。例如一段生产级的两个协程相互协作，错误使用 channel 代码逻辑如下：func recoveryIndex(reader *mmap.ReaderAt, indexs []*indexMap) error { offset := int64(len(dataFileMetadata)) type index struct { inum uint64 inode *inode } nqueue := make(chan index, (int64(reader.Len())-offset)/_INDEX_SEGMENT_SIZE) equeue := make(chan error, 1) var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() defer close(nqueue) buf := make([]byte, _INDEX_SEGMENT_SIZE) for offset < int64(reader.Len()) && len(equeue) == 0 { _, err := reader.ReadAt(buf, offset) if err != nil { equeue <- fmt.Errorf("failed to read index node: %w", err) return } offset += _INDEX_SEGMENT_SIZE inum, inode, err := deserializedIndex(buf) if err != nil { equeue <- fmt.Errorf("failed to deserialize index (inum: %d): %w", inum, err) return } if inode.ExpiredAt > 0 && inode.ExpiredAt <= time.Now().UnixMicro() { continue } nqueue <- index{inum: inum, inode: inode} } }() wg.Add(1) go func() { defer wg.Done() for node := range nqueue { imap := indexs[node.inum%uint64(shard)] if imap != nil { imap.index[node.inum] = node.inode } else { equeue <- errors.New("no corresponding index shard") return } } }() wg.Wait() select { case err := <-equeue: close(equeue) return err default: close(equeue) return nil } }在这段代码 recoveryIndex 函数中，有两个 goroutine 需要相互配合完成任务，并且要求任意一个 goroutine 发生错误时，另一个能够及时感知并同步退出，目前采用的实现方式是通过一个名为 equeue 的 channel 进行协调，并通过 for 循环的条件进行控制：for offset < int64(reader.Len()) && len(equeue) == 0 { ... }[scode type="red"]该实现表面上看是可行的，但在实际运行过程中会暴露出明显的并发问题：产生的错误无法可靠、及时地在 goroutine 之间传递；另一方面在错误发生或消费不及时的情况下，可能因为 equeue 缓冲区已满而导致某个 goroutine 阻塞，进而引发 goroutine 泄漏或整体流程无法正常退出。[/scode][scode type="yellow"]与这种非结构化并发相对应的，是结构化并发的程序结构设计；结构化并发的核心目标是：让并发任务的生命周期、错误传播与取消语义，严格受控并与代码结构保持一致。[/scode]在当前工业级实践中，最成熟、最系统化的方案来自新版本的 Java 语言，JavaSE 21 版本引入的 StructuredTaskScope 为结构化并发提供了语言层面清晰、可组合且可推理的 API 设计，使并发代码首次具备了类似顺序代码那样的可读性与可维护性。例如下面这个基于 JavaSE 21 的结构化并发示例：void serve(ServerSocket socket) throws IOException, InterruptedException { try (var scope = new StructuredTaskScope<Void>()) { try { while (true) { var conn = socket.accept(); scope.fork(() -> handle(conn)); } } finally { // 如果发生异常或线程被中断，停止接收新连接 scope.shutdown(); // 主动取消所有仍在运行的子任务 scope.join(); // 等待子任务有序结束 } } }访问控制与构造函数在编写项目中最常见的就是把基础标量类型封装为一个 struct 类型结构体，例如下面代码是一段实现了 gossip 协议所需要的代码，分别为 Node 和 Cluster 结构体，这两个结构体要提供外部的包进行调用和访问。这两个结构体的字段都是大写开头，这是由 Go 访问控制权限规则设计要求的，但是在某些需求下为了不让外部包的调用者能直接通过 new(Node) 和 &Node{} 构造器（准确来说没有传统 class 的构造器）来创建 Node 类型的实例，只能通过 gossip.NewNode() 静态工厂函数进行实例化一个节点，因为 gossip.NewNode() 内部会提供一些默认的初始化规则。在这种场景下只能使用下面的方式来封装一个 innerNode 类型，将其嵌入到 Node 中，这样就可以避免其他人滥用 Node {} 创建，代码如下：type ( // 类似于私有化 Node {} 构造器，其他包就不能直接 {} 来创建 Node 实例 // 只能通过 gossip.NewNode() 进行实例化一个节点 innerNode struct { ID string // 节点唯一标识 Address string // 节点地址 Heartbeat int // 心跳计数 Timestamp int64 // 最新心跳时间 Alive bool // 节点状态 Hash uint32 // 节点哈希值，用于一致性哈希 } innerCluster struct { Neighbors map[string]*Node // 所有已知节点的信息 Self *Node // 本节点信息 Mutex sync.Mutex // 保护 Nodes 的并发访问 Interval time.Duration // 心跳间隔 Timeout time.Duration // 失效阈值 HashRing []*Node // 一致性哈希存储范围的键 } // Node 协议集群中的节点 Node struct { innerNode } // Cluster 协议的集群集合 Cluster struct { innerCluster } ) // NewNode 只能使用此暴露外部的函数进行创建 func NewNode(id, addr string) *Node { node := &Node{ innerNode{ ID: id, Address: addr, Heartbeat: 0, Timestamp: time.Now().Unix(), Alive: true, }, } // 通过节点 ID 算出在哈希环中的值 node.Hash = NodeHash(id) return node }[scode type="yellow"]在这段代码逻辑中存在一个问题，就是由于 struct 中的字段都是以小写开头，导致如果需要对此 Node {} 这个结构体进行 JSON 正反序列化，就会出现无法进行序列化的情况，原因就是因为上面的需求导致的，导致一个很矛盾的问题。要解决这个序列化问题，可以让要被序列化的 struct 实现自定义的 MarshalJSON 方法，这种方式要额外的编写正反序列化的代码实现，另外就是无法被反射编程动态修改其值。[/scode]封装和扩展限制final 、sealed 密封类。目前我个人认为 Go 的优点在于语法简单甚至略显简陋，同时保留了指针的特性。Go 的源代码可以直接编译为原生机器码，能够在计算机上直接运行，这带来了快速冷启动和低内存占用的优势，在 Cloud Native 场景下，这种静态编译特性确实非常适合。然而这并不意味着 Go 在所有方面都能与基于 VM 的语言相比。例如 JVM 提供了强大的运行时反射能力，支持动态加载和卸载 Class 字节码，对于业务开发者来说，JVM 能够实现动态类的 AOP 或动态注入 DJ，这种能力在某些场景下尤其方便。举例来说，在金融或银行系统中，可以通过自定义 Class Loader 从 JDBC 获取字节码并动态加载，从而增强程序的安全性和保密性。Go 虽然也提供一定的反射能力，但总体上并不如 Java 灵活，设计一门编程语言，本质上就是一种权衡：语言在某些特性上取舍了便利性或性能，必然会在其他特性上有所牺牲，如何在性能、安全、易用性和灵活性之间保持平衡，是语言设计者面临的核心问题。许多人在决定入门一门新的编程语言或者为新项目选择技术栈时往往会陷入纠结，甚至最后讨论变成了“语言之争”。这种争论通常围绕一些抽象的概念展开，例如 “某某语言设计很优雅” “某某语言语法很糟糕” “这门语言不适合写大型系统” 等等，但这些讨论往往缺少关键的一点实践和实际的使用体验来做论点的有力支撑，或者哪些程序员就只会某门编程语言...我这篇文章是结合实际的开发遇到问题所编写而成，让读者明白不同设计之间差异在哪里，好根据自己情况做选择。
在现代社会网络通信的隐私和安全性已成为人们日益关注的重要话题，无论是浏览购物网站、在线银行交易，还是日常的社交媒体互动，我们的个人信息都面临着潜在的风险和威胁。在这篇文章中，将探讨在浏览购物网站时，如何保护我们的隐私和确保信息安全，是谁在保护我们的数据隐私和通信的安全性。在谈论网络通信的安全性之前，让我们回顾一下人类在通信保密性的发展史。通信的保密性自古以来就是一个重要的问题，人类不断发展新的方法来保护信息的隐私和安全，最早的加密技术可以追溯到古代文明，古希腊和罗马使用了各种简单的加密方法，如凯撒密码（Caesar Cipher），其中每个字母被替换为其后固定位置的字母。凯撒密码也称为移位密码（Shift Cipher），是以古罗马将军 Julius Caesar 的名字命名的，据说凯撒在军事通信中使用这种密码来加密敏感信息。在战争期间使用这种加密方法来保护军事命令和机密信息，确保即使信息被截获，敌人也无法轻易破译，凯撒通常使用 N <= 25 位移位（即每个字母向后移 N <= 25 个位置）来加密消息。当消息被敌军获取了，敌军在不知道消息加密的方式情况下，只能得到一堆胡乱排列的字母。凯撒密码的数学公式为如下：凯撒密码的核心原理是通信的双方有一个固定字母顺序表，以这个固定顺序的字母表作为消息加密的基础，通过将原本的消息进行移动位置的方式进行加密。凯撒密码易于破译，尤其是使用频率分析法，通过统计文本中各个字母出现的频率，可以很容易地推测出移位量并解密消息。比如，在加密文本中可以通过寻找加密文本中最常出现的字母来推断移位量，当找到这个字母偏移量就很容易破解出来密文信息了。[scode type="blue"]下面是通过 Java 自定义码表实现凯撒密码加密和解密逻辑：[/scode]public class CaesarCipher { private static final char[] ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); public static void main(String[] args) { String text = "HELLO"; int shift = 3; String encrypted = encrypt(text, shift); String decrypted = decrypt(encrypted, shift); System.out.println("Original: " + text); System.out.println("Encrypted: " + encrypted); System.out.println("Decrypted: " + decrypted); } // 加密函数 public static String encrypt(String text, int shift) { StringBuilder result = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (Character.isUpperCase(ch)) { // 拿到对应字母的下标 int originalPosition = findPosition(ch); // 找到偏移量之后字母下标，A 至 Z 之间循环 int newPosition = (originalPosition + shift) % ALPHABET.length; result.append(ALPHABET[newPosition]); } else { result.append(ch); // 非字母字符不变 } } return result.toString(); } // 解密函数 public static String decrypt(String text, int shift) { StringBuilder result = new StringBuilder(); for (int i = 0; i < text.length(); i++) { char ch = text.charAt(i); if (Character.isUpperCase(ch)) { int originalPosition = findPosition(ch); int newPosition = (originalPosition - shift + ALPHABET.length) % ALPHABET.length; result.append(ALPHABET[newPosition]); } else { result.append(ch); // 非字母字符不变 } } return result.toString(); } // 查找字符在自定义码表中的位置 private static int findPosition(char ch) { for (int i = 0; i < ALPHABET.length; i++) { if (ALPHABET[i] == ch) { return i; } } return -1; // 应该永远不会发生 } } 这种古代的加密方法，在现代计算机面前已经变得非常脆弱，凯撒密码只是在字母表上进行简单的移位操作，加密之后的密文与明文没有本质上的区别，只是信息的错位混淆处理。本质上密文依然是明文，现代计算机可以轻易地编写程序，快速破解这样的简单加密方式。现代的程序员在存储用户密码时，通常使用 MD5 或者 SHA-1 等不可逆的哈希函数进行存储。这些哈希函数将密码转换成固定长度的哈希值，使得即使密码被窃取，也很难通过哈希值逆推出原始密码，这种方法存储的才是真正的密文。通过将要传递的信息和语言文本信息通过特殊手段进行加密存储，消息在传递过程中能够达到通信完整性和机密性。实际上，这就是一种消息编码和解码的规则。类似于人类语言中使用中文交流的人，听不懂或看不懂用法语表示的语言信息，反之亦然。出现这种情况的原因是缺乏对方语言的编解码器。在计算机通信中，加密和解密的过程也是类似的问题。[scode type="yellow"]如果两台计算机要通过网络进行安全通信，首先需要设计出一套双方能够理解并使用的编解码程序，任何第三者计算机都无法破解或知道编解码的规则。这是保证通信机密性和安全性的前提条件。只有在这个前提条件成立的情况下，通信双方的安全性才能得到保障。总结一下两台计算机在不可靠网络链通信要面临的 3 个问题：如何确认对方真实身份？如何保证数据不能被截获篡改？如何保证数据机密性？这是整个网络通信要面临和解决的问题？从这 3 个问题提取出来的，要保证通信安全的 3 个核心要素：机密性、完整性、可用性 ，换一种说法就是：防窃听 、防篡改 、防伪造 ，满足这些条件才能保证通信隐私安全性。[/scode]对称加密在上面的 Caesar Cipher 故事中使用的是传统对称加密方式，只要密钥不被公开或被黑客窃取就能保证安全性。在现代的计算机中通常都是程序员或者用户设置一段固定长度字符串作为密钥，或者在程序运行期间动态生成一段随机字符串作为加密使用的密钥，这个密钥将被用于针对明文的数据块进行加密。常见的加密算法有 AES 和 DES 算法，可以看作为是一个接收明文数据和密钥返回密文特殊函数，数学函数公式表示为 C=E(P,K) ，对称加解密过程如下图：对称加密一般用于单体的应用或者本机运行的程序中，密钥一般都被存储在运行时的程序内存中，但也需要采取措施保护这些密钥。例如使用硬件安全模块（HSM）或者操作系统提供的安全存储机制（如 Windows 的 DPAPI 或 macOS 的 Keychain）来保护密钥。再者不要长时间使用一个固定的密钥，定期更换密钥，减少单个密钥被破解或泄露后造成的影响，实现自动化的密钥轮换机制，并确保旧密钥的数据能够正确迁移到新密钥上。非对称加密传统对称加密在密钥管理时会遇到如何防止加密密钥泄露的问题，导致对称密钥泄露的原因有很多，例如：密钥过于简单被黑客使用彩虹表攻击；或者将密钥明文硬编码在程序中，当程序被打包为二进制就连密钥一起被打包发布出去了，这样黑客可以通过反汇编技术查看到密钥。[scode type="yellow"]另外一个问题就是非对称加密如果用于在互联网上的两台不同计算机通信，会出现一个问题，两台要进行通信的计算机要协商一个用于数据加密的密钥。如果密钥由互相通信两台计算机中一台生成，那么密钥就要通过网络先发送给另外一台计算机，但在互联网上传输的密钥是明文文本内容，在传输的过程中很有可能被其他的恶意的计算机所截获到密钥，此时就失去了加密通信的意义。[/scode]要建立安通的通信通道就要解决：机密性、完整性、可用性，这 3 个问题。目前计算机网络 OSI 标准中的会话层 TLS（Transport Layer Security）协议，它就是基于非对称加密来保证通信安全性。该协议的前身是网景公司的浏览器为了解决 HTTP 协议在通信过程中明文传输数据时，容易造成数据泄露的问题所开发出的 SSL 协议 （Security Socket Layer）。TLS 协议通过使用对称加密和非对称加密、数字证书和公钥基础设施（PKI），为应用层提供了安全的会话通信机制，开发者在实现安全通信时，通常可以直接利用现有的 TLS 实现来确保通信的安全性。TLS 协议是 TCP/UDP 传输层的安全扩展，旨在确保传输层的数据传输安全。TLS 协议涉及到很多关于密码学和数学相关知识，它基于 RSA（Rivest-Shamir-Adleman）加密算法设计开发的，和传统对称加密解密所使用同一个密钥不同，它需要两个不同的密钥互相配合才能完整加解密的工作，分别为公钥和私钥，工作原理如下图：非对称加密的密钥是一对密钥，由公钥和私钥组成，公钥用于加密数据，生成的密文只能通过对应的私钥进行解密，反之亦然。公钥和私钥之间存在数学上的强绑定关系，确保数据的加密和解密过程是安全。具体运作过程，公钥可以公开分发，而私钥则必须严格保密，以保证只有合法的私钥持有者能够解密通过公钥加密的数据，从而确保数据的保密性、完整性和发送者的身份认证。TLS 的工作原理是首先使用非对称加密技术进行密钥交换，协商出一个用于当前会话的对称加密密钥，随后通信的数据报将通过这个对称密钥进行加密和解密。这样 TLS 协议不仅确保了数据的机密性，还有效地防止了中间人攻击在密钥交换过程中的威胁。TLS 依赖于非对称加密体系，在非对称加密体系中有一个严重容易忽略的问题就是，一对密钥中公钥都公开的，任何人都可以获取得到对方的公钥和使用对方公钥来加密数据发送给对方，这就使得任何人都可以冒充另外一个人的身份来使用公钥，对于解密方来说无非确定是谁使用了自己的公钥。数字签名为了确保通信数据的机密性，首先必须建立信任关系。在 TLS 协议中，使用了非对称加密的 RSA 算法，解决了加密与解密过程中的密钥分发问题。然而，尽管非对称加密在密钥传输上具有优势，它并没有解决公钥可能被伪造或冒充使用的问题。为防止这一风险，TLS 协议通常结合证书颁发机构（CA）来验证公钥的合法性，确保通信双方的身份真实可信。信任关系的建立是保障通信安全的前提，要建立通信双方的信任关系，就要解决公钥公共使用的问题，自己公钥被谁使用了？接收到的数据包是谁使用了自己公钥？解决问题这个方法就是使用数字签名，数字签名原理和日常生活中的纸张上盖章一样。下图为数字签名工作原理：[scode type="yellow"]实现使用不可逆向哈希函数对要发送的数据包进行哈希运算，生产这个数据包内容的摘要信息，也就是对应的哈希码。再使用自己的私钥对摘要信息进行加密，生成对应的密文（数字签名），将其和自定的公钥、数据文件内容打包发送给对方。对方收到数据包取出数据包中的公钥，对数据包中密文（数字签名）进行解密，获得到数据包内容的摘要信息，再使用相同哈希函数对数据包的内容进行哈希运算，得到哈希码，通过比对数字签名的摘要哈希码和本地运算计算出的哈希码来确定数据包真实性和完整性。[/scode]CA 签发证书通过非对加密的私钥和哈希函数对数据进行数字签名，可以解决数据文件归属和署名的问题，接受者可以通过数字签名来确定发送者是谁，但随着而来的一个另外问题是如何获取对应的非对称加密公钥，在验证数字签名过程中需要获取公钥。上面的步骤中将公钥和数字签名、数据文件，打包一起发送给接受者，一个明显的问题是公钥匙明文存储的，且也没有被加密，容易导致数据包中间人截获之后调包更换了公钥。[scode type="yellow"]解决公钥获取最简单一个方式是使用数字签名同样对数据包中的公钥进行签名，公钥使用者在使用公钥之前进行签名验证来确保公钥的安全性。通过对公钥也进行数字签名来解决身份问题，但这样做问题又回到信任问题上，因为整个数据包都是在互联网上传输，也有可能遭受到中间人调包到风险。那如何解决这个问题呢？在现在互联网基础设施中引入了一个公共的公钥签发中心，它可以来管理所有人的公钥，以及确认公钥持有者的身份信息，确保持有者是合法合规的。CA 证书验证中心类似于现在社会中的政府设置安全部门，例如大家信任银行的安全性，都把钱存储在银行里，银行保管着用户存储的钱；通过信任 CA 用户可以信任与其相关的数字证书和公钥，从而建立起安全的通信链路。[/scode]上图为通过权威的 CA 认证证书签发中心颁发任何人的公钥的工作流程。用户首先将自己的非对称密钥的公钥和自己身份信息提交给 CA 中心，CA 中心会对公钥所有者身份进行校验，以确保公钥确实是属于所有者。在 CA 确认了公钥所有者的合法身份之后，会使用自己的私钥对提交者的公钥进行数字签名，并且打包为数字证书文件。当有人需要和公钥所有者通信的时候，只需要通过 CA 来获取被通信者的证书，可以通过 CA 颁发的证书来鉴别公钥，再使用 CA 的公钥对数字证书的内容进行数字签名合法校验，通过验证确认了身份，即可使用非对称加密进行网络通信。通过 CA 认证的证书签发中心就可以完全避免通信双方密钥的配送遭到中间人劫持的问题，一个完整的 CA 签发证书上面包含了 CA 信任链以及使用的签名算法，证书所签发的日前和终止日期，证书所在地球的国家和组织名称。如果当发现私钥泄露的问题了，可以考虑通过 CA 认证中心链来吊销相关的证书[scode type="yellow"]CA 签发的证书只能解决通信双方的身份认证问题，不能保证整个通信信道的安全性，解决这个通信信道安全性问题就要回到上面公钥和对称加密的混合密码体系了。[/scode]通过整合多种密码学技术（如混合加密、数字签名、单向散列和 HMAC）来实现 TLS 网络协议，确保了网络通信的机密性、完整性和身份验证能力。TLS 协议保证从通信双方的端系统从连接建立到通信会话结束整个过程中的安全性，即浏览器和服务器通信所有的数据包在不可信通信链路是经过加密处理的。使用了 TLS 协议进行加密通信，并不意味着就能掉以轻心，这个取决于使用者的习惯和服务器端的维护者如何管理这些客户端发送过来的数据；用户可能使用被二次编译修改过源代码的浏览器，这些浏览器可能就被黑客植入了一些木马后门程序；另外就是当这些数据在服务器端如何保存，没有妥善的保管这些数据，黑客也可能从服务器端漏洞进行窃取这些数据，从而导致数据外泄，这样即使用了 TLS 协议也无法保证敏感性数据不外泄的风险。
Java 这门语言与生俱来的显著特性就是“一次编译，到处运行”，这种功能得益于 JVM 平台的支持，Java 程序通常通过将其打包为 JAR 或 WAR 包，并依赖 JVM 和 Servlet 容器来运行。其底层运行时 JVM 采用 JIT（即时编译）模式来执行程序代码，JVM 会在运行时进行编译优化和动态执行代码，这通常会导致较高的内存占用。这样的好处是采用 JIT 可以热更新和热部署程序，并且 JVM 可以在运行期间对程序进行动态分析，来实时优化程序以达到最好的性能状态。Cloud Native 面临的问题随着云计算的发展，服务端程序的运行和部署方式发生了巨大变化，特别是在云原生（Cloud Native）场景下，服务器程序被打包成一个个容器运行。在这种环境下，传统方式容器化的 Java 程序面临着打包后的镜像文件过大的问题。镜像文件中需要包含 JRE 和各种第三方库，导致镜像体积庞大，占用更多的磁盘空间。目前云计算的计费方式也发生了改变，采用按需分配的实时计费方式，根据程序的内存占用和运行时间计费。这与传统 IDC 自托管式机房中长时间运行服务器程序形成鲜明对比，按需分配的计费方式显然更具性价比，因此优化 Java 程序在容器环境中的运行方式变得尤为重要。Serverless 面临的问题传统的 JVM 工作原理要求程序员先将编写的 Java 程序打包为 JAR 或 WAR 包，这是普通 Java 开发者所能接触到的程序编译和构建阶段。然而当程序运行起来之后，底层的 JVM 会进行许多复杂的操作，包括内存管理、垃圾回收、即时编译等。需要先启动 JRE 中的 JVM 来加载 Tomcat 服务器所需的字节码，再加载 WAR 包中的字节码文件到 Servlet 容器中运行服务，这个过程非常耗时。下图为整个传统 Java 程序从源代码到程序运行各个阶段的工作流程：下图为传统的 Java 运行时执行程序的生命周期，首先应用的开发者将 *.java 源代码编译为 bytecode 和 jar 依赖包通过 maven 构建打包为一个单独的 jar 包。当执行 java -jar xxx.jar 命令时，首先要启动 JRE 中的 JVM 程序，因为 JVM 本身也是一款单独程序对应着 $JAVA_HOME\bin 目录中的 java ，当 JVM 启动之后会在系统中创建一个进程，将要运行的 jar 程序包和 JRE 标准库中 class 文件加载入 JVM 内存中，并且找到 main 方法开始解释执行。当程序运行一段时间之后 JVM 充分获取了 Java 程序的一些执行数据信息，开始对程序进行 JIT 及时编译优化程序，当程序被 JIT 优化之后的程序才能达到和 C/C++ 这种直接编译为 Native Code 程序的性能。在云计算推崇的 Serverless 架构场景下，Java 的 JIT 模式并不适合，Serverless 应用程序需要能够快速启动以响应事件驱动的请求，并且在空闲时自动关机销毁，释放在服务请求期间所使用的内存。然而，这种需求与传统的 Java 程序运行模式背道而驰，使得 Java 在 Serverless 场景中显得不适用，这就是被业界称为的 冷启动 问题。为了解决这一痛点，Oracle Labs 推出一款新的 JDK 产品 GraalVM JDK ，相比传统的 JDK 该产品内置 Java 静态编译工具和一款新的虚拟机，和传统的 JVM 不一样 GraalVM 是支持多语言进行混合编程的虚拟机，这也是它一大亮点。GraalVM 中的静态编译工具不仅是支持将 Java 编译为二进制可执行文件，还支持 JavaScript 和 Python 等其他语言的编译优化功能，本文只关注于 GraalVM 对 Java 程序进行 AOT（Ahead-Of-Time）编译的解决方案。使用 GraalVM 对 Java 程序进行 AOT 编译的解决方案，可以显著缩短启动时间并降低内存占用，使 Java 程序更适合在 Serverless 环境中运行，通过 AOT 编译，编译器生成一个 Native Code 二进制文件，即对应平台架构的机器代码程序。这种方式无需像 JIT 那样依赖于 JVM 运行，在内存和磁盘占用上有显著改善。下图为 AOT 编译工作流程，在编译阶段会程序进行静态分析，通过它内置工具分析 Java 源代码中依赖关系，将其所有依赖和代码执行逻辑都进行提前编译为机器代码，缺点也很明显可能会失去在 JIT 模式下的动态反射功能。如上图，在 AOT 模式下将在运行时的过程放到在程序构建阶段，构建阶段会对 Java 程序执行静态代码分析和依赖可达性分析，将其所有的依赖软件包都编译生产所属平台对应的可执行二进制文件。AOT 编译优化对程序进行 AOT 编译特别适合云应用程序，被 AOT 编译优化之后的程序启动速度足够快，从而缩短启动时间并更直接地水平扩展云服务，内存占用相比 JVM 模式下要少很多，这对于在云中运行的容器初始化的微服务尤其有益。AOT 前提是完全封闭的运行空间，因为它消除了各种代码注入的可能性，例如 2021 年震惊网络的 Log4j 漏洞就是由于利用了 Java 中的动态类加载机制而发生的，在 AOT 编译之后程序就不会发生类似于漏洞。下面这张蜘蛛图详细展示了不同程序运行方式的区别：介于 Java 程序部署平台和主流的容器技术都是基于 Linux 系统，所以这里以 Ubuntu 发行版本作为基础 AOT 编译和运行打包环境。要对 Java 程序进行 AOT 编译的话首先要安装 GraalVM JDK 的发行版本，这个可以去它的官方网站 graalvm.org 上下载和安装，安装步骤和传统基于 OpenJDK 的发行版本步骤一致。在 Linux 中要是使用 GraalVM 提供的 AOT 静态编译功能，首先需要在操作系统中安装一些 C/C++ 编译器的工具链，在 Ubuntu 和其他基于 Debian 的系统中执行下面命令：sudo apt-get install build-essential libz-dev zlib1g-dev其中的 build-essential 是一个用于 C 语言的开发包，包含了 gcc make gdb 和 libc 函数库等很多工具，这些工具和库对于编译 C 和 C++ 程序是必不可少的。libz-dev 和 zlib1g-dev 是用于压缩和解压缩的开发库，这两个软件包提供了开发 zlib 库的头文件和开发工具，允许您在编写 C/C++ 程序时链接和使用 zlib 压缩库，GraalVM 的 AOT 编译依赖于这些工具来构建本地映像。默认情况下，GraalVM 的 AOT 编译在链接时通常使用系统的标准 glibc 库，但在小型嵌入式设备或对内存要求极低的环境中，glibc 并未针对这些场景做出特别优化。相比之下目前有另外一种专有的 musl 标准库实现，musl 被开发出来就是专注在内存受限的设备上表现优异，使用 musl 生产的二进制文件通常比使用 glibc 生产的文件体积更小，所以更推荐直接使用 musl 替代 glibc 去做编译链接优化。在 GraalVM 的 AOT 编译工具中提供自定义选项，允许开发者去自定义使用 musl 去做生成二进制文件优化。部分 Linux 发行版本系统可能没有内置 musl 库需要提前安装，通过源代码安装的方式可以去 musl.libc.org 官方网站下载源代码包编译安装。但这种方式的需要自己手动配置和编译步骤比较繁琐，建议使用下面这种方式，通过别人已经编译好的 x64 位的 x86_64-linux-musl-native 程序，下载压缩包到指定位置就可以使用。基于 musl 编译的前置条件是依赖于 musl 工具链，但在 GraalVM JDK 中并未提供相关的工具链的支持，需要提前下载 x86_64-linux-musl-native 和 zlib 文件，创建一个用于存放 musl 工具链的目录，将其解压到这个目录中，并且将 x86_64-linux-musl-gcc 和 zlib 进行整合编译安装，步骤如下：# 创建一个工具存储目录 mkdir /usr/local/graal-aot-tools # 进入到这个目录 cd /usr/local/graal-aot-tools # 下载 x86_64-linux-musl 工具链 wget http://more.musl.cc/10/x86_64-linux-musl/x86_64-linux-musl-native.tgz # 解压下载到的 musl 工具链包 tar -zxvf x86_64-linux-musl-native.tgz # 复制文件到工作根目录 cp -r x86_64-linux-musl-native/* . # 下载 zlib 依赖 wget https://zlib.net/current/zlib.tar.gz # 解压 zlib 依赖源码包 tar -zxvf zlib.tar.gz通过一条特定 TOOLCHAIN_DIR 环境变量来指定 musl 工具链安装路径，graalvm 的 aot 编译器会通过此变量来找到 x86_64-linux-musl 的位置：export TOOLCHAIN_DIR="/usr/local/graal-aot-tools" export PATH="$TOOLCHAIN_DIR/bin:$PATH" export CC="$TOOLCHAIN_DIR/bin/gcc"最后把 zlib 和 TOOLCHAIN_DIR 整合进行编译安装，进入 zlib 源代码安装目录中执行： ./configure --prefix=$TOOLCHAIN_DIR --static make make install配置完成之后可以通过 x86_64-linux-musl-gcc 命令检查 musl 工具链是否安装成功。如果使用的是 maven 来构建自动化项目，可以使用 pom 文件中配置 AOT 相关构建参数，来自动化来构建二进制文件： <plugin> <groupId>org.graalvm.buildtools</groupId> <artifactId>native-maven-plugin</artifactId> <version>${native.maven.plugin.version}</version> <extensions>true</extensions> <executions> <execution> <id>build-native</id> <goals> <goal>build</goal> </goals> <phase>package</phase> </execution> </executions> <configuration> <skip>false</skip> <imageName>${bin.file.name}</imageName> <mainClass>${app.main.class}</mainClass> <buildArgs> <buildArg>--gc=G1</buildArg> <buildArg>--static</buildArg> <buildArg>--libc=musl</buildArg> <buildArg>--no-fallback</buildArg> <buildArg>-march=compatibility</buildArg> </buildArgs> </configuration> </plugin>在项目的根目录中执行 mvn native:build 命令就可以调用位于 $JAVA_HOME/bin/native-image 工具执行静态分析并且生成二进制可执行文件。AOT 编译对计算机硬件资源的要求非常高，取决于项目的大小，大型项目编译时需要占用很多 GB 的内存和大量的 CPU 使用率，不过这会使用高性能的 CI/CD 服务器来完成自动化构建工作。通过 x86_64-linux-musl-gcc 和 musl 做链接编译生成的二进制可执行文件，完全使用的是静态链接的方式，从而提高了可移植性。在不同的 x86_64 的 Linux 系统上运行这些静态链接的二进制文件时，不需要任何额外的库，这对于部署和分发二进制文件非常有用。从编译日志输出可以看到，AOT 编译器启用了 PGO: ML-inferred 优化，这个默认在其他平台是不会启动的，只要在 Linux 平台下使用 x86_64-linux-musl-gcc 才会开启的，由于使用的完全静态链接的二进制文件，所以可以使用 upx 针对这个二进制文件进行压缩，压缩之后的二进制文件可以小到 3 MB 大小。这对于将其打包为 docker 容器镜像文件非常有利，意味着不需要任何其他基础镜像，直接打包可执行二进制文件放到容器中执行，基于 scratch 基础镜像打包之后镜像大小和二进制文件大小一致。# 使用一个轻量的基础镜像 FROM scratch # 复制静态链接的二进制文件到根目录 COPY target/vasedb / # 设定工作目录，这是可选的 WORKDIR / # 默认的启动命令 CMD ["/vasedb"]采用 AOT 和 JIT 模式通过这些数据，形成鲜明的对比，优势也展示出来了，以往采用 JIT 模式运行的 Java 被容器化之后镜像大小往往都是上百 MB 大小的。个人认为有 Serverless 更激进优化方案，在 AOT 编译之后程序会内嵌入一个微型的 SubstrateVM 运行时，这个运行时负责 Java 程序在运行阶段一些多线程和内存分配、垃圾回收功能，默认 AOT 编译使用的 serial 垃圾回收器是一种单线程垃圾回收器，适用于单线程应用或有少量线程的应用，另外一个可选的垃圾回收器是 G1 垃圾回收器，G1 用于多核处理器和大内存机器的高性能垃圾回收器。但是在 Serverless 应用场景中往往应用程序运行时间很短，在程序在运行一段时间之后就主动销毁了，这有点类似于在 AOT 编译阶段的编译程序本身步骤，当编译器编译完程序就销毁了，是一种一次性应用程序。熟悉 JVM 多个垃圾回收器实现应该知道其中有一个叫 Epsilon 垃圾回收器，该垃圾回收器不会执行任何垃圾回收操作，它只进行内存分配而不释放内存。所以我个人认为在某些特定的 Serverless 应用场景中完全可以使用 Epsilon 内存分配器的方案，因为部分场景程序运行时长还没有等到 GC 回收器触发的时间点就被底层基础设施销毁了，程序运行所分配的内存会被 OS 主动回收，而不需要执行垃圾回收器进行回收，去掉 SubstrateVM 中的垃圾回收器部分，打包应用程序体积会更小。引用资料文件Oracle (The Art of) (Java) Benchmarking PPTAOT vs. JIT Compilation in JavaGraalVM Native Image 快速入门
在 MySQL 8.x 带来了许多新特性和改进包括 JSON 数据类型和 Java Script 作为 JavaScript Shell 支持，可以直接使用 JavaScript（JS）作为数据库查询语言或存储过程的语言，更好的性能和安全性，例如连接过程中出现的 Public Key Retrieval is not allowed 错误提示，这些都是新版本特性。本文将记录完整的基于 Ubuntu 最新版本中安装 MySQL8.x 版本的过程。安装和配置在安装 MySQL8.x 之前可以尝试先更新一下操作系统和软件包，执行下面命令可以更新：sudo apt update && apt upgrade -y更新系统和软件包版本需要一点时间，这个过程取决于你网络和所使用的 apt 软件包数据源地址，数据源配置文件路径 /etc/apt/sources.list。[scode type="yellow"]如果不确定系统中是否安装其他版本的 MySQL 版本，可以先执行清理命令。清空完成之后，提供 apt search mysql 包管理工具搜索最新的 MySQL8.x 软件安装包，最后通过 apt install 执行进行安装。[/scode]sudo apt remove mysql* sudo apt autoremove sudo apt autoclean清空完成之后提供 apt 所提供的 MySQL 软件包进行安装：sudo apt search mysql sudo apt install mysql-*安装完成之后，可以使用 sudo mysql_secure_installation 来初始化 root 用户的密码，这是 MySQL 新版本所提供的初始化数据库程序：sudo mysql_secure_installation[scode type="yellow"]如果需要让 MySQL 服务器能被远程的用户提供客户端访问，需要配置修改服务器所绑定监听的 ipv4 地址，默认是本地环路地址，在这个 /etc/mysql/mysql.conf.d/mysqld.cnf 中找到 bind-address 的值修改它为 0.0.0.0 即可。[/scode]# changed. Ref https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_tmpdir # tmpdir = /tmp # # Instead of skip-networking the default is now to listen only on # localhost which is more compatible and is not less secure. bind-address = 0.0.0.0 #这一行 ✅ mysqlx-bind-address = 127.0.0.1通过上面的配置目前 root 还是只能通过本机的客户端进行连接，有远程登录需求的可以配置一个远程用户并且允许远程登录，并且设置初始化密码，我这里开放的是 root 用户，执行下面命令：GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED WITH mysql_native_password BY '新密码' WITH GRANT OPTION;解决安全连接问题在 MySQL8.x 版本中如果要使用正常的密码连接到 Server 中连接过程中出现的 Public Key Retrieval is not allowed 错误提示，这个是新特别特意到安全功能增强，如果要使用这种方式连接必须要配置 SSL/TLS 密钥对进行连接。[scode type="yellow"]指定用户使用的密码验证方法 mysql_native_password ，这让 MySQL 使用的默认身份验证插件，它支持基于用户名和密码的常规身份验证机制，执行下面命令使用默认密码验证方式进行连接：[/scode]ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'new password';执行上面的 SQL 之后，刷新一下 MySQL 授权表：flush privileges;最后记得在服务器上允许开放 3306 端口数据包的连接。
在我的 Java 编程生涯中使用过很多 Java 开发环境，从编写第一个 Java 程序使用的 Notepad++ 到 Eclipse 再到 IntelliJ IDEA。能编写 Java 的 IDE 产品有很多，如何选择一个好的 Java 开发环境？这对于从事 Java 程序设计的程序员来说是一个重要的问题。选择一个合适的 IDE 不仅能提高开发效率，还能减少调试和排错的时间。本篇文章将分享一些选择 Java 开发环境的关键考虑因素，并介绍如何通过 WSL + VSCode 打造一个混合开发基础 IDE 环境来开发 Java 程序。从事 Java 程序开发的程序员主流都是做服务器端程序开发。在编写程序项目源代码阶段，通常使用 Windows 系统的 PC 进行开发工作，而当程序部署上线之后，往往运行在 Linux 系统环境中。这就要求我们不仅要熟悉 Windows 系统，还要熟悉 Linux 系统，才能更好地编写和调试 Java 程序。这也是选择开发环境时需要考虑的一个重要因素，因为一个好的 IDE 应该能够方便地在不同操作系统之间切换，并提供良好的跨平台兼容性。目前主流的 Eclipse 和 IntelliJ IDEA 都是重量级的 Java 集成开发环境，并且针对不同系统架构开发了对应的版本。程序员在 IntelliJ IDEA for Windows 版本下开发，就必须使用 Windows 下的一些开发和调试技巧，而在 IntelliJ IDEA for Linux 中又需要掌握一些 Linux 下特有的功能和设置。那么有没有一种折中的解决方案，可以在 Windows 开发 Java 程序并且能获得 Linux 系统中的一些开发体验和特有功能呢？答案是有的。[scode type="green"]使用 Microsoft 推出的轻量级 IDE 产品 VSCode 和最新版本 Windows 系统所提供的 WSL（Windows Subsystem for Linux）功能就可以实现这种开发模式。我个人认为此种开发方式是更符合现在 Java 程序开发的方式，虽然 JVM 为 Java 程序提供强大跨平台能力，能让 Java 程序一次编写到处运行，但在开发阶段会不可避免的使用所在的开发机上特有命令和功能。对于需要跨平台开发和调试的需求，使用 VSCode 与 WSL 的组合可以在 Windows 环境中提供类似 Linux 的开发体验，能在够让 Windows 和 Linux 系统之间文件和命令互相操作，这种方式不仅简化了环境切换还能提高开发效率，目前最新版本的 IntelliJ IDEA 的 UI/UX 设计越来越像 VSCode 了，所以个人还是更推荐 VSCode 作为基础的 IDE 产品。[/scode]基础 WSL 配置使用 WSL 和 VSCode 作为基础开发环境，最低要求操作系统为 Windows 10。如果使用 Windows 11 则更为理想。在 Windows 11 系统中，WSL 功能已默认开启，但要正常使用还是需要一点额外的操作。WSL 本身不提供完整的 Linux 系统功能，只是提供了一个与 Linux 兼容的内核接口层和虚拟化技术在轻量级虚拟机 VM 中运行 Linux 内核，再配合其兼容层的功能使用户能够无缝结合使用 Windows 和 Linux 的功能。[scode type="yellow"]默认情况下，WSL 并未内置 Linux 系统镜像，需要开发者自行选择和安装相应的 Linux 发行版。[/scode]可以通过在终端中输入以下命令来查看 WSL 官方提供的 Linux 发行版：# 查看能安装的 WSL Linux 版本 wsl --list --online通过列出的版本信息，建议使用默认的 Ubuntu 即可，按照指定的 Linux 发行版本到 Windows 11 系统中：# 指定对应的 Linux 版本进行安装 wsl --install -d Ubuntu通过以上命令就可以安装 Linux 发行版本到 Windows 系统中，命令执行流程如下图：当有多 Linux 版本环境需求时，可以进行多个版本的选择性安装，同样使用 wsl --install -d <发行版本名称> 来获取相应的版本安装，查看已经安装的 Linux 发行版本可以使用 wsl --list --verbose 命令查看，下面是一组常用的命令：功能命令安装指定的版本wsl --install -d <发行版本名称>查看已经安装的版本wsl --list --verbose选择启动指定的 Linux 发行版wsl -d Ubuntu更新 WSL 程序wsl --update查看 WSL 状态wsl --status以某个身份用户运行wsl --user关闭 WSL 的 VMwsl --shutdown上面只列出一些相对常用的 WSL 命令，在其官网文档上有列出更多命令说明 WSL 的基本命令 。[scode type="yellow"]当 ubuntu 子系统安装完成之后建议更新一下系统，更新系统软件方便后面执行其他操作时能正常执行，执行以下命令可以更新 ubutnu 子系统的软件包到最新版本：[/scode]# 更新包列表和升级系统所有已安装的软件包 sudo apt update && sudo apt upgrade{ // Java 基础环境配置 "java.jdt.ls.java.home": "/Users/dings/.sdkman/candidates/java/current", // maven 使用默认的 Java Home 路径 "maven.terminal.useJavaHome": true, // maven 可执行文件文件路径 "maven.executable.path": "/Users/dings/devsoft/apache-maven-3.9.0/bin/mvn", // maven 全局配置文件 "maven.settingsFile": "/Users/dings/devsoft/apache-maven-3.9.0/conf/settings.xml", // 在菜单栏显示 maven "maven.showInExplorerContextMenu": true, }{ "version": "2.0.0", "tasks": [ // 编译 java 源代码 { "label": "Compiler Project", "type": "shell", "command": "javac", // javac 能使用的参数 "args": [ "-d", "${workspaceFolder}/bin", "${workspaceFolder}/src/**/*.java" ], "group": { "kind": "build", "isDefault": true }, "problemMatcher": [], "detail": "Compiles all Java files in the src directory" }, // 清理编译 class 文件 { "label": "Clean Project", "type": "shell", // 调用系统 rm 目录清理文件夹 "command": "rm", "args": [ "-rf", "${workspaceFolder}/bin" ], "group": "none", "problemMatcher": [], "detail": "Clean all Java files in the bin directory" } ] }其他资料什么是 WSL 2？
自从高中开始学编程已经使用过好几门（PL）编程语言了，目前主流最多使用的编程语言 C/C++ 、Java 、JavaScript 这些都属于结构式和命令式编程语言。但在 Java 在 8 版本发布之后引入了 Lambda 表达式方式对函数式编程的支持，从现代的 Java 设计角度上来说并不是一个纯粹的结构和命令式编程语言了，个人将其归属为一个综合性很强的多编程范式的语言。在过去一年的重新学习了基于 ES6 版本的 JavaScript 之后，让我对函数式编程语言更为感兴趣了。在此之前使用过 Go 语言的实现过一些类似于函数式编程的代码，但 Go 在语言设计的时候并没有添加三目运算符和 Lambda 表达式，使其在编程体验上逊色于 JavaScript 和 Java8 这类原生支持三目运算符和 Lambda 表达式的函数式语言，目前我已经从完全放弃了 Go 语言传统命令式进行编程了，除了几个极少历史遗留开源项目正在使用它进行维护之外。如果读者正在考虑深入学习一门 PL 编程语言的话，我建议学习并且使用现代 Java21 之后的版本，现代 Java 已经添加很多新特性，这篇文章我会介绍一下函数式编程的优势。面向过程程序员日常开发中使用的最多编程方式是面向过程和面向对象，面向对象 OOP 是从 C 语言的面向过程演变发展设计出来的；面向过程取决于程序员如何设计一个好的数据结构和用编写好的函数去操作数据完成逻辑计算，在面向过程编程代码逻辑过程中，函数是主要的组织单元，程序的执行是一系列的函数调用，使用结构体来组织数据，但是能操作这些结构体仍然是通过函数来进行的。[scode type="yellow"]编程范式最典型的例子，生活中一件事情抽象到计算机程序如何做？例如一个抽象的问题：如何把一头大象放到冰箱里面该如何做？换到人类的思维第一反应是怎么可能会有冰箱能装得下一头大象？在计算机里你只要想将具体逻辑告诉它，它能可以按照你逻辑去做某件事情，它很笨不会考虑这么多其他因素，不能去考虑大象是不是比冰箱大，除非在装填函数中编写了一些逻辑来检查是否合规。[/scode]下面就是使用 C 语言就编写一个 3 个函数把大象结构放到冰箱结构里，每个步骤对应着一个操作函数，面向过程的窜行代码：#include <stdio.h> #include <stdbool.h> // 定义大象的结构体 struct Elephant { char name[20]; int weight; }; // 定义冰箱的结构体 struct Refrigerator { bool isOpen; bool hasElephant; }; // 函数原型 void openRefrigerator(struct Refrigerator *fridge); void closeRefrigerator(struct Refrigerator *fridge); void placeElephantInRefrigerator(struct Elephant *elephant, struct Refrigerator *fridge); int main() { // 创建大象对象 struct Elephant elephant = {"大象", 5000}; // 创建冰箱对象 struct Refrigerator refrigerator = {false, false}; printf("将大象放到冰箱里面：\n"); // 打开冰箱 openRefrigerator(&refrigerator); // 将大象放到冰箱里 placeElephantInRefrigerator(&elephant, &refrigerator); // 关上冰箱 closeRefrigerator(&refrigerator); return 0; } // 打开冰箱的函数 void openRefrigerator(struct Refrigerator *fridge) { printf("打开冰箱门\n"); fridge->isOpen = true; } // 关闭冰箱的函数 void closeRefrigerator(struct Refrigerator *fridge) { printf("关闭冰箱门\n"); fridge->isOpen = false; } // 将大象放到冰箱里的函数 void placeElephantInRefrigerator(struct Elephant *elephant, struct Refrigerator *fridge) { if (fridge->isOpen) { printf("将 %s 放进冰箱\n", elephant->name); fridge->hasElephant = true; } else { printf("无法将大象放进冰箱，因为冰箱门没有打开\n"); } } [scode type="green"]面向过程重点就是数据和算法的组织，采用顺序执行函数的方式，通常会存在一些全局结构体变量，这些变量会在函数之间通过参数传递；面向过程编程解决问题是：如何设计解决问题的过程？ 这里的过程是指的是函数和函数之间的关系，将问题分解为一个一个小函数，而问题所需要的元数据信息则被抽象为了变量，变量可以传入到函数中执行改变其状态，一步一步顺序执行调用这些过程函数就可以解决问题。[/scode]面向对象典型的面向过程的编程语言是 C 语言，在很长一段历史时间内程序员唯一选择就是使用 C 语言进行面向过程进行程序设计，在复杂业务场景下没有更好的其他 PL 替代选择；只能把编程中所需要的数据变量简单的抽象，在不断的代码项目迭代过程中可能会代码的重用性和可维护性较差，编写的函数和结构体之间没有任何的关联，只能通过显示的函数实参进行调用，使得函数和具体结构体类型之间没有明确绑定关系，在需求不断变化过程中扩展能力差，不利于后期对代码结构进行扩展。直到后面 Smalltalk 、C++ 、Objective-C 、Java 等 Object-Oriented Programming 语言的出现给计算机编程领域加快发展，使得程序员可以用其他编程语言选择了。[scode type="yellow"]在面向对象编程中提倡的是程序员将有状态的变量和能修改变量状态的函数封装成一个单一对象，其对象被作为基本的程序操作单元，把变量和函数封装一个单独对象，通过调用对象上面的函数这里称之为对象的方法（method），通过方法来操作对象状态数据的变更。这些抽象的对象又可以彼此之间具有相互依赖性，对象可以包含另外一个对象，对象方法可以接受另外一个对象作为参数或者返回另一个对象，让程序员编程只需要关注对象之间关系，通过操作对象的实例来解决程序设计问题。[/scode]正如上图一辆汽车可以被抽象为一个 Class 名为 Car 基础类型，汽车的属性信息是可变的可以抽象为对应的对象属性字段，而汽车所应用基础功能可以抽象为功能方法。如果需要编程的过程中需要使用汽车的时候，就可以使用 Car 类型创建出来一辆自定义的汽车，每辆汽车可以具有不同的属性值，但都拥有相同的方法和功能。[scode type="green"]面向对象编程思想本质是现实世界中的事物进行抽象，通过抽象的表达来模拟现实世界中物理关系，此种编程方式更符合人类的直觉，编程过程中关注的是对象与对象之间的关系。对象是通过一个物抽象模版 Class 创建出来的实例，通过实例进行编程。在编程过程中程序员都是提倡如果一段代码被重复的使用，或者类似的代码不止出现于一次，可以考虑将其共同的特性抽象出来。这样对应了面向对象编程的 3 大特性：继承 、封装 、多态，笔者认为面向对象编程最终编程模式是面向接口 interface 编程，程序员更应该更关注是对象之间共同特性就其抽象出来，而不需要关注实现对象的数据结构和算法功能实现的细节，抽象的一个对象包含了数据和操作数据的函数。[/scode]函数编程在 OOP 编程风格中一个 Class 在程序运行中被实例化成为一个对象变量，变量对象中存储运行时所需要的数据信息，在运行时通过对象进行修改其内部属性的值，我们可以改变对象的状态，从而影响程序的行为。这在单线程编程环境很好的设计，但是在多线程环境未必很好设计，当程序在并行执行的时候会涉及到多线程的数据竞争问题，当多个线程同时访问和修改共享的对象状态时，可能会出现不确定的行为和错误的结果。在函数编程中提倡像数学中函数一样求值，强调函数的纯粹性、不可变性和无副作用性，避免修改可变状态和使用可变的数据。这个怎么理解呢？大家在编程中可能使用过类似于 int add(int x，int y){ ... } 这样的函数，该函数对传入的参数进行求值计算。这个 add(x，y) 函数体里面的逻辑就不会对外部的数据进行修改，而是进行某些数学计算得到另外一个结果，纯函数是指函数的输出只依赖于输入不会对外部状态造成影响，也不会产生副作用，即给定相同的输入永远得到相同的输出。下面是使用 C 语言编写一个纯函数例子：#include <stdio.h> // 定义一个纯函数 add，接受两个参数并返回它们的和 int add(int x, int y) { return x + y; } int main() { // 1. 使用 add 函数来执行整数相加的操作 // 2. 调用 add 函数并将返回值赋给 result int result = add(5, 3); // 3. 输出结果，输出结果：8 printf("%d\n", result); return 0; }在这段代码中声明一个 add(x,y) 纯函数，它接受两个参数并返回它们的和，不需要依赖外部的条件变量，而且不会改变任何外部状态。[scode type="yellow"]与传统指令式编程不同，函数编程是围绕着函数进行的，在传统的指令式编程范式中，程序通常被组织为一系列的语句和命令，这些命令会改变程序的状态和数据。而在函数编程中，程序的主要执行单元是函数，它们接受输入并产生输出，而不会改变外部状态或数据。函数之间可以被组合、嵌套和传递，从而实现复杂的逻辑和操作。[/scode]下面这段 C 语言代码就是使用函数式的方式抽象中缀表达式计算过程函数式编程，中缀表达式是数学和计算机科学中常见的表达式形式，其中运算符位于操作数之间。当中缀表达式被抽象为函数的时，每个函数就对应着每个运算符和具体计算步骤，总体整合就会形成一个函数调用嵌套，其结果和中缀表达式计算结果一致，抽象例子如下：#include <stdio.h> /* 数学表达式：(1 + 2) * 3 - 4 过程式和命令式： int add = 1 + 2; int mul = add * 3; int sub = mul - 4; 函数式： int result = subtract(multiply(add(1,2),3),4); */ int add(int x,int y); int mul(int x,int y); int sub(int x,int y); int add(int base,int n) { return base + n; } int mul(int base,int n) { return base * n; } int sub(int base,int n) { return base - n; } int main(){ int res = sub(mul(add(1,2),3),4); printf("(1 + 2) * 3 - 4 = %d",res); }const twoSum = (nums, target) => { const search = (index, complement, map) => { if (index >= nums.length) return []; if (map.has(complement)) return [map.get(complement), index]; map.set(nums[index], index); return search(index + 1, target - nums[index], map); }; return search(0, target - nums[0], new Map()); };
对于写代码和编写程序来说，放到今天稍微学习一下 Python 和 JavaScript 这类编程语言就可以实现满足自己需求的应用程序，因为已经有很多很好封装好的框架共我们开发者使用，乃至一个中学生都不需要懂一些数学知识，调用相关的 Math 包就可以进行高等数学相关计算。现在有许多高级的编程语言和优秀的框架，使得开发变得更加容易和高效，Python 和 JavaScript 等语言拥有丰富的生态系统和强大的社区支持，为开发者提供了各种各样的工具和框架，使得构建复杂的应用程序变得更加容易。上文提到从需求变成编程再到程序，需求是代码编写者对于软件系统所需功能和特性的描述，例如实现一个四则运算计算器程序，可以是在程序直接编写 console.log("20 + 4 = ",20 + 4) 这种形式来完成需求，也可以是实现一个漂亮的界面完成对应的需求。从 IDEA 到 Coding 到 Product 这个过程中 coding 环节是属于技术实现环节，实现某个功能可以有很多种技术方案，但是方案这么多作为程序员如何选？主流开发的软件都属于应用软件，目前跑在电脑上和跑在手机上的应用软件并没有一些本质区别，都是程序员将自己的大脑逻辑抽象通过代码的形式告诉计算机，当执行这段代码运行之后该程序就会按照你编写的逻辑去交给 CPU 执行，只不过 CPU 要比我们脑子里面计算快得多。这个过程就是 Coding 阶段不能确保程序员大脑里的逻辑一定是正确的，当不合规的逻辑被编写到程序代码中，就可能会产生很严重的后果，例如程序崩溃导致的事件。在电脑和手机只要你开发的不是一些基础的软件，应用软件遇到问题最多就是卡死重启一下，现在的服务器端软件更是搞出来主从或者高可用架构来解决程序员在编码阶段留下的 bug 的问题。在工业时代崇尚机器自动化，现在计算机程序已经渗透到各行各业中了，天上飞的地上跑的水下游的，飞机汽车潜艇这些设备上都靠着计算机程序在辅助人类完成一些操作。机械工程师在设计一些机械结构时犯下的错可能会导致飞机会坠毁，即使是设计合理的机械结构现在控制这些机械装置的已经不是人类了，是跑在计算机中的程序在控制这些机械结构。所以一架飞机或者自动化驾驶的汽车出了事故，责任不完全在机械工程师身上，更有可能的是编写软件的工程师导致的。即现在从一名普通人转行为一名程序员太容易了，普通人通过特定的 IT 培训经过几个月的训练就可以写代码，此种工程师可能不具备专业的 CS 知识就可以动手写程序了，带了问题就上面所说的不可靠软件，如果需要开发专业型的软件还是需要专业型的知识作为铺垫的，例如让一个学土木工程或者做建筑设计的去做软件工程师，那就对应上了中文的俗语 “隔行如隔山” 。一些知名计算机科学界的先驱们就提出了点观点 Dijkstra 曾说过：[scode type="yellow"]我们所使用的工具对我们的思维习惯有着深远的影响，因此也对我们的思维能力有着深远的影响。......使用 COBOL 会削弱人的思维；因此，教授 COBOL 应被视为刑事犯罪。[/scode]简单的工具容易被使用，但是会让使用者不去思考背后的原理，导致缺乏逻辑思维和不去思考事物的本质。这在日常生活中使用的智能手机可以拍照可以打电话可以玩游戏，但是发明和制造一台手机所需要的知识远远不是一个人就能做到的，是一个跨学科和跨领域合作的产品。现在用电脑写一篇文章的时候打错字可以回退修改，但是如果在纸上你写错了一个字可能就报废一张纸，解决办法则你可以使用铅笔进行写字配合着橡皮擦使用，古代人使用毛笔更需要思维和手法练习才发展出来了书法行业。现在已经很少有人去使用了 COBOL 语言进行程序设计了，虽然我本人也是没有写过，但是在研究一些 PL 过程中了解到 COBOL 这门 PL 语言属于结构化加命令式结合产物。但是新的流行语言例如脚本语言和弱语言 JavaScript 和 Python 这些只有在运行的时候才能确定某个变量的类型，还有 C/C++ 这类有空指针设计的 PL ，也会带来底层软件的 Bug ：使用这里语言开发的程序如果程序在开发期没有有效找出 bug 或者程序员的逻辑错误导致 bug 产生，当程序发布之后这些带有 bug 程序会被发布到线上环境，可能在未来的某一时刻会因为这些 bug 导致程序出现故障从而影响到整个系统。一些隐秘的 bug 不一定尽快被修复，例如一个计算闰年和数据类型存储精度丢失的 bug ，要导致触发这个 bug 条件是当前是时间是处于闰年或者闰秒才会发生错误。这类隐秘的 bug 是开发阶段程序员逻辑错误导致的没有处理好，在修复的这些 bug 过程中需要花大量的时间解决，如果在开发这些涉及相关领域知识软件系统时应该提前去查看一些相关领域的资料文件，使用一些业绩主流的解决方案，例如被使用最广工业级时间库，来减少不必要的找 bug 和修复 bug 的时间浪费。现在内存安全的编程语言研究有很多了，但是能作为系统层开发的编程语言屈指可数，除了 C/C++ 这两门语言以外近些年也出现了能替代他们的编程语言例如 Zig 、Rust 这些编程语言，当时目前具备这样的能力程序员还很少，还另外一种解决方案使用静态程序分析产品来提高代码和软件的质量。应用层的内存安全编程语言更不用说，目前主流的 Python 、Java 、C# 、Ruby 这些都属于应用层的编程语言，但是应用层编程语言大部分都是脚本和解释性语言，能作为工业级程序设计的也就只有 Java 语言为主流了。很多人在开始一个新项目之前都会尝试使用新比较时髦技术去开始试错，尝试比较新的编程语言去写新的项目，风险比较大，而且很多解决方案也没有一个主流统一的企业开发标准，例如 Java 是有企业开发标准的。最典型的例子就是 Go 语言，Go 语言在很早之前连一个包管理工具也没有，统一包管理工具也没有了，这就导致每个团队都各自搞各种的开发依赖包解决方案；包括现在依赖包管理工具也不是那么成熟，很多开源的第三方库，从 v1 版本升级到 v2 版本时，是直接弃用掉 v1 方式的 API 设计，导致再次使用新版本时会重新去学习新的 API 使用方式和在源代码里面更改 v1 包路径映射到 v2 版本上，这也是浪费大大时间的。在程序员技术圈里经常流行一些鼓吹某些编程语言和技术方案的案例，实质为其争吵起来。一些编程语言脑残粉那种会鼓吹某个编程语言，当然一些也是抱着相关技术布道的目的地。很多某个语言脑残粉就会盲目的推崇某个语言，在大公司里有足够的人力和财力，开源让一些人去试错，但是试错是有代价的。选择 PL 还是要根据场景来的，例如开发高性能的网络 IM 聊天服务器应用，只要能实现网络通信的 PL 都可以去实现。那如何去选择对应的 PL 呢？但是如果从事迹开发角度来说你考虑内存占用吗？考虑实时性要求吗？考虑性能要求吗？还是考虑团队综合技术水平和开发效率时间快慢问题？不要试图去说服和你观点不一致的人，因为很多人它的认知和所需要的领域知识不及你全面，而且讨论问题的你还要帮助他去科普某些知识细节，交流起来很费劲。某些脑残粉给予给你扣上一个帽子，某些情况下一部分人就是故意在讨论中参入一些不相关的话题扯开话题或者故意添加干扰原本话题源的质量；参入讨论的前提跟你是一个层级的人或者说是有具备相关领域知识的人，如果观点不一致的双方可以互相尊重交流和倾听求同存异的面对观点进行交流沟通。能够正常沟通前提是双方获取的信息来源都是相同一致的，很多系统在设计阶段采用的方案因为信息不对称信息获取问题，导致此用一些不是主流解决方案和技术栈，这会导致自己在某些新技术栈下走其他成熟技术栈道老路，出现了问题和 bug 也找不到官方的文档和相关的问题解决方案。那如何选择技术栈呢？我一直坚持一个观点就是从源头获取信息，例如通过 Google 搜索统计和当下流行技术栈，还有招聘网站上 JD 大多数公司所使用的技术栈来判断。例如使用 spring 框架可以去 spring 官方技术博客和文档上获取，而不是在互联网上看一些其他人已经生产的二手文章和二手信息。通过数据来说话，主流的技术用什么，解决了什么样的问题，以前有没有这样的方案，二者对比弊端呢，再配合自己应用场景做选择。在中国大陆因为一些特殊原因不能正常访问国际互联网，这也是程序员获取信息源的阻碍，可以尝试着在 GitHub 和 Twitter 找到相关领域专家进行交流，一般这些技术框架或者开源软件都会提供相关负责人的邮箱地址，通过此种方式就能和一线的技术专家交流了。很多国内程序员喜欢通过技术图书来学习国外新的技术，其实很多一部分技术都提供了官方网站和官方文档，通过官方资料去获取信息是最为正确的方式，但是弊端是官方的文档只是介绍如何使用这个技术和为什么要这个技术，还有一些使用此技术优势。但是对应真正开始尝试使用这些技术时，又不知道如何和现有系统做整合。此时可能就会在互联网上查别人的资料和购买技术图书，我不推荐国内的技术图书，很多都是将官方文档抄袭就出版了。我个人比较推荐使用所使用技术栈方案对官方出版的图书进行学习，因为这是官方团队成员所写的，没有谁能比这项技术的作者更为了解这项技术本身，所以推荐找到学习源头获取技术知识。
随着就业形势不佳，互联网科技公司的寒冬来临，越来越多的程序员被裁员。在这样的背景下，越来越多的程序员开始尝试成为独立开发者（indie Hacker），希望通过开发一款产品来实现财务和工作时间上的自由。他们致力于通过开发和推广自己的产品或服务来实现个人或团队的利润和成功。相比之下，独立开发者在产品开发和运营方面可能相对弱势。独立开发者往往只有一个人，大多数是程序员出身，没有在公司里面一个团队的支持，自己需要去发觉需求，对需求进行分析和产品 UI 设计直到通过代码实现该产品，最后部署上线运营，在这个过程中需要很多资源的支持，这篇博文我将介绍一些关于从小白如何转型为独立开发者经验。编码技能当从一名产品经理和 UI 设计师转型到独立开发者，可能已经熟悉产品开发的流程和方法论，但是缺乏专业的编码技能。产品经理通常需要与程序员合作才能完成产品，但当你想作为一名独立开发者时必须要掌握编程这门技能了，或者找程序员外包的项目，但是我不太推荐这样去做，都不是自己用代码实现出来的产品怎么能称之为独立开发者呢。我建议你最好先学习一门编程语言，因为编程可以帮助你将想法变为现实中的产品，在这方面我建议学习 JavaScript 语言，因为它可以用于开发 Web、Desktop App 和 Mobile App 应用。首先学习最新的 ES6 标准 JavaScript 基础，再学习前端 MVVM 类型的 UI 框架，这里推荐 Vue.js 和 React 框架，此外如果需要将产品开发为 Mobile Native App 产品，要考虑多个手机端的同步开发工作，在过去可能需要两个团队才能完成工作，当现如今已经有很多的第三方框架可以帮助实现此需求，此时推荐你学习 ionic Framework ，ionic 可以允许你使用 Vue 和 React 技术来实现 App 程序，并且已经提供了各种手机设备上的 UI 组件来使用，可以省去一些设计 UI 的时间。对于如果产品考虑多个桌面端推荐考虑使用 Electron ，可以使用 JS + HTML + CSS 来完成传统 QT 的桌面端开发工作，因此推荐要学习编码技能转行的朋友，去学习 JavaScript 技术栈是不二之选。从 IDEA 到产品做独立开发者从来都不是一件轻松的事情，除了要应对前期持续的时间投入和打造产品的费用都投入亏损，想法太多，过于追求完美，很多想要的功能根本无法在短期内实现，最后在慢慢实现的煎熬中放弃了整个产品；第二辛苦做的产品真正上线之后却不被市场认可，数据没有起色，从而失去了继续更新的力量。作为一名程序员出生的开发者，把自己的需求变为一款产品，要经历多个阶段，从 UI 和 UX 原型图再到开发阶段实现，最终发布产品上线这个周期很长，且需要的资源支持非常多，面临着很多问题。首先从产品选型出发是做 App 形式的产品还是基于 Web 的 Saas 形式的产？取决于多种因素，包括您的产品定位、目标用户群、技术栈、团队能力、市场竞争情况等等。如果第一款产品，只是作为一次独立开发者尝试，建议还是采用 App 的方式只需要将应用提交到应用商店（如 App Store 或 Google Play，当然需要支付一些对应平台的软件数字开发者证书费用），由平台管理应用的分发和更新；采用 App 的方式发布产品不需要购买服务器资源和考虑扩容问题，使用用户客户端本地计算和硬件资源，如果在产品发布并且运营一段时间之后没有盈利，乃至盈利金额都不能抵消掉开发阶段的资金投入，那么问题可能就出现在 IDEA 和产品选型上了，可以考虑暂时停止开发这款产品了，因此工具类的 App 产品就不会需要维护服务器后期更多费用。获得用户和收入有的独立开发者在做开源项目，经营开源社区，并构建自己的影响力；有的人通过分享、出书、写专栏获得收益；有的人则会开发工具类软件，笔记、闹钟、创意工具等等；有的独立开发者自己开发了很多款游戏。
现在注册一个域名和购买一台服务器部署一个 Web 网站非常容易，现在流行的 Serverless 架构更是连服务器都不需要购买，直接在各个云厂商的后台点点鼠标就可以部署一个在线的 Web 程序，虽然相比传统建站的方式简单很多，但是也有考虑一些很基础因素和配置，这篇博文我讲以博客系统为例子讲解如何维护一个 Web 服务程序。现在有很多流行的博客系统，例如静态的 Jekyll、Hugo、Hexo，动态以 PHP 为首的 WordPress 系统，WordPress 有着完善的生态系统是世界上目前二次开发使用最多的 Web 系统，静态的博客可以部署在 Github Pages 和 Vercel 等，直接将编写好的网站源代码上传到免费的空间即可。而对于动态博客系统还说，需要 LNMP 环境的支持才能正常运行，现在也流行直接购买已经部署好的数据库环境和运行环境来跑博客，对于动态博客系统还是数据才是最重要的，这篇博客将介绍一些方法一名合格的站长该如何运维自己博客。基础防御[scode type="yellow"]博客系统存放在主机之上通过互联网提供世界各地访问，这些访客里面有普通人也有一些黑客，黑客可以利用博客程序的漏洞例如 XSS 和 SQL 注入漏洞来破坏程序，这对于博客系统安全有了新的要求。普通的静态博客完全可以不用管这些，但是对于 PHP 这类的动态脚本语言来说更需要关注，因为 PHP 是一门动态语言如果被黑客注入了恶意代码会导致程序数据泄露，严重的会破坏程序的运行，目前场景的作为就是在博客核心代码里面对用户输入的数据进行过滤或者做合法性校验，或者在 PHP 使用 disable_functions 配置项来禁用一些敏感函数，来保证程序安全性。[/scode]如果考虑不是在应用程序嵌入代码来做防御，可以使用一些带有代码注入攻击检查防御的网关产品，博客作为一种 Web 程序提供的服务是要经过 HTTP 网络请求的，可以考虑采用带有防御型的网关产品。例如 Cloudflare 它可以提供基于 SASE 和 SSE 平台，将网站的域名的 DNS 接入到 Cloudflare 并且使用它来解析服务网站，可以改善安全和韧性减少攻击参数传入到主程序中，国内也有长亭科技的 SafeLine 的 WAF 产品。Cloudflare 并且提供 CDN 服务，可以隐藏真实的主机 IP 地址，也能确保域名和 DNS 设置的安全，防止域名劫持和 DNS 污染攻击。对于 HTTP 协议来说才有基于 TLS 安全层的 HTTP 协议，启用 SSL/TLS 证书，确保所有数据在传输过程中都是加密的，防止 Cookie 和 Session 信息被截取或篡改。对于基于使用 LNMP 作为基础设施运行的系统，要考虑是不是使用的长期支持版本的软件，如果使用的不是长期支持的版本的软件，如果出现了漏洞，对于这些已经不维护的基础软件会很麻烦，不能得到相应的补丁更新。操作系统环境还是建议使用基于 RHEL 系列的 Linux 发行版本，现在 CentOS 已经是最优的，我推荐使用 Rocky Linux 这个系统是 CentOS 创始人创建的一个克隆 RHEL 的开源操作系统，是在 RHEL 的基础上进行重新编译的，该系统在很大程度上兼容 RHEL 版本的，并且可以通过相同的方式安装 RHEL 软件包，还能得到长期的更新支持。当整个基础环境搭建完成之后，强烈不建议使用 root 账号去运行网站的基础设施，更应该创建一个专用的用户组和用户来运行对于的软件，这样即使黑客绕过前面的基层防护，也没有更高的权限去访问其他的数据和程序。服务器防火墙软件还是推荐使用目前主流的 firewalld ，这是 REHL 企业级的服务器管理软件，相比一些老的 iptables 来管理出入站流量复杂的操作更容易操作。不要开放不必要的端口，一个 Web 服务器最多开发基础的端口 80 、 443 、22 端口，其他端口可以一律禁止外部访问使用，最好的方式使用白名单 IP 的方式进行访问控制，通过跳板机进行管理。对于 root 用户来说应该不使用 root 密码进登录，而使用基于 SSH 的非对称密钥进行登录，可以将登录电脑和被管理的服务器进行绑定，将公钥上传到远程服务器来进行登录管理。此外此种方式还要考虑私钥的安全性，不要将私钥公开，避免在页面或日志中输出敏感信息，如数据库凭据、API 密钥等。数据备份在线系统的核心是数据，这些数据一般会被存储在 MySQL 这些关系型数据库中，目前主流的动态博客系统数据都是存储在关系数据库中，前面提到黑客通过一些特殊的手段获取到了服务器执行权限，从而可能会导致数据库被拖库导致数据泄漏。对于数据库系统来说管理也是不推荐开放 root 权限给远程用户登陆，对于 Web 程序所依赖的数据库表所在数据库提供对于数据库账户来管理，并且取该账户的一些敏感权限例如删除数据表，添加新字段等，最后 Web 程序的 IP 和能被访问数据库 IP 白名单进行绑定，这样以免泄露了账户和密码也能阻挡恶意的用户远程连接到数据库。建议在数据库中的敏感字段进行加密存储，并且定期对数据库的数据进行周期性备份。对于普通博客系统建议每天执行一次数据库备份计划，例如使用 crontab 进行周期性执行备份脚本，这个备份可以使用 mysqldump 这类工具部分，对其备份的数据可以考虑加密存储。前面所说的是传统备份，部分云厂商提供了主机快照功能，可以对整个主机的数据进行快照备份储存，当然要额外支付一些费用，不管以何种方式进行数据备份，最重要的是养成备份数据习惯，即使数据丢失也能通过备份数据恢复。