跳到主要内容

MongoDB 常用查询语句/技巧

· 阅读需 2 分钟

转换为 UTC 时间字符串​

// 转换为 UTC 时间字符串
db.collection.find({
timestamp: { $gte: new Date( new Date("2025-03-22T00:00:00+08:00").toISOString()) }
});

创建视图​

标准视图使用底层集合的索引。

// ==============  AI800 A相进线温度  ==========
db.AI800.drop();
db.createView(
"AI800", // 视图名称
"equipment-1123", // 源集合
[
{ $match: { "code": "AI424" } }, // 过滤条件
]
)

db.AI800.find();

MongoDB 储存空间优化​

数据库状态查询​

db.status()

db.stats(1024*1024); // 输出单位为 MB

查询结果说明:

{
"db": "tmp_dev",
"collections": Long("275"),
"views": Long("0"),
"objects": Long("7891781"), // 文档总数
"avgObjSize": 234.529085766572, // 单个文档的平均大小,单位是:byte
"dataSize": 1765.11019039154, // 逻辑大小,不压缩的情况下,数据大小
"storageSize": 299.5, // 物理大小
"indexes": Long("281"), // 索引数量
"indexSize": 123.61328125, // 索引占用的存储空间
"totalSize": 423.11328125, // 总存储空间 = storageSize + indexSize
"scaleFactor": Long("1048576"), // 缩放因子, 1048576 = 1024 * 1024, 即 MB
"fsUsedSize": 126627.28515625, // 文件系统已使用的存储空间
"fsTotalSize": 181303.5546875, // 文件系统总存储空间
"ok": 1
}

空间优化​

1. 删除 collection 是可以直接释放空间的

2. 删除 collection 里面的数据是不会直接释放空间,需要执行 db.runCommand({compact:"collectuinName"})

优化脚本​

for (var collectionName of db.getCollectionNames()) {
// 清空数据
if (collectionName.startsWith("equipment")
|| collectionName.startsWith("transformer")
|| collectionName.startsWith("circuit-breaker")) {
// db.getCollection(collectionName).deleteMany({});
// db.runCommand({
// compact: collectionName
// })
db.getCollection(collectionName).drop();
}
}

字段排序​

.sort(), 1 增序, -1 降序。

db.getCollection("transformer-1000").find({
"code": "AI801"
}).sort({
"dataTime": -1
}).limit(1000);

hacker

· 阅读需 12 分钟
  1. 动手实践,查看文档反复循环。动手实践可以把脑海中陌生知识转为熟悉技能。阅读文档可以补充新的学习素材。
  2. 学会一个 Linux 基本操作(man、grep、vim、输入输出重定向操作符),编辑器工具,要玩的炉火纯青,每个快捷键烂熟于心。
  3. Desk checking,在编译前检查代码。(code review)
  4. 出现 bug 时,思考 2 个问题
    • 哪些设计或编程习惯导致了这个 bug ?
    • 哪些习惯能系统性的预防此类 bug ?
  5. 项目延期就要延个彻底,预留足够多的时间来处理未完成的事情
  6. 砍功能也要彻底,不要想着这个功能以后会用。(删除注释掉的代码同理)
  7. 做工程需要清醒的头脑,不要沉迷于长时间解决某个问题。(10:00 am and 2:00 pm 效率最高)

原文

Merc Release 2.1   
Sunday 01 August 1993

Furey mec@shell.portal.com
Hatchet hatchet@uclink.berkeley.edu
Kahn michael@uclink.berkeley.edu

我开 MUD 服务器就是为了学 C 语言编程!​

Yeah, right.

本文旨在记录我们的知识、经验和理念。无论您处于什么水平,我们都希望这份文档能帮助您成为更优秀的软件工程师。

请记住,工程实践才是根本,任何文档都无法替代您自己的思考、学习和动手实践。

学习之道​

(1) 动手实践
(2) 查阅文档
(3) 继续实践
(4) 再读文档
(5) 深入实践
(6) 反复研读
(7) 持续实践
(8) 温故知新
(9) 明白了吗?

人脑在单次学习中能吸收的新知识量是有限的。动手实践虽然不会带来太多新内容,却能把脑海中的" 陌生知识"转化为"熟悉技能"。阅读文档虽无法让知识变熟悉,却能持续补充新的学习素材。

多数人即便看了文档也很少重温,达到基本够用就止步不前。但现代操作系统、编程语言、网络技术乃至应用软件, 都不可能一蹴而就掌握。必须反复经历"学习-实践"的循环才能真正精通。

环境配置​

计算机:运行 Merc 程序的各类主机设备,无论体积大小。每台计算机都有其_制造商_和_型号_标识。以下是您可能会遇到的常见厂商及型号列表:

	Manufacturer	Model
------------ -----
Sun Sun-2
Sun Sun-3
Sun Sun-4
DEC Vax 5000
DEC Vax 5900
IBM RS/6000
NeXT NextCube
Sequent Symmetry
Sequent Balance

就硬件而言,Merc 可在任何 32 位硬件上运行。

操作系统:计算机上运行的最低层级程序。大多数常见计算机运行 Unix 或其变种版本,如 SunOS、Ultrix、AIX、Mach 或 Dynix。注意这些变种名称多以 IX 结尾。

Unix 系统主要分为两大"家族":伯克利 Unix(由著名的加州大学伯克利分校开发)和 System 5 Unix(由 Unix 的鼻祖贝尔实验室开发)。

最常见的非 Unix 操作系统是 VMS(DEC 公司为其 VAX 计算机开发的专有系统)。在个人电脑领域,你会看到 IBM PC 及兼容机使用的 MS-DOS 和 OS/2 系统,以及苹果麦金塔电脑的 MacOS 系统。

先说清楚:VAX不是操作系统,而是 DEC 公司一系列计算机的名称。很多 VAX 机运行 VMS 系统,但运行伯克利 Unix 或 Ultrix 系统的 VAX 机更多。运行 Unix 的 VAX 机与其他运行 Unix 的机器共性更多,反而与运行 VMS 的同系列机型差异更大。

就操作系统兼容性而言,Merc 可在支持伯克利 Unix 版 TCP/IP 网络的 Unix 及其变体系统上运行。它也支持 MS-DOS 系统,但仅限于单用户模式。经过适当修改,Merc 可以移植到任何提供 telnet连接 TCP 服务的操作系统。

编程语言:Merc 采用 C 语言编写。美国国家标准协会(ANSI)制定了 C 语言规范,Merc 完全遵循 ANSI 标准 C 语言规范开发。

最流行的 ANSI 标准 C 编译器是自由软件基金会开发的 GNU gcc编译器,可通过 prep.ai.mit.edu 匿名 FTP 获取。Merc 在 Gcc 1.38 版本下编译良好,因此您可以直接使用 1.42版本,无需安装更庞大的 2.X 系列。

您并非必须使用 gcc 编译器。运行 AIX 操作系统的 IBM RS/6000 已预装 ANSI C 编译器,NeXT 工作站亦是如此(其标准cc编译器实际就是 GNU C 编译器)。任何符合 ANSI 标准的编译器都能正常使用。

遗憾的是,目前仍有大量设备未配备 ANSI 标准 C 编译器(Sun工作站在这方面最为典型)。您可以通过mktrad脚本尝试用非 ANSI(传统)C 编译器编译 Merc,具体操作请参阅 trad.txt 文档。

若不清楚电脑的品牌型号、操作系统以及 C 编译器是 Ansi 还是非 Ansi 标准,您需要先查明这些信息。

Unix 基础工具​

man -- 提供在线手册页查询功能

grep -- 全称是"全局正则表达式打印"

vi、emacs、jove -- 编辑器随你挑,但必须玩到炉火纯青,每个快捷键都要烂熟于心

ctags -- 为编辑器生成标签索引,让你能通过函数名直接跳转到源码任意位置

>、>>、<、| -- 输入输出重定向操作,要么找人演示,要么自己查man csh手册

这些都是程序员吃饭的家伙什。如果连这些基本功都不扎实,就像开车不会换挡一样寸步难行

调试方法论​

调试是门科学。你需要建立假设,基于假设做出预测,运行程序并输入测试数据,观察其行为,最终验证或推翻假设。

优秀的假设能做出令人惊喜且最终应验的预测——这是其他假设无法做到的独特预见。

调试的首要原则是从源头避免制造 bug。这话听着简单,可惜人们总是置若罔闻。

编写程序时若出现任何错误或警告,务必先修正再继续。 C 语言的设计允许许多有缺陷的编码方式合法通过,但足够智能的编译器(比如开启-Wall选项的 gcc)会发出警告。花几分钟检查并修复这些警告代码,能为你省下数小时的排错时间。

1993 年时,"Desk checking"(代码审阅)这门手艺几乎失传了,实在可惜。你应当在编译前就进行代码自查,并定期复查以保持思路清晰、发现新问题。如果团队中有人专职审核他人代码,这个人发现的错误会比其他人加起来都多。

人工逐行检查代码每小时能覆盖数百行。顶尖程序员首轮编写的代码准确率约 99%,即每百行仍有一个漏洞。而你并非顶尖选手... 因此通过桌面检查每小时会发现多个漏洞。这堪称最高效的调试手段——比起在崩溃程序中耗费数小时才能揪出一个错误,效率简直天壤之别。

进阶技巧当属历史悠久的"打印语句调试法"——在代码中插入 print 语句追踪变量值。在 Merc 代码体系中,可调用 printf 或 fprintf 在关键节点输出关键数值。何时何处输出这些数值堪称调试艺术,唯有实践方能掌握其中精髓。

若尚未掌握操作系统中的输出重定向技巧,现在正是学习良机。Unix 系统下输入 man csh 命令,重点研读 > 操作符章节。同时必须厘清标准输出(如 printf 输出)与错误输出(如 fprintf 输出)的本质区别。

归根结底,如果不先理解程序的运行机制,就无法真正修复它。强大的调试工具能帮你收集数据,但它们既不能解读数据,也无法解决根本问题——这些只有你能做到。

发现 bug 时...你的第一反应往往是修改代码、消除表象就宣布修复完成。且慢!你看到的 bug 通常只是更深层问题的症状。应该追根究底,彻底参透这个 bug 的价值后,再让它"魂飞魄散"。

此外,发现 bug 时要问自己两个问题:"最初是哪些设计和编程习惯导致了这个问题?"以及:"哪些系统性习惯能预防此类 bug 的产生?"

调试工具篇​

当 Unix 进程访问无效内存地址时,或(较罕见地)执行非法指令,或(更罕见地)发生其他异常时,Unix操作系统会接管控制权。该进程无法继续执行且必须被终止。但在终止前,系统会执行一项辅助操作:创建一个名为core的文件,并将该进程的完整数据空间写入其中。

因此,dumping core 并非问题的成因,甚至不是问题的直接表现。这是操作系统为帮助开发者定位导致进程崩溃的致命错误而提供的诊断机制。

开发者需使用调试器分析core文件。Unix 系统最常用的调试器是adb和gdb ,偶尔也会遇到dbx。典型启动方式如:adb merc或gdb merc core。

调试时首先要做且往往唯一需要做的就是获取堆栈追踪。在adb中对应命令是$c,gdb 中则是backtrace。

堆栈追踪能显示程序崩溃时所在的函数及其调用链,调试器还会列出这些函数的参数。要正确解析这些参数并运用更高级的调试功能,需要扎实的汇编语言编程功底。

如果你能使用名为Purify的程序...务必掌握其用法。

性能分析​

程序性能分析步骤如下:

(1) Remove all the .o files and the merc executable:

rm *.o merc

(2) Edit your makefile, and change the PROF= line:

PROF = -p

(3) Remake merc:

make

(4) 照常运行 merc。当运行足够长时间获得良好的性能分析基准后,使用 shutdown 命令关闭游戏。如果游戏崩溃或从外部终止进程,将无法获取性能分析数据。

(5) 运行prof命令:

prof merc > prof.out

(6) 阅读 prof.out 文件。执行man prof命令了解输出格式说明。

要进行高级性能分析,可在步骤(2)中使用PROF = -pg参数,并在步骤 5 执行 gprof命令。gprof分析工具能生成详细报告,精确显示函数间的调用次数。 这些数据对调试和性能优化都极具价值。prof和gprof工具的可用性因系统而异。几乎所有 Unix 系统都内置 prof工具,但只有部分系统支持gprof。

进度、功能与质量的三角博弈​

现在简单谈谈项目管理

几乎所有项目最终都会面临进度、功能与质量的取舍困境。就像学生在截止前夜赶论文时,面临三个痛苦选择:要么迟交(牺牲进度),要么缩减内容(阉割功能),要么胡编乱造(降低质量)。

软件开发同样如此,团队常在三种方案间纠结:要么延期发布,要么砍掉功能,要么硬着头皮按时交付——而最后这种情况往往以失败告终。

这个决定最关键的是要意识到它确实是个决定。别指望会有奇迹发生来逃避。如果你不主动应对,外部环境就会替你做出选择。

假设你面临取舍选择了延期,那就别小打小闹——要延就延个痛快。如果想着"解决这个问题就尽快收尾",事后你准会后悔没多留点时间。但如果说"需要多干一天,干脆延期一周",到周末时完成质量反而更有保障。一次性大幅延期,总比重蹈覆辙地每天每小时拖延强。

如果要砍功能,同样要下狠手。别畏首畏尾地幻想"等有空再做"。这个功能已经彻底出局了,抓住需求简化的机会能省则省!

关于如何降低质量我给不出什么建议,这永远是我在项目中最后才会考虑的妥协方案。

睡眠之道​

道理简单却深刻:做工程需要清醒的头脑。

连续数小时埋头解决问题既轻松又诱人。人们会进入一种"心流"状态,满脑子都是问题,工作成果源源不断地涌现。许多作家描述过这种体验——仿佛亲眼目睹故事展开,只需将所见记录下来,就能一页接一页地创作。软件工程师们也常有类似感受,代码似乎自发地从指尖流淌而出。

我相信最有价值的工作往往诞生于这种状态。

但根据我的经验,"心流"状态往往在不知不觉中悄然消退。等我反应过来时,新工作已经不再行云流水般从手中产出,反而要花大量时间修正刚犯的错误。原本灵光闪现的头脑开始被各种疑虑和问题占据。

这时候很容易产生"再熬几小时"的冲动。"反正都在这儿了,刚才效率那么高,干脆通宵搞定算了?"千万别上当!这绝对是个陷阱!

我的建议是:回家吃饭、冲澡、睡觉,让自己满血复活。第二天再战。睡眠时大脑会自动处理问题,醒来时往往就有新思路。第二天上午10 点到下午 2 点的高效产出,绝对胜过熬夜到凌晨的混沌状态。

这个策略有个难点:如何重燃晨间斗志。如果是自主选择的项目还好说,但面对不得不做的任务时,就得权衡"早起没干劲"和"熬夜低效率"的利弊了。

程序员必读经典书单​

在浩如烟海的编程书籍中,这三本堪称经典:

Kernighan and Plaugher, 《编程风格要素》

Kernighan and Ritchie, 《C程序设计语言》

Brooks, 《人月神话》

scoop

· 阅读需 1 分钟

ref​

https://linux.do/t/topic/566873

Install​

1. 调整执行策略​

Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

2. 安装​

irm get.scoop.sh | iex

3. 检验是否安装成功​

Initializing...
Downloading...
Creating shim...
Adding ~\scoop\shims to your path.
Scoop was installed successfully!
Type 'scoop help' for instructions.
scoop help

Usage​

Bucket​

buckets 是存储应用程序清单的仓库,每个 bucket 包含了一系列 JSON 文件,每个文件对应一个软件包,描述如何下载、安装以及依赖等信息。

需要有 git 环境

scoop install git
scoop bucket add extras
scoop bucket add java
scoop bucket add nerd-fonts
scoop bucket add versions
scoop bucket add nonportable

更新 scoop 和 buckets

scoop update

更新安装的软件

scoop update *

安装软件​

scoop install 7zip
scoop install geekuninstaller
scoop install everything
scoop install fastcopy
scoop install apifox
scoop install localsend
scoop install screentogif
scoop install sumatrapdf

TIMESTAMP 的时间范围

· 阅读需 2 分钟

执行 sql 报错。

UPDATE inspection_plans t SET t.plan_end_time = '2041-04-08 10:41:03' WHERE t.id = 1

错误信息:

Data truncation: Incorrect datetime value: '2041-04-08 10:41:03' for column 'plan_end_time' at row 1

问题原因:TIMESTAMP 的范围是 '1970-01-01 00:00:01' UTC 到 '2038-01-19 03:14:07' UTC

解决方法:将 TIMESTAMP 改为 DATETIME 类型。

alter table inspection_plans modify plan_end_time datetime;

总结:

  • DATE 能够表示的时间范围:1000-01-01 到 9999-12-31 ,只记录年月日,不存储时分秒
  • TIME 能够表示的时间范围:'-838:59:59' 到 '838:59:59' ,只记录时分秒,不存储年月日
  • DATETIME 能够表示的时间范围:1000-01-01 00:00:00 到 9999-12-31 23:59:59 推荐
  • TIMESTAMP 能够表示的时间范围:1970-01-01 00:00:01 UTC 到 2038-01-19 03:14:07 UTC
  • YEAR 能够表示的时间范围:1901 到 2155
  • TIMESTAMP 和 DATETIME 的区别:
    • TIMESTAMP 是 UTC 时间,存储时会转换为 UTC 时间,取出时会转换为当前时区的时间
    • DATETIME 是本地时间,存储时不会转换为 UTC 时间,取出时也不会转换为 UTC 时间

解决Nginx多SSL配置中未匹配域名的默认回退问题

· 阅读需 2 分钟

问题现象​

我给 nginx 配置了多个server,分别设置了 ssl 代理,但是当其中一个服务未启动是,会访问到第一个 server 的内容。

server {
listen 443 ssl;
server_name a.domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://xxxx:10010;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 443 ssl;
server_name b.domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://xxxx:10086;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

当 10086 端口的服务未启动时,访问 https://b.domain.com 时,会出现 10010端口的服务的内容。 也就是 nginx 只会访问第一个 server 的内容。经过一番查找,发现是因为没有设置 default_server。

解决方案​

添加一个下面的 server 配置

server {
listen 443 ssl default_server;
server_name _;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
return 444; # 立即关闭连接
}

问题剖析​

Nginx 请求匹配机制​

  1. 端口监听优先级:Nginx优先匹配 listen指令相同的配置
  2. 域名精确匹配:在相同监听端口中,server_name最精确匹配的配置生效
  3. 默认回退机制:当没有匹配的server_name 时,自动选择:
    • 第一个定义的 server 块(未显式声明 default_server 时)
    • 标记为default_server的配置块

问题根源​

当后端服务不可达时,Nginx 的请求处理流程:

  1. 客户端请求 b.domain.com
  2. Nginx正确匹配到 b.domain.com 的 server 块
  3. 尝试代理到http://xxxx:10086时发现连接失败
  4. 错误配置导致匹配降级,回退到默认 server 块(第一个定义的 a.domain.com )

Nginx 反向代理 WebSocket 服务

· 阅读需 1 分钟
# 反向代理
server {
server_name domain.com;
listen 443 ssl;

# 通过 acme.sh 进行签署,具体见:https://blog.wangzhy.com/ssl#acmesh-%E7%AD%BE%E7%BD%B2%E8%AF%81%E4%B9%A6
ssl_certificate /etc/nginx/ssl/xxxx/fullchain.cer;
ssl_certificate_key /etc/nginx/ssl/xxxx/domain.com.key;

# SSL 增强配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers on;


location / {
proxy_pass http://host.docker.internal:9001;
# 基础代理头设置
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;

# 连接数限制
proxy_http_version 1.1; # 明确指定HTTP1.1
proxy_buffers 8 16k; # 缓冲区优化
proxy_buffer_size 32k;

# WebSocket 专用设置
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

# 调整超时时间(秒)
proxy_read_timeout 86400; # 保持长连接
proxy_send_timeout 86400;
}
}

TypeScript interface 关键字介绍

· 阅读需 3 分钟

interface 用于定义对象类型、函数签名、类结构等。

基础语法​

interface User {
name: string;
age: number;
isAdmin?: boolean; // 可选属性(Optional)
readonly id: number; // 只读属性
}
  • ? 表示非必须
  • readonly 表示只读

函数类型​

  1. 定义了一个函数签名,参数是 source keyword,返回值是 boolean 类型。
  2. 声明了一个 search 常亮,并赋值了一个箭头函数。
interface SearchFunc {
(source: string, keyword: string): boolean;
}

// src, kw 名称不需要与函数类型定义中一致,但参数类型需要一致。
const search: SearchFunc = (src, kw) => src.includes(kw);

使用场景​

约束回调函数

interface ClickHandler {
(event: MouseEvent, context: HTMLElement): void;
}

const handleClick: ClickHandler = (e, element) => {
console.log("Clicked on:", element.tagName);
};

统一 API 请求函数

interface ApiRequest {
(url: string, params: Record<string, string>): Promise<Response>;
}

const fetchData: ApiRequest = async (path, data) => {
return await fetch(path, { body: JSON.stringify(data) });
};

高阶函数参数

function higherOrderFunc(callback: SearchFunc) {
callback("Hello", "H"); // 调用时必须传入符合签名的函数
}

higherOrderFunc((s, k) => s.startsWith(k));

可索引类型​

// 数组类型
interface StringArray {
[index: number]: string; // 索引为数字,值为字符串
}

// 字典类型
interface NumberDictionary {
[key: string]: number; // 键为字符串,值为数字
length: number; // 明确声明的属性必须符合索引签名
}

使用案例:

// 用索引获取数组元素
interface StudentArray {
[index: number]: string;
}

let studentArr: StudentArray = ["Bob", "Fred"];
let student1: string = studentArr[0]; // 'Bob'
let student2: string = studentArr[1]; // 'Fred'

// 用索引获取对象属性
interface StudentObject {
[key: string]: number;
}

let studentObj: StudentObject = {};
studentObj['Bob'] = 1;
studentObj['Fred'] = 2; // { Bob: 1, Fred: 2 }

继承与扩展​

interface Animal {
name: string;
}

interface Dog extends Animal {
breed: string;

bark(): void;
}

// 多重继承
interface SuperDog extends Dog {
fly(): void;
}

类实现接口​

interface ClockInterface {
currentTime: Date;

setTime(d: Date): void;
}

class Clock implements ClockInterface {
currentTime: Date = new Date();

setTime(d: Date) {
this.currentTime = d;
}
}

混合类型​

interface Counter {
(start: number): string; // 函数签名
interval: number; // 属性
reset(): void; // 方法
}

// 实现示例
function createCounter(): Counter {
let counter = function (start: number) {
} as Counter;
counter.interval = 5;
counter.reset = () => {
};
return counter;
}

接口合并​

interface Box {
width: number;
}

interface Box {
height: number;
}

// 合并后等价于:
interface Box {
width: number;
height: number;
}

动态扩展接口​

interface Window {
myCustomProp: string; // 扩展全局 Window 类型
}

泛型接口​

interface ApiResponse<T> {
code: number;
data: T;
message?: string;
}

// 使用
const userResponse: ApiResponse<User> = {
code: 200,
data: {name: "Alice", age: 30}
};

环境变量替换

· 阅读需 1 分钟

可以在 Dockerfile、docker-compose.yml 中使用环境变量 替换${}。它允许你在构建镜像时动态地设置变量的值。

${} 语法​

基本形式: ${VAR}​

environment:
- APP_PORT=${APP_PORT}

默认值: ${VAR:-default}​

HOST_PORT 未设置时,采用默认值 8080

ports:
- "${HOST_PORT:-8080}:80"

强制: ${VAR:?error}​

DATA_PATH 如果未设置,会报错并退出。

volumes:
- "${DATA_PATH:?DATA_PATH must be set}:/data"

组合: ${VAR:-${DEFAULT_VALUE}}​

environment:
- DB_HOST=${DB_HOST:-${FALLBACK_DB_HOST}}

环境变量设置​

1. Shell 环境变量​

export APP_PORT=8080

2. .env 文件​

docker-compose 会自动加载项目根目录下的 .env 文件。

可以通过 --env-file 指定 .env 文件。 例如:

docker compose --env-file custom.env up -d

3. 命令行通过 -e --env 参数指定​

Dockerfile

docker -e APP_PORT=8080 run myapp

docker-Compose.yml

docker compose up -d  --env APP_PORT=8080

go:embed 指令

· 阅读需 2 分钟

[GoLang官方提案]https://go.googlesource.com/proposal/+/master/design/draft-embed.md

//go:embed 支持嵌入静态文件。

  1. 对于单个文件,可以嵌入为字符串和 byte slice
  2. 多个文件,支持嵌入为新的文件系统 FS
  3. 只支持嵌入为string, byte slice和embed.FS三种类型,这三种类型的别名(alias)和命名类型(如type S string)都不可以

嵌入字符串​

//go:embed hello.txt
var s string

嵌入为 byte slice​

//go:embed hello.txt
var b []byte

嵌入为fs.FS​

基本使用

//go:embed hello.txt
var f embed.FS

支持同一变量上嵌入多个文件

//go:embed hello1.txt
//go:embed hello2.txt
var f embed.FS

func main() {
data, _ := f.ReadFile("hello.txt")
fmt.Println(string(data))
data, _ = f.ReadFile("hello2.txt")
fmt.Println(string(data))
}

匹配模式

go:embed dirpath 次文件夹下的除了 . 和 _ 开头的文件/文件夹都会被嵌入(递归)。

如果要嵌入.和_开头的文件/文件夹,需要使用 go:embed dirpath/*。

注意:这个只能嵌入 dirpath 目录下的 .和_开头的文件/文件夹。子文件夹下的 . 和 _ 开头的文件/文件夹是不会嵌入。

使用​

将前端静态文件打包进 go 程序​

嵌入静态文件

//go:embed frontend/out/*
var buildFS embed.FS

输出日志,检查文件是否被正确嵌入

// 打印嵌入的文件系统内容,用于调试
fs.WalkDir(buildFS, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
log.Printf("嵌入文件系统中有: %s", path)
}
return nil
})

静态文件服务

// 创建一个子文件系统,将嵌入的文件系统 buildFS 中的 frontend/out 目录作为新文件系统的根目录
contentFS, err := fs.Sub(buildFS, "frontend/out")
if err != nil {
log.Fatalf("无法获取嵌入的前端文件: %v", err)
}

// 创建一个文件服务处理器
fileServer := http.FileServer(http.FS(contentFS))

// 为所有路径提供服务
r.PathPrefix("/").Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 打印请求路径,帮助调试
log.Printf("请求文件: %s", r.URL.Path)
fileServer.ServeHTTP(w, r)
}))

Go语言JSON结构体标签

· 阅读需 2 分钟

结构体标签详解​

type User struct {
ID int `json:"id"` // 字段重命名(服务端返回"id"字段)
Name string `json:"name"` // 保持字段原名
Email string `json:"email,omitempty"` // 空值时自动隐藏(避免空字符串污染JSON)
Secret string `json:"-"` // 彻底隐藏敏感字段(如密码)
Created time.Time `json:"created_at"` // 时间类型自动格式化为RFC3339字符串
}

序列化最佳实践​

// 带缩进的友好格式输出
data, _ := json.MarshalIndent(user, "", " ")
/*
{
"id": 1,
"name": "Alice",
"created_at": "2023-09-15T10:00:00Z"
}
*/

// 原始紧凑格式(适合网络传输)
binaryData, _ := json.Marshal(user) // []byte类型数据

反序列化注意事项​

var newUser User
err := json.Unmarshal(data, &newUser)

// 处理日期字段(需显式转换)
type CustomTime struct {
time.Time
}
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
// 自定义日期解析逻辑...
}

常见问题排查​

  1. 字段丢失:检查结构体是否导出(首字母大写)
  2. 时间格式错误:使用time.RFC3339格式字符串
  3. 零值问题:结合omitempty与指针类型*string
  4. 循环引用:避免结构体嵌套循环

高级技巧​

// 动态字段处理
type FlexibleStruct struct {
Extra map[string]interface{} `json:"-"` // 收集未定义字段
}

// 条件序列化
func (u User) MarshalJSON() ([]byte, error) {
// 自定义序列化逻辑...
}

动态类型处理​

// 解析未知结构 JSON
data := []byte(`{"name":"Bob","age":30}`)
var result map[string]interface{}
json.Unmarshal(data, &result)
fmt.Println(result["name"].(string))

// 部分解析(使用匿名结构)
var partial struct {
Name string `json:"name"`
}
json.Unmarshal(data, &partial)

自定义序列化​

// 实现 json.Marshaler 接口
type CustomTime time.Time

func (ct CustomTime) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, time.Time(ct).Format("2006/01/02"))), nil
}

// 实现 json.Unmarshaler 接口
func (ct *CustomTime) UnmarshalJSON(data []byte) error {
// 自定义解析逻辑...
}

处理 HTML 转义​

buf := new(bytes.Buffer)
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false) // 禁用 &, <, > 转义
enc.Encode(data)