AI已经极大程度地更改、影响了这个世界的走向,我们可以看到一些趋势,比如 画师、网文写手、真人短剧逐渐地被替代,程序员的替代还没有那么快。

大家有没有想过这个原因呢?我认为核心就在于:AI生成的产物,是否是最终的交付件。

这和Leader分配任务给下属一样,如果下属的交付件可以直接用,那Leader可能就说,你这个代码日志一定要符合规范、你这个材料字号要调大一些;反之,可能Leader就用下属的没那么完美的交付件,自己再整理成最终的交付件。

而程序员的情况不同:代码不是最终交付件,系统才是。

代码只是中间产物,它需要:运行、集成、部署、监控、演进。这一整条链路还没有完全“标准化交付”,所以替代速度较慢。

那么结论其实很自然,个人提效的关键就在:让AI Agent直接生成交付件,这个交付件一定要是可通过迭代提示词、上下文不断优化的,最好这个产物是可编辑的(Markdown、Excel、Html),最后一公里的时候人可以做一定的修改。我在2月27日的朋友圈里也表达了类似的观点。

那么怎么更好地让AI Agent生成工作中用的交付件呢,我目前的实践是 Agent+MCP+SKILLS+PARA。Agent 负责执行,MCP 连接外部世界,SKILLS 提供可复用能力,PARA 提供稳定上下文,我们一个一个解释。

Agent+MCP

Agent毋庸多说,相比于只能对话的LLM,是一个围绕目标,能够多步执行、调用工具并持续迭代的执行单元。Ask模式(只跟AI对话)是没法更进一步地提效的。

SKILLS

SKILLS可以理解为一组可复用的能力模板(Prompt + Tool + Workflow 的组合)。例如

  • 整理Inbox文件
  • 生成周报
  • 分析某个Area的内容
  • 输出某种固定格式的文档
    如果没有SKILLS,每一次都在“重新提示AI”,有了SKILLS,相当于把最佳实践固化下来,交给Agent反复使用。

PARA

对于Agent来说,与其给 AI 造新工具,不如给它一个它已经「会用」的旧接口。在于 LLM 的训练数据。LLM在训练阶段已经看过了很多文件系统的操作,grep、ls、find等等。LLM会非常善于在文件系统里探索他想用的内容,而不是拼凑一个高度定义的DSL。

PARA呢,和文件系统强强联合,给Agent提供了一个稳定、可理解的上下文环境。

个人实践案例

我的PARA目录

PARA是一个指导原则,每个人的PARA都可以有一些定义,比如我的PARA目录是这样子的,适配了我平时使用的OneDrive(它会强制有一些顶层文件夹,我自认为巧妙地利用起来了),同时我还有把一些作为Blogs发布的诉求。同时也放了一些README.mdAbout.md 方便Agent读取。

如果大家感兴趣的话,我可以单独写一个文章介绍一下。

1
2
3
4
5
6
7
8
9
10
11
生活/
Archive/
Areas/
Attachments/
Blog/
Documents/
Projects/
Resources/
Software/
About.md
README.md

下面的实践过程,本质上不是一次性的。

如果你每隔一段时间都需要做类似的事情,比如:

  • 盘点近期工作成果
  • 挑选可以对外分享的主题
  • 判断哪些材料已经足够支撑一次输出

那么这个流程其实是高度稳定的:

  1. 扫描目录
  2. 识别活跃内容
  3. 提取关键材料
  4. 组织成候选结果

这类流程,就可以被固化为一个 SKILL,例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Skill: summarize-workspace-for-sharing

输入:
- 一个工作目录路径

输出:
- 可用于分享的主题列表
- 每个主题对应的支撑材料
- 是否已经具备“可交付”的判断

内部流程:
- 扫描最近活跃文件
- 聚类主题
- 评估材料完整度

这样一来,下次再做同样的事情,就不需要重新写 Prompt,而是直接调用这个 Skill。

实践1:筛选个人成果用于会议分享

向 Agent 提问

1
这是我的个人工作文件夹,你可以看看我最近几个月的工作成果,看看有没有什么东西可以拿去"ApacheCon"分享的?

Agent 回复

1
2
3
4
5
我会先快速梳理这个工作区最近几个月的文件分布和活跃项目,找出适合做 ApacheCon 分享的候选主题,再回头看哪些材料已经足够支撑成一场 talk。

已探索 3 个列表,ran 1 command

我已经看到这个目录更像一个长期知识库,不只是单一项目,所以接下来我会缩小范围:重点看近 6 个月新增或活跃的 Apache / 开源 / AI研发 相关材料。

实践2:整理 Inbox,迁移到合适的 PARA 目录

个人文件夹里最容易失控的地方,通常不是 Projects,也不是 Resources,而是各种临时下载、微信传文件、会议附件、截图和草稿。它们会先堆在一个“待处理区域”里,时间久了以后,人自己都不太愿意翻。

这种场景其实很适合交给 Agent。因为它不需要一开始就“理解整个世界”,只需要先做一件很具体的事情:识别这些文件分别属于什么类型,应该进入哪个目录。

向 Agent 提问

1
这是我的 Inbox 目录,里面有很多最近积累的文件。你可以帮我分析一下这些文件分别属于 Projects、Areas、Resources 还是 Archive 吗?先不要直接移动,先给我一个整理建议和理由。

Agent 回复

1
2
3
4
5
我会先扫描 Inbox 里的文件名、扩展名、最近修改时间,以及部分文档内容,判断这些文件更像是项目资料、长期领域资料、通用参考资料,还是已经可以归档的历史内容。  

已探索 4 个目录,ran 2 commands

初步看下来,这里面混合了会议纪要、下载的安装包、临时截图、正在推进项目的材料和几份标准文档。我接下来会先按“是否仍在活跃使用”进行第一轮分类,再给出建议路径和批量整理方案。

实践3:让 Agent 总结一个 Area 当前都积累了什么

有些 Area 是长期负责的,比如英语、软件工程、孩子教育、投资理财。这类目录的问题不在于“没有内容”,而在于内容太多了,久而久之自己都不知道里面已经沉淀了什么。

Agent 很适合做这种“盘点当前资产”的工作。它不一定直接帮你产出最终文档,但可以先把目录里的内容做一轮结构化总结,帮你建立重新利用这些内容的入口。

向 Agent 提问

1
这是我 Areas/软件设计 目录。你可以帮我看看这里现在主要沉淀了哪些内容?按主题帮我归一下类,再告诉我哪些内容已经比较完整,哪些内容还只是零散积累。

Agent 回复

1
2
3
4
5
我会先浏览这个目录下的子目录和关键文档,判断当前沉淀更偏向方法论、实践记录、工具使用,还是项目复盘,并整理出一个主题结构。  

已探索 5 个目录,ran 3 commands

从目前看到的内容来看,这个 Area 里既有一些长期积累的方法论笔记,也有若干和具体项目相关但仍有复用价值的实践文档。我接下来会把它们拆成几个主题,并标出哪些已经可以整理成对外输出的文章或分享材料。

实践4:从一个目录里提炼周报 / 月报素材

很多人写周报、月报最痛苦的地方,不是“不会写”,而是回头找素材太费劲。尤其是当你的工作记录分散在会议纪要、草稿、截图、PRD、临时文档、代码仓库说明里时,人会本能地拖延这件事。

如果文件组织本身还算规整,Agent 就可以直接从目录里提炼候选素材,最后输出一个可编辑的 Markdown 初稿。

向 Agent 提问

1
这是我最近两周的工作目录。你可以帮我整理一版周报素材吗?先按“已完成事项、推进中的事项、问题和风险、下周计划”四个部分来归纳,尽量基于已有文件内容,不要凭空发挥。

Agent 回复

1
2
3
4
5
我会先查看最近两周活跃修改的文档和项目目录,提取其中能够反映工作进展的内容,再按周报结构组织成一版可编辑草稿。  

已探索 6 个目录,ran 3 commands

目前已经看到几个高频修改的项目目录,以及两份会议纪要和一批方案文档。我接下来会优先提取能够代表实际推进结果的内容,避免把讨论中的想法误写成已完成事项。

参考资料

  1. Agent:一切皆文件:https://mp.weixin.qq.com/s/ulmVG3_yrfy-7EofRgnyGw

其实,一直写Java,用Spring框架的同事,一定会觉得依赖注入就像呼吸一样。但是在其他语言的开发者看来却不尽言。

我在项目的开发过程中使用过Java,也使用过Go语言。见过很多写Go语言的同事对Spring框架嗤之以鼻。也被同事讲过写的代码Java味很重,emmm,学习一个语言就要学习它的最佳范式与哲学,这我无比地认同。

首先,依赖注入 != 依赖注入框架 ,比如

1
2
3
4
5
type Client struct {
logger *slog.logger
}

client.logger = newLogger()

其实,这样显式地为 logger 赋值,本身就是一种依赖注入。 只不过,它是手动注入,而非通过框架自动完成。

如果完全不用设置,那就代表这个模块是单例的,举个例子,Java中的log4j2,绝大部分场景下都是单例的,通过配置文件来反向控制某个包下面的日志级别等等。

之前读过一本书有个很有意思的理念,就是”包变量/静态变量”是不好的,是违反物理规律的。

只是由于种种原因,在程序运行时,只存在一个实例。25年1月份,在一个Go项目中,我做的一件重构就是把原本的包变量引用替换成了成员变量,因为发现在运行中会存在多个相应的struct实例,他们对这个变量的需求是不一样的。

那么依赖注入框架呢?
其实Go语言也有很多依赖注入框架,如果有很多的strcut都要get、set,那建议还是使用依赖注入框架。我做的项目没有使用依赖注入框架,主要有两点原因

  1. Go语言依赖注入没有形成统一的标准。
  2. 其次,目前产品还没有很多的strcut都要get、set。

DI框架本身也有学习成本,这块框架我没有详细了解,如果能在工程组织,测试上大大简化的话,我还是很乐意用的

一方面,是Vibe效率的飞升:三天完成一个Demo,一下午Vibe一个Poc项目。
另一方面,是企业效率没有想象中的提升,似乎,没有AI辅助流程,对企业没有任何影响。

为什么?

首先,这是极具代表性的两个开发场景:
其一,单人开发,担任产品经理、开发、测试、运维角色,一人承担所有的责任。(即使GPT写错了付款接口你也只能责怪自己)。
其二,多人协作开发,分工明确,各自承担不同的角色与责任。

基于此,针对AI辅助开发,笔者分析整理了三个原因,这三个问题层层叠加,从模型能力到系统隔离,再到协作方式,构成了企业级 AI 提效的现实瓶颈。

一、模型、算力

模型的推理能力、支持上下文的长度,对工具的效果影响是天差地别的。企业出于对资产安全的考虑,往往会选择私有化搭建模型,这可能会导致如下两个问题:

  • 内部模型参数量较小:推理能力明显低于公有云大模型,尤其在多语言代码生成和复杂语义补全上差距明显。
  • 上下文长度受限: 受限于上下文窗口,AI 工具只能看到当前编辑文件或部分上下文,导致生成结果割裂。

    先进的理论、经验都是基于AK 47的,确定一定要基于AK 36继续优化么?

二、信息系统孤岛

在传统的逻辑下,将信息分隔到多个系统,进行精细化管理,通过人来串起整个流程,这无疑是非常优秀的做法。笔者也曾经将团队中的所有数据库相关实体定义集中到一个代码库管理。

而AI辅助开发工具通常运行在代码仓,单人开发模式下,我可以把设计文档提交到代码仓,典型测试集都提交到代码仓,编写代码需要的Everything,只要适合转换为文本模式的,我都可以提交到代码仓。

在AI辅助开发工具没有集成对应信息系统的情况下,需要开发人员手动提炼复制上下文到AI辅助开发工具,这和我再提炼提炼,以Chat的方式对话,有什么区别?

其实这一点在代码仓管理上也存在,Repo的分隔也存在这个问题,前端、多个语言的客户端、API的Yaml文件,进行精细化的管理。但CC、Cursor这样的工具无法看到全貌。

下一个我做的项目,我倾向于使用Mono Repo,充分利用AI工具提交,并且如果是AI相关项目的话,我倾向于使用Python语言。

但是我相信,如果多个系统都能与AI工具对接打通,给AI提供了更加结构化、质量更高的数据,理论上上限要高于所有东西以markdown格式放在代码仓库不同文件夹的做法。

三、工作流程

AI没有改变软件工程,只是把其中的很多工作加速到不可思议的地步。

AI只能加速单人闭环的工作,对人和人的交互没有帮助。

AI 当前的强项,是提升“单人闭环任务”的速度。但当任务涉及跨角色交互时(如开发与测试、测试与运维、产品与开发),AI 的作用显著减弱。

左图是人与人的交互,严丝合缝。如右边两张图所示,AI交付件还不能做到严丝合缝地对接,为了能顺利走完流程,蓝色、橙色,必须得有一方做出额外的工作才行。(当然,为了避免同事说你不靠谱,我还是推荐蓝色方做出额外工作。)

AI时代,应该尽量把一个工作的各个方面交给一个人,这样来减少人与人的交互,充分提高效率。

这和交给Agent足够的上下文的道理是相通的。

来看一个典型案例:微服务开发与测试的协同。一个典型微服务的开发、测试、发布上线流程如下:

产品与开发、开发和运维,这样的问题也存在,以编码举例子,主要是因为时间相对长,矛盾更加明显。


测试负责产品质量的出口,测试在自己的领域范围内,通过自己的理解,对整体的测试进行分层落地(基本接口测试与集成测试环节)。

图中共有四项活动:代码、单元测试、基本接口测试、集成测试。这个模式下,单元测试与基本接口测试存在能力上的重叠。对流程进行了如下优化(测试将测试设计左移,基本接口测试放在开发阶段,弱化单元测试。

从传统领域的角度来看,下面的方式,测试更早介入,且减少了重复工作。但是从AI辅助的角度来看,那个效率更高?

从AI辅助的角度,橙色的环节充斥着大量的交互,诸如代码中书写的测试要匹配测试的设计(通过方法名与用例Id匹配),开发要确认测试设计表达的含义等。当团队想要使用AI提效时,这个矛盾就被放大。尤其是当 模型能力不足信息孤岛 并存时,协作复杂度被进一步放大。

当下,在笔者所在的团队,上下两种流程都存在。上下两种方式那种更好?笔者下意识地觉得当未来AI的生产力突飞猛进的时候,上面的方式会更好(这一判断源于相信人和AI协作的效率会远远大于人和人协作的效率),但是在当下,笔者想不清楚,也给不出答案。当前笔者针对上下两种场景,寻找集成了对应信息源、最合适的工具进行提效。

总得来说,企业要避免陷入 “工具因为流程导致效果不及预期”、”流程不会为没有效果的工具变更,让步”、”开发人员对工具没有信心” 的恶性循环。

总结

AI 在个人层面的高效令人振奋,但企业的整体效率并未出现预期的跃升。问题主要集中在模型能力、信息孤岛与工作流程三方面。前两者可以通过技术投入与资源建设逐步改善,而工作流程的变革则需要企业在思维模式与协作方式上完成真正的转变。

近日,在推特上看到了有关GitHub用两台服务器,部署了200万个pages网站的帖子,也想分享一下2019年左右,使用Nginx正向代理数千个配置文件的往事。

当时项目上主要用Nginx来做网络平面的隔离以及TLS的解码,无论是从外到内,还是从内到外的流量都会经过Nginx。

从外到内的流量虽然大,但是也就是几个端口提供服务。从内到外有个外出推送的功能,它是可以将不同用户的数据推送到用户的HTTP服务器,这里有数千个HTTP服务器,涉及到HTTPS的证书也有数千个,Nginx的配置就集中在这里。

每个用户变更HTTP服务器的时候也会涉及到Nginx的Reload,这和GitHub分享的场景也很相似。如果你的应用接受不了不定周期的Reload,不要采用这种方案。

在这个项目里面,我磨练了很多,对TCP、HTTP协议栈都有了很深的理解,也见识了各种各样的客户服务器,比如用C语言手写解析json的等等。

我还经历过一次这个规模级别平台的迁移,为了判断迁移后配置的准确性,写过脚本对这些http server一个一个进行测试。很怀念那时候的时光。

业务服务碰到的问题

主要就是海量客户的HTTPS Server多种多样,有些响应时间较长,有些可能还会定时关机。如果Server一直hang住,可能也会导致业务服务卡住。针对客户的HTTPS Server,长期发送不通的引入黑名单机制,每数分钟尝试通行一个业务,如果成功则从黑名单移除,不通的话继续在黑名单中等待。

生成配置文件时间长

由于Nginx服务上运营了数千个配置文件、数千个证书,这导致每次Nginx容器启动的时候要从配置中心/证书中心逐个获取,未优化前这个时间最长可以达到20分钟。通过批量拉取,挂载虚拟机路径缓存的方式解决。这避免了每次容器启动都从配置中心逐个拉取文件,启动时间从20分钟降到3分钟左右。

安全

安全主要就是先保护自己,避免推送到一个内网地址把系统自己打爆。比如恶意客户拿到了Kubernetes API Server的地址,配到海量业务推送上。其次,tls上证书,算法等级,是否过期等等。

HTTP请求的各种错误

connect() failed(110:Connection timed out) while connecting to upstream

Nginx尝试发起TCP连接到HTTP Server,但是没有收到SYN+ACK响应,最终超时。

connect() failed(111:Connection refused) while connecting to upstream

Nginx 发起了 TCP 连接请求,收到了 RST 响应 —— 即目标主机明确拒绝了连接。

upstream prematurely closed connection while reading response header from upstream

HTTP Server接收了TCP连接,但是没有回复HTTP响应。

peer closed connection in SSL handshake while SSL handshaking to upstream

Nginx 在与上游建立 TLS 握手的过程中,对端在握手尚未完成时主动关闭了连接。

SSL_do_handshake() failed(SSL: error:140770FC:SSL routines23_GET_SERVER_HELLO:unknown protocol) while SSL handshaking to upstream

Nginx 与上游建立 SSL 握手时,收到的响应数据并非 TLS 握手数据(例如是纯 HTTP 响应),因此握手解析失败。

TL;DR

笔者推测Palantir起初以支持定制代码运行为基础,在构筑自己部署平台(Apollo,Palantir GitHub上也有很多开发者构建、Lint工具)的同时,逐渐抽象出Dataset、本体、Function、Action API,打造了坚实的Foundry平台,让应用从定制化开发逐步“长在平台上”。最终,Palantir 推出人工智能平台(AIP),实现数据驱动的智能决策。

前言

近年来,Palantir 无疑成为数据分析领域的焦点之一。作为一家以解决复杂问题为核心的公司,Palantir 为政府、国防和企业客户提供了强大的数据整合与分析能力。Palantir 的核心产品 Foundry 是一个面向数据整合与分析的平台,它如何从最初的定制化开发逐渐演变为如今的通用数据智能平台?笔者尝试基于公开资源推测梳理 Palantir Foundry技术平台的演进路线,分享一些分析与推测。本文仅代表个人观点,欢迎读者交流探讨。

阶段0 定制代码运行

从Palantir的Offering来看,其核心始终是为客户解决复杂问题,拥有大量的FDE。合理推测Palantir最早其实以定制代码运行交付作为基础,通过高度定制化的软件开发满足客户在政府、国防和企业领域的特定需求。

阶段0,此时都处于定制开发状态。

阶段1 从定制代码运行到Palantir平台运行

正如《人月神话》中所说,优秀的程序员都会有自己的library库,优秀的定制开发商也倾向于提炼可复用的技术框架。

对于定制代码来说,我们把定制代码分为编写态和运行态

  • 编写态,对应Palantir Code Repositories,可以看到Palantir的很多东西,其实跟Git很相似,有分支、合并等等。
  • 运行态,将Palantir Code Repositories的代码构建运行,支持多种触发方式,比如通过API调用来执行,定时执行等。
    Apollo 平台进一步支持多环境部署(如云和边缘)。

阶段2 数据的平台化存储和管理

当开发工作逐渐迁移到 Palantir 平台后,数据的存储和管理成为下一个重点。如果代码已经运行在平台上,那么数据为什么不能也存储在平台中呢?

Palantir 在这一阶段引入了 Dataset 和本体(Ontology)模型,构建了平台化的数据管理能力。Dataset 作为数据的核心容器,支持结构化和非结构化数据的存储;本体则定义了数据之间的语义关系,为数据提供了更高级的抽象层。此外,Palantir 接入了时序数据库,增加了对时间序列数据的支持,满足了金融、工业等领域对实时数据处理的需求。

同时,也把数据集的变更增加为一个触发条件。例如,当某个 Dataset 发生变化时,平台可以自动触发预定义的操作,如运行一段代码或更新其他数据集。

阶段3 抽象Action Function

在本体已经定义了DataSet以及数据集之间关系的基础上,通过Action、Function的定义,同时Action、Function可以通过拖拉拽简单地生成,无需书写代码。对于难以无码的复杂逻辑,还可以通过定制代码来书写。

其实Workflow和Pipeline都是在更高层次、更简便地操作代码的手段而存在,底层实现上:

  • Pipeline = Datasets+Builds+Schedules
  • Workflow = Schedules + Builds + Jobs

阶段 4:AIP 的智能决策赋能

在Foundry坚实的基础上,Palantir 2023 年推出了 AIP(人工智能平台)整合大语言模型(LLM)与 Foundry 数据,自动化复杂决策。其核心功能包括:

  • 自然语言处理:用户通过对话界面查询数据或生成分析,如“预测下季度库存需求”。
  • 自动化工作流:基于 Ontology,AIP 驱动智能决策,例如优化供应链或调度资源。
  • 实时推理:结合时序数据,AIP 支持动态预测,如医疗资源分配或工业故障检测。

总结


图:笔者设想的企业使用Foundry路线图

本文分析了Palantir Foundry的技术实现路径,笔者认为Palantir Foundry 的技术演进展现了一个从“定制”到“平台原生”的清晰路径。应用从分散的定制代码,逐步迁移到平台上运行,扎根于平台的数据和触发机制,最终成为完全依赖平台功能的原生应用。

令人深思的经历

曾经历过这样的事情,平台侧要求应用提供满足平台特有格式的交付件,经过多次协商,最终还是应用侧与平台侧一起开会,由平台侧帮助应用侧输出。

另一件事,Kubernetes Yaml以其独特、强大的合并属性能力闻名于江湖。应用侧对Kubernetes Yaml不熟悉,新手想要把环境上的Yaml导出直接作为标准交付件,虽然也行,但是包含了很多噪音,环境上的id、环境上的annotation、时间戳等等。

私有化格式的交付困境

越来越多的软件将自己定位为”平台”,无论是微信、飞书这样的国民应用,还是各类企业级软件。但平台交付的过程中,一个普遍存在的问题是:许多平台要求合作伙伴或第三方开发者使用其私有化的交付格式。这种私有化格式往往存在诸多问题:

  • 学习成本高,难以掌握。
  • 文档不完善,依赖平台方支持。
  • 迁移困难,形成供应商锁定。
  • 最终往往仍需平台方投入人力协助。

软件交付应该标准化

软件交付应该使用标准的格式,这有助于降低合作伙伴的接入成本,提高自身的可扩展性,尤其在AI辅助研发的现状下,采用标准的格式更有利于AI理解和生成代码。

交付件 标准格式 使用场景
Java库 Jar包 作为依赖库被其他Java项目引用和集成,需要发布到Maven仓库。
应用镜像 标准镜像包 以容器方式交付,确保运行的一致性。(但如x86、armv8、armv7)的差异依然存在。
应用部署(I层资源已具备) helm、docker compose 商用场景多用Helm包,单机伪集群/组合方式多用docker compose。
应用部署及I层资源创建 Terraform 需要交付底层基础设施或云服务的场景,如整个应用运行环境。

如果实在要使用私有的格式,可以对标准格式做一些裁剪/扩展(Kubernetes的annotation),将标准格式转化到私有格式。

在软件开发中,健壮的异常处理是编写高质量代码的关键。本文将探讨现代编程语言中的通用异常处理方法,帮助你优雅地处理异常并写出健壮的代码。我们将不拘泥于某种语言,而是讨论一些普遍适用的策略。

异常链概述

现代编程语言通常将异常视为一条单向链表,链表中的节点包含根本原因和相关的上下文信息。例如:

graph TD
    C --> D[MicroServiceError, call user service failed]
    B --> C[DatabaseError, select * from user failed]
    A --> B[HttpError, http://localhost:6379 failed]
    A[SocketError, localhost, 6379 connect failed]

异常就这么向外传播也不错,但是抽象是会泄露的,正常的时候顺风顺水,异常就需要判断一下,比如一个很常见的需求,文件已存在异常,就当做成功处理,用Java来写就是这样

1
2
3
4
5
if (exception instanceof FileAlreadyExistsException) {
log.info("file already exists");
return SUCCESS;
}
throw exception;// or wrap it

综上来看,我们对现代编程语言的需求就是,能组织异常链,判断异常是否是某类异常,把异常用字符串的形式打印出来。

当我们在构筑一个library的时候,应该尽可能保持完整的异常链,除非你认为这个异常在library内可以处理,比如上面的情况。并且应该在项目的README,或者项目的某个文件中,详细地列出本library可能抛出的异常,以及异常的含义。

我们在opengemini-client-go中就有这样的例子,我们在errors.go中定义了所有可能的异常,以及异常的含义。

有些时候,我们构筑的不是library,出于隐藏内部实现或者是向终端用户隐藏逻辑上的低级错误,我们会对异常进行处理,比如常见的

1
2
3
4
5
6
if (exception instanceof DuplicateKeyException) {
log.info("duplicate key");
return new ServiceException("already exists");
}
// many if else
throw new ServiceException("unknown error"); // or just internal error

题外话,由于Java只能判断本级的异常类型,你会经常看到getCause的代码,比如Apache Pulsar项目中的

1
2
3
4
if (exception.getCause() != null
&& exception.getCause() instanceof PulsarClientException.InvalidServiceURL) {
throw new MalformedURLException(exception.getMessage());
}

包括层次一多,甚至可以看到递归代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private static Throwable mapToBkException(Throwable ex) {
if (ex instanceof CompletionException || ex instanceof ExecutionException) {
return mapToBkException(ex.getCause());
}

if (ex instanceof MetadataStoreException.NotFoundException) {
BKException bke = BKException.create(BKException.Code.NoSuchLedgerExistsOnMetadataServerException);
bke.initCause(ex);
return bke;
} else if (ex instanceof MetadataStoreException.AlreadyExistsException) {
BKException bke = BKException.create(BKException.Code.LedgerExistException);
bke.initCause(ex);
return bke;
} else if (ex instanceof MetadataStoreException.BadVersionException) {
BKException bke = BKException.create(BKException.Code.MetadataVersionException);
bke.initCause(ex);
return bke;
} else if (ex instanceof MetadataStoreException.AlreadyClosedException) {
BKException bke = BKException.create(BKException.Code.LedgerClosedException);
bke.initCause(ex);
return bke;
}

return ex;
}

Go在这里易用性做的不错,支持了errors.Iserrors.As,可以判断异常链中是否包含某个异常,也可以直接获取异常链中的异常。不过如果异常链里面有两个一模一样类型的异常,你想精准取到其中一个就比较困难,不过这在实际场景中非常少见。

这里,我们说异常链发生了变更,那么什么时候打印日志也比较明确了,当异常链发生变更的时候打印,保证完整的堆栈信息用于问题分析。这也可以保证在一条链的过程中,有且仅有一次打印日志。

在异常链发生终止,比如转化为http content,或者是print到console的时候,要不要打印日志呢?这个问题有些见人见智,这取决于你的用户在report问题的时候,会不会携带http content或者是console output,如果不会,那么你就需要打印日志,如果会,那么你就不需要打印日志。

Java里面,比起将底层的error抛出,我们更倾向于定义一个符合本library抽象层级的异常,并在方法的签名中只返回这个异常,一方面使得下层library的异常如果发生变化,本library依然是编译兼容的,另一方面也更符合抽象层级。

但是在Go里面,事情就更复杂一些,我愿意称之为类型的细化具备传染性,一旦你将某个方法的签名不返回interface,而是返回一个具体的类型,比如

1
2
3
4
5
6
func (c *Client) CallService() (Result, *ServiceError) {
if failed {
return nil, &ServiceError{Code: 500, Message: "service error"}
}
return result, nil
}

然后有一个方法调用了它

1
2
3
4
5
6
7
8
9
10
11
12
13
func main() {
err := MakeFriend()
if err != nil {
panic(err)
}
}

func (c *Client) MakeFriend() (Result, error) {
err := io.Read("friend_list.txt")
if err != nil {
return nil, err
}
return CallService()

这下就麻烦了,当*ServiceError转化为error, nil的ServiceError指针转化为error的时候就不是nil了,这很致命,是的,这非常致命。即使CallService()成功了,main函数还是会panic。

把这个叫做传染性还是比较准确的,异步的代码、鸿蒙的ets都具备一样的性质,他们会不断向上传播,我对这个词还是比较满意。

综上,Go里面,我们可以构筑具体的异常,但是在不能确保上层一直都是用这个细化类型的情况下,接口还是返回error interface。

漫谈了许多,我简单做一个总结

  • 现代编程语言的异常是一条链
  • 现代编程语言应该具备构筑异常链,判断异常是否是某类异常,异常打印的能力
  • 设计符合抽象层级的异常
  • 构筑一个library的时候,尽可能保持完整的异常链,在项目的README,或者项目的某个文件中,详细地列出本library可能抛出的异常,以及异常的含义
  • 在异常链发生变更的时候进行日志打印

消费组名称

  • 共享消费者使用微服务名称,比如(DeviceManager)
  • 广播消费者使用微服务名称+唯一标识,比如
  • kubernetes部署场景下可以将pod名称的唯一部分作为唯一标识,比如下图的nginx可以使用5d4f5c59f8-7hztx作为唯一标识
    1
    2
    3
    4
    5
    $ kubectl get pod
    NAME READY STATUS RESTARTS AGE
    nginx-deployment-5d4f5c59f8-7hztx 1/1 Running 0 2d3h
    nginx-deployment-5d4f5c59f8-xvbnm 1/1 Running 0 2d3h
    redis-5f67c8d8c9-4g2h3 1/1 Running 0 10h
    • pod的IP地址
    • UUID

数据库表

  • 数据库表名使用单数。
  • 数据库的主键,要考虑对应实体物理上是否唯一。
  • 数据库可以分为多个列组合唯一、单列唯一、是否有唯一索引、是否有二级索引。

Unicode起源

ASCII

ASCII(American Standard Code for Information Interchange)是一种字符编码标准,它使用7位二进制数来表示128个字符,包括大小写字母、数字、标点符号、控制字符等。ASCII编码是由美国国家标准协会(ANSI)制定的,于1963年发布,是最早的字符编码标准之一。

ASCII不够用了

随着计算机不仅仅用于英文,而是用于全球各种语言,ASCII编码已经不能满足需求,针对不同语言的编码方案也应运而生,这其中诞生了很多编码方案,比如GB2312、BIG5、ISO-8859等,这些字符集典型的就是将ASCII的最高位利用起来,将7位扩展到8位,这样就可以表示256个字符。比如ISO-8859-1就是将ASCII的最高位利用起来,表示了拉丁字母。ISO-8859-5表示了西里尔字母。

这些字符集各自不包含全部的字符,而且不兼容,这就导致了字符集混乱。这导致在一个文件中混用多种字符成为了不可能完成的事情。而Unicode改变了这一切,它的愿景就是Unicode官网中说到的。

1
Everyone in the world should be able to use their own language on phones and computers.

在早期,Unicode曾想过固定使用16位来表示字符,这就是UCS-2编码,也是UTF-16的前身,后面发现固定16位字符还是不够用,这才发展成了我们现在熟知的Unicode。

Unicode介绍

Unicode是一个文本编码标准。Unicode通过一个唯一的数字来定义每个字符,不管平台、程序或语言。这个数字叫做码点(code point)。Unicode码点是从0x000000到0x10FFFF(十六进制),书写上通常使用U+打头,跟上至少4位十六进制数(不足则补0),如U+0041(字母A)、U+1F600(emoji 😀),理论上,Unicode可以定义1114112个字符。

Unicode的码点跟字符是怎么对应的呢?Unicode将这些码点分成了若干个区段,每个区段称为一个平面(plane),每个平面包含65536(对应低位的0x0000~0xffff)个码点。Unicode总共有17个平面,编号从0到16。Unicode的码点分布如下:

平面编号 码点区间 英文缩写 英文名 中文名
0 号平面 U+000000 - U+00FFFF BMP Basic Multilingual Plane 基本多文种平面
1 号平面 U+010000 - U+01FFFF SMP Supplementary Multilingual Plane 多文种补充平面
2 号平面 U+020000 - U+02FFFF SIP Supplementary Ideographic Plane 表意文字补充平面
3 号平面 U+030000 - U+03FFFF TIP Tertiary Ideographic Plane 表意文字第三平面
4 号平面 ~ 13 号平面 U+040000 - U+0DFFFF / 已分配,但尚未使用 /
14 号平面 U+0E0000 - U+0EFFFF SSP Supplementary Special-purpose Plane 特别用途补充平面
15 号平面 U+0F0000 - U+0FFFFF PUA-A Private Use Area-A 保留作为私人使用区 (A区)
16 号平面 U+100000 - U+10FFFF PUA-B Private Use Area-B 保留作为私人使用区 (B区)

中文、英文均在0号平面,详细的分配可以参考Unicode的RoadMap

那么Unicode先定义了码点和字符之间的对应关系,但是如何存储在磁盘上,如何在网络中传输,这就引入了编码方式,编码方式决定了Unicode的码点如何转换为字节流。这就是Unicode定义的三种编码方式:UTF-32、UTF-16、UTF-8。

UTF-32(32-bit Unicode Transformation Format)

在介绍完Unicode之后,UTF-32是最简单、最容易想到的一种编码方式,直接将Unicode的码点以32位整数的方式存储起来。其中Rust的字符类型char,就使用32位值来表示Unicode字符。

但是这种方式也有很显然的缺点,就是浪费空间,实际Unicode的范围,只需要21位就可以表示了,变长编码就应运而生。

UTF-16(16-bit Unicode Transformation Format)

这里我想给大家讲一个背景知识,编码方案的扩展,通常会尝试去兼容旧的编码方案,这使得新的编辑器可以打开旧的文件,如果没有用到新的字符,那么新的文件也可以被旧的编辑器打开。这使得演进更加平滑,更易落地。

那就不得不先说一下UCS-2编码方案,如前所述,UCS-2想通过固定16位来表示字符,虽然它最终失败了,但是也影响了很多的系统,比如Windows、Jdk。

UTF-16编码就以兼容UCS-2编码、变长为两个目标,UTF-16的编码规则

  • ① 对于码点小于等于U+FFFF的字符,直接使用16位表示,兼容UCS-2
  • ② 对于码点小于等于U+10FFFF的字符,使用两个16位表示

这个补丁机制也常被人称作是surrogate。

对于变长编码来说,对于文件中的任意一个字符,怎么能判断出来这是场景①的字符,还是场景②的第一个字符?抑或是场景②的第二个字符?

Unicode给出的答案是,通过在BMP中舍弃U+D800到U+DFFF的码点,这个区间被称为代理对(surrogate pair),这个区间的码点不会被分配给字符,这样就可以通过这个区间来判断是场景①还是场景②。如果读取的时候,发现前两个字节是D8到DB,那么就是场景②的第一个字符;如果是DC到DF,那么就是场景②的第二个字符;否则就是场景①的字符。

  • 高代理(High-half surrogates):范围是0xD800~0xDBFF,二进制范围为1101 1000 0000 ~ 1101 1111 1111,这也代表着高代理的前六位一定是110110。
  • 低代理(Low-half surrogates):范围是0xDC00~0xDFFF,二进制范围为1101 1100 0000 ~ 1101 1111 1111,这也代表着低代理的前六位一定是110111。

那么聪明的读者应该分析出来了,使用两个16位表示,由于存在代理对的固定部分,剩余的有效位还剩下20位。这20位恰好可以覆盖从U+010000到U+10FFFF的码点范围。由于U+0000-U+FFFF已经在场景①中覆盖,通过将码点减去0x10000,范围就变成了0x000000~0x0FFFFF,恰好是20位整数。

UTF-8(8-bit Unicode Transformation Format)

UTF-8的编码规则

  • ① 对于码点小于等于U+007F的字符,直接使用8位表示,兼容ASCII。
  • ② 对于码点小于等于U+07FF的字符,使用两个8位表示,其中有效位为11位。
  • ③ 对于码点小于等于U+FFFF的字符,使用三个8位表示,其中有效位为16位。
  • ④ 对于码点小于等于U+10FFFF的字符,使用四个8位表示,其中有效位为21位。
  • ⑤ 使用n个字节(n>1)来表示一个字符时,第一个字节的前n位都是1,第n+1位是0,后面的字节的前两位都是10

那么对于一个字节,就可以通过首位是不是1,来判断是1个字节还是n个字节,再通过第二个字节判断是否是首位,最后通过首位来判断字节的个数。

由于UTF-8的有效位最大可达21位,这也就使得UTF-8不用像UTF-16那样减去0x10000。

通过兼容ASCII,最短只用1个字节,这使得UTF-8成为了堪称最流行的编码方式,如果不需要兼容UCS-2,那么几乎可以说UTF-8是最好的选择,堪称当前事实上的标准。值得一提的是,UTF-8的主要设计者,也是Unix的创始人之一,Go语言的设计者之一,Ken Thompson

扩展知识

JDK17中英文字符集内存占用量降低了一半

读者可能会觉得JDK17中中文字符内存占用降低一半是从UTF-16切换到UTF-8导致的,但实则不然,对于JDK来说,切换一种编码方式可谓是伤筋动骨,JDK17通过了JEP254提案,通过添加一个标志位,如果字符串的字符都是ISO-8859-1/Latin-1字符,那么就使用一个字节进行存储。

本文包含,Gin项目推荐布局,一些最佳实践等等。

Gin项目推荐布局

假设项目名称叫Hearth

  • xxx、yyy代表大块的业务区分:如用户、订单、支付
  • aaa、bbb代表小块的业务区分:如(用户的)登录、注册、查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
example/
|-- cmd/
| |-- production/
| |-- hearth.go
| |-- local/
| |-- hearth_local.go
|-- pkg/
| |-- apimodel/ 存放所有的ApiModel,用oapi-codegen解析uadp yaml来生成
| |-- boot/
| |-- boot.go //装备Struct,用于Lauch整个项目
| |-- handler/
| |-- xxx/
| |-- xxx_aaa_handler.go
| |-- xxx_bbb_handler.go
| |-- yyy/
| |-- yyy_model.go
| |-- yyy_aaa_handler.go
| |-- yyy_bbb_handler.go
| |-- xxx/
| |-- xxx_aaa_model.go // 存放持久化model,如数据库表,消息中间件结构,redis结构等
| |-- xxx_aaa_service.go
| |-- yyy/
| |-- yyy_bbb_model.go
| |-- yyy_bbb_service.go
| |-- ignite/
| |-- ignite.go
| |-- ignite_test.go
| |-- influx/
| |-- influx.go
| |-- influx_test.go
|-- docker-build/
| |-- scripts/
| |-- start.sh
|-- Dockerfile

放弃的布局方式

此种布局比较适合独立的包,对api结构体的操作复用较差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
example/
|-- cmd/
| |-- production/
| |-- hearth.go
| |-- local/
| |-- hearth_local.go
|-- pkg/
| |-- boot/
| |-- boot.go //装备Struct,用于Lauch整个项目
| |-- handler/
| |-- xxx/
| |-- xxx_model.go // 将大块业务的model也放在这里,可以使用oapi-codegen来生成结构体
| |-- xxx_aaa_handler.go
| |-- xxx_bbb_handler.go
| |-- yyy/
| |-- yyy_model.go
| |-- yyy_aaa_handler.go
| |-- yyy_bbb_handler.go
| |-- xxx/
| |-- xxx_aaa_model.go // 存放持久化model,如数据库表,消息中间件结构,redis结构等
| |-- xxx_aaa_service.go
| |-- yyy/
| |-- yyy_bbb_model.go
| |-- yyy_bbb_service.go
| |-- ignite/
| |-- ignite.go
| |-- ignite_test.go
| |-- influx/
| |-- influx.go
| |-- influx_test.go
|-- docker-build/
| |-- scripts/
| |-- start.sh
|-- Dockerfile
0%