<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>imicola</title><description>人,进来</description><link>https://blog.imicola.com/</link><language>zh_CN</language><item><title>浮游叙事——2025 年终总结</title><link>https://blog.imicola.com/posts/%E6%B5%AE%E6%B8%B8%E5%8F%99%E4%BA%8B/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E6%B5%AE%E6%B8%B8%E5%8F%99%E4%BA%8B/</guid><description>浅浅回头看看</description><pubDate>Wed, 31 Dec 2025 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;浮游叙事——2025 年终总结&lt;/h1&gt;
&lt;p&gt;年终，再过几个小时就意味着2025年的离去，在每一年在尽头的时候，我们都会不禁感慨到今年的一年过的是如此的迅速，时间从来都是最无情的，用不快不慢的速度在向着前面走去，仿佛一个独行的满腔执念的人一样，全然不顾周围。&lt;/p&gt;
&lt;p&gt;每年的年终总结时间，想到的一个念头便是 “平凡”，甚至是枯燥。“平凡”的一年让我在回忆的时候无法想到最令我记忆深刻的事情，甚至无法让我回忆起什么事情是今年发生的，什么事情是去年发生的。人的记忆就是如此，去掉了一切不必要的，模糊的，细节的东西，只在脑海中留个“感觉”，这种感觉让我们能在看到照片时迸发，能在我们与好友交谈时突兀的冒出来，能在夜深人静的时候一个一个涌上心头。到内心充盈着这种回忆的感情的时候，我们不由得发现————2025好像也没那么枯燥。&lt;/p&gt;
&lt;p&gt;今年感受最深的一句话来自平台跳跃游戏蔚蓝中的“你能做到”。过去我总是用一种谨慎的，怀疑的目光来审视自己，以至于我开始就丧失了去做一件事情的动力。但是很多时候，我只是需要一个开始的理由，一个把行动的小球从高处推下的动力源。“你能做到”就是给我的这样的一个动力，没有什么利弊权衡，只是对未来自己的相信，对自己潜力的自信。&lt;/p&gt;
&lt;p&gt;一年的时间如此迅速，但是又如此漫长，我的生活被一条跨越中国南北的铁道线划成上下册；上册我在南方，回过荔园，去过珠海。在盛夏中看过海与烟花，和一群重要的人度过了高中生活的开朗DLC；下册我来到了北方，见过北方的风和雪，看过长春的地上跑的地铁，看过北京的天安门，见到了哈尔滨的雪花一层一层积累，在这里与另外一群重要的人度过了大学篇续作的第二章。这一切似乎看起来都是一些零零碎碎的小故事，但是一年怎么就不是这些小事日常拼凑起来的呢？&lt;/p&gt;
&lt;p&gt;当我真的去好好看看一年都不怎么回首的2025时候，也不免会发现一切都变了很多。从我到这个世界，变化不息。我们已经很难想象，在25年年初deepseek R1才被推出，我们也很难想象去年才高三的生活，明年要变成大三了。这种变化悄然的改变我们的生活和日常，如同一名初学者落入水中一般，浮游着摆动着。世界是大海，带来的浪花敲击着我们，我们在浪花的摇摆下浮游的记录着生活的故事。&lt;/p&gt;
&lt;p&gt;世界在网络线下，将我的生活再次划分为二,一面是引力沉重的现实，一面是飘浮着的数据；在那个现实的反面，在费伦的一个“小城”里体验过史诗的故事,登上过塞莱斯特的山顶，在银河系下看过文明的太空歌剧，在一个奇怪的井里找过蛋和兔子。同样的我见过了一个支离破碎的乐队变成两个乐队的故事，看过一个没有一个人却详细刻画了整个人类文明的故事。在另一种意义的浮游叙事下，我是参与者，亦是欣赏者，看向浮动的世界。&lt;/p&gt;
&lt;p&gt;2025亦有有些遗憾之事，我的打A之路并没有什么能拿的出手的牌子，有本想看的书一直只翻开的序章，许许多多的未完成，未成功依旧留在2025，并且会随着我一起带到2026。2025不是完美的，正如每一个年份都不是完美的一样，缺陷与遗憾仍然是2025组成的部分甚至是绝大部分。&lt;/p&gt;
&lt;p&gt;但是，你能做到，不是吗？&lt;/p&gt;
&lt;p&gt;2025和2024，2026没有本质区别，是银河系猎户臂下一个小小星系团的一个类地行星绕着它的恒星一圈的时间而已。因为有人的存在，我们赋予机械而物理的事实以意义，将岁月的印记留在岁月之中，这是属于人的浪漫。正如我上面说的，我们因为生活在世界的海洋中，所以我们需要锚点需要叙事需要存在的意义。人类就在这样不断压缩自己的过去的路上走向未来，望向过去只觉得短暂，看向未来又觉得漫长。&lt;/p&gt;
&lt;p&gt;生活细碎，万物成诗&lt;/p&gt;
&lt;p&gt;祝好，2026&lt;/p&gt;
</content:encoded></item><item><title>命令行的艺术(上)</title><link>https://blog.imicola.com/posts/%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%9A%84%E8%89%BA%E6%9C%AF%E4%B8%8A/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E5%91%BD%E4%BB%A4%E8%A1%8C%E7%9A%84%E8%89%BA%E6%9C%AF%E4%B8%8A/</guid><description>从 Windows Terminal 的配色、字体、背景，到 Zsh 与 PowerShell 的提示符、补全、模糊搜索与 fastfetch，一份&quot;如何配置好看且顺手的命令行&quot;的实战小教程。</description><pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;本文是来自对 &lt;a href=&quot;https://github.com/jlevy/the-art-of-command-line/blob/master/README-zh.md&quot;&gt;命令行的艺术&lt;/a&gt; 的拙劣模仿，结合了一些笔者的实际使用经验，得到的一个，&quot;如何配置好看且顺手的命令行&quot; 的小教程吧&lt;/p&gt;
&lt;h2&gt;终端环境&lt;/h2&gt;
&lt;p&gt;一个好的终端环境对命令行的配置至关重要，如果你的终端不能配置字体，不支持流输出，不支持背景图片配置，那我只能说你不是一个合格的 &quot;技术宅&quot;(&lt;/p&gt;
&lt;p&gt;笔者没怎么用过真机Linux，在此不做推荐。&lt;/p&gt;
&lt;p&gt;而 Windows 上，首推 Window Terminal (wt) ,作为微软良心且 &lt;a href=&quot;https://github.com/microsoft/terminal&quot;&gt;开源&lt;/a&gt; 的产品，wt 预安装在每个 Windows 用户的设备上, 拥有丰富的配置选项, 只不过大部分人都不知道怎么将一个黑色的,使用微软雅黑中文字体显示英文符号的超级无敌对比度的初始终端(或许是蓝色?)配置成好看的窗口&lt;/p&gt;
&lt;h3&gt;选择一个shell&lt;/h3&gt;
&lt;p&gt;在配置终端前，先选择一个适合自己的 shell 环境, 如果你是一个小白，那么你的默认 shell 环境大概率是 powershell 5 ,它已经过时了,现在建议你升级到&lt;a href=&quot;https://github.com/PowerShell/PowerShell&quot;&gt;powershell 7&lt;/a&gt; ，如果你有Agent或者Harness,告诉它需求，它会帮你的;如果没有，你可以尝试去 github 下载安装包，或者使用 &lt;code&gt;winget install pwsh&lt;/code&gt; 命令，使用 winget 包管理器完成安装&lt;/p&gt;
&lt;p&gt;下载安装完成后，你可能需要重启一次你的 wt 来导入 pwsh7 的配置文件，然后使用快捷键 &lt;code&gt;ctrl + ，&lt;/code&gt; 进入 wt 的配置界面，在 &quot;启动&quot; 选项卡中选择 &lt;code&gt;powershell&lt;/code&gt; 作为你的默认配置文件，如果这里有两个 &lt;code&gt;powershell&lt;/code&gt; ,选择黑色的那个&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;wt-default-profile.png&quot; alt=&quot;wt 启动设置中选择 powershell 作为默认配置文件&quot; /&gt;&lt;/p&gt;
&lt;p&gt;当然，如果你更加熟悉bash语法环境,你也可以试试用 &lt;em&gt;git bash&lt;/em&gt; 作为主力终端，使用 &lt;em&gt;git bash&lt;/em&gt; 的另外一个好处就是: &lt;strong&gt;在你跑一些 Agent 的时候, LLM不会和 pwsh 语法斗殴&lt;/strong&gt;
(笔者现在主力用6.1sol也会经常出现和pwsh语法打架的环节)&lt;/p&gt;
&lt;h3&gt;配置这个 shell 窗口&lt;/h3&gt;
&lt;p&gt;在 wt 的设计界面，左侧的选项卡向下滑，应该能看见你 wt 上 shell 的配置文件，请直接修改 &lt;em&gt;其他设置-外观&lt;/em&gt; ,除此以外的配置尽量保持默认选项，除非你真的知道你自己在干什么&lt;/p&gt;
&lt;p&gt;进入外观后，可以看到第一个 配色方案 文本，可以挑选自己喜爱的配色方案设置，但是说实话，wt 自带的配色方案不敢说好看喵，这里推荐一个 配色方案合集 &lt;a href=&quot;https://catppuccin.com/&quot;&gt;catppuccin&lt;/a&gt; ,这个提供了一整套好看的配色，而且覆盖了很多常见，对于我们需要的 wt 配色，&lt;a href=&quot;https://github.com/catppuccin/windows-terminal&quot;&gt;这个项目&lt;/a&gt; 就提供了解决方案，当然你现在直接看我下面的设置就行了，或者你直接让你的大模型帮你设置(&lt;s&gt;实际上这个文章中的所有内容大模型都可以帮你做好，你只需要知道改了什么就行&lt;/s&gt;)&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;在当前页面，你应该能看见一个 「打开JSON文件」 的按钮&lt;/li&gt;
&lt;li&gt;使用任意能编辑文件的编辑器打开,vscode,zed,甚至是记事本&lt;/li&gt;
&lt;li&gt;在 &lt;a href=&quot;https://github.com/catppuccin/windows-terminal&quot;&gt;这里&lt;/a&gt; 选择一个你喜欢的配色方案 (frappe, latte, macchiato, mocha).json 和 与其对应的 XXXTheme.json&lt;/li&gt;
&lt;li&gt;将 &lt;strong&gt;不带Theme&lt;/strong&gt; 的文件内容(如&lt;code&gt;macchiato.json&lt;/code&gt;)复制，并且如下面的格式粘贴到 wt 的配置文件中&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
	// 你的其他配置 (注意wt json不允许注释,这一行要删)
	&quot;schemes&quot;:
	[
		{
			&quot;background&quot;: &quot;#24273A&quot;,
			&quot;black&quot;: &quot;#494D64&quot;,
			&quot;blue&quot;: &quot;#8AADF4&quot;,
			&quot;brightBlack&quot;: &quot;#5B6078&quot;,
			&quot;brightBlue&quot;: &quot;#8AADF4&quot;,
			&quot;brightCyan&quot;: &quot;#8BD5CA&quot;,
			&quot;brightGreen&quot;: &quot;#A6DA95&quot;,
			&quot;brightPurple&quot;: &quot;#F5BDE6&quot;,
			&quot;brightRed&quot;: &quot;#ED8796&quot;,
			&quot;brightWhite&quot;: &quot;#A5ADCB&quot;,
			&quot;brightYellow&quot;: &quot;#EED49F&quot;,
			&quot;cursorColor&quot;: &quot;#F4DBD6&quot;,
			&quot;cyan&quot;: &quot;#8BD5CA&quot;,
			&quot;foreground&quot;: &quot;#CAD3F5&quot;,
			&quot;green&quot;: &quot;#A6DA95&quot;,
			&quot;name&quot;: &quot;Catppuccin Macchiato&quot;,
			&quot;purple&quot;: &quot;#F5BDE6&quot;,
			&quot;red&quot;: &quot;#ED8796&quot;,
			&quot;selectionBackground&quot;: &quot;#5B6078&quot;,
			&quot;white&quot;: &quot;#B8C0E0&quot;,
			&quot;yellow&quot;: &quot;#EED49F&quot;
		}
	],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;将 &lt;strong&gt;带Theme&lt;/strong&gt; 的文件内容(如&lt;code&gt;macchiatoTheme.json&lt;/code&gt;)复制，并且如下面的格式粘贴到 wt 的配置文件中&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;themes&quot;:
	[
		{
			&quot;name&quot;: &quot;Catppuccin Macchiato&quot;,
			&quot;tab&quot;:
			{
				&quot;background&quot;: &quot;#24273AFF&quot;,
				&quot;iconStyle&quot;: &quot;default&quot;,
				&quot;showCloseButton&quot;: &quot;always&quot;,
				&quot;unfocusedBackground&quot;: null
			},
			&quot;tabRow&quot;:
			{
				&quot;background&quot;: &quot;#1E2030FF&quot;,
				&quot;unfocusedBackground&quot;: &quot;#181926FF&quot;
			},
			&quot;window&quot;:
			{
				&quot;applicationTheme&quot;: &quot;dark&quot;,
				&quot;experimental.rainbowFrame&quot;: false,
				&quot;frame&quot;: null,
				&quot;unfocusedFrame&quot;: null,
				&quot;useMica&quot;: false
			}
		}
	],
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意这里的两个 json 键都是一级,不要放到其他的键下了&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;保存，然后退出这个 json 文件的编辑&lt;/li&gt;
&lt;li&gt;回到你的 shell 的配置界面，选择 配色方案 选项，现在你加入的主题应该会在选择框中&lt;/li&gt;
&lt;li&gt;选择它即可&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;然后是 「字体」 选项，你应该选择一个 &quot;好看且多功能&quot; 的字体集，好看是指字体在代码阅读层面是清晰且分明的，多功能指字体可以承担部分&quot;字符集&quot;的作用，这里特指带 &lt;em&gt;Nerd&lt;/em&gt; 或者 &lt;em&gt;NF&lt;/em&gt; 表示的字体，例如 &lt;code&gt;JetBrainsMono Nerd Font&lt;/code&gt;，但是我这里更加推荐 &lt;code&gt;Maple Mono NF CN&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;你可以在 &lt;a href=&quot;https://font.subf.dev/zh-cn/download/&quot;&gt;字体下载 | Maple Mono&lt;/a&gt; 这里下载，也可以直接点击 &lt;a href=&quot;https://github.com/subframe7536/maple-font/releases/download/v7.9/MapleMono-NF-CN-unhinted.zip&quot;&gt;这里&lt;/a&gt; 下载&lt;/p&gt;
&lt;p&gt;下载完成后你会得到一个压缩包,解压后在解压出来的文件夹中按下 &lt;code&gt;ctrl + a&lt;/code&gt;全选文件，右键后找到 &quot;安装&quot; 选项，如果没有，请在 &quot;显示更多选项&quot; 中查找&lt;/p&gt;
&lt;p&gt;安装完成后就可以回到 shell 的字体配置，选择刚刚安装好的 &lt;code&gt;Maple Mono NF CN&lt;/code&gt; 使用即可&lt;/p&gt;
&lt;p&gt;接下来的一些配置，基本都是字面意思, 「字号」「行高」「单元格宽度」保持默认即可&lt;/p&gt;
&lt;p&gt;往下滑可以看到 「背景图像路径」,优秀 技术宅 都会选择一张适合自己的图片作为终端背景喵(&lt;/p&gt;
&lt;p&gt;「拉伸模式」 选择 「均匀填充」,对齐选择 &quot;居中&quot;，不透明度选择 10% 最好(太高了容易看不清字)&lt;/p&gt;
&lt;p&gt;下面的有个 「透明度」设置，可以选择 75% 左右，然后根据个人喜好配置 「是否启用亚克力效果」，反正 &lt;em&gt;imicola&lt;/em&gt; 是挺喜欢的&lt;/p&gt;
&lt;hr /&gt;
&lt;h3&gt;配置 Shell&lt;/h3&gt;
&lt;p&gt;要想 shell 好用，不光是 shell 的窗口，shell 本身也值得精细配置，下面我会讲述linux 的 Zsh 和 pwsh 的关于我自己是如何配置的，当然你也可以自己选择你喜欢的方式，去折腾这一圈东西&lt;/p&gt;
&lt;h4&gt;Zsh配置&lt;/h4&gt;
&lt;p&gt;先看效果:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;zsh-completion.png&quot; alt=&quot;zsh 补全效果 w-90%&quot; title=&quot;补全效果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;fastfetch.png&quot; alt=&quot;fastfetch 配置效果 w-90%&quot; title=&quot;fastfetch配置&quot; /&gt;&lt;/p&gt;
&lt;p&gt;要实现这样的效果，我们首先需要安装系统包，使用你对应的发行版(&lt;em&gt;imicola&lt;/em&gt;这里是Arch)安装&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S zsh eza fzf fastfetch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后我们这个是以 &lt;code&gt;oh-my-zsh&lt;/code&gt; 为框架的命令行系统,安装 &lt;code&gt;oh-my-zsh&lt;/code&gt; 并且将 &lt;code&gt;zsh&lt;/code&gt; 设定为 &lt;strong&gt;默认shell&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sh -c &quot;$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)&quot; &quot;&quot; --unattended
chsh -s /usr/bin/zsh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着我们装 &lt;em&gt;spaceship&lt;/em&gt; 主题，当然你也可以选择你喜欢的主题&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ZSH_CUSTOM=&quot;${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}&quot;
git clone https://github.com/spaceship-prompt/spaceship-prompt.git &quot;$ZSH_CUSTOM/themes/spaceship-prompt&quot; --depth=1
ln -s &quot;$ZSH_CUSTOM/themes/spaceship-prompt/spaceship.zsh-theme&quot; &quot;$ZSH_CUSTOM/themes/spaceship.zsh-theme&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后是 &lt;code&gt;oh-my-zsh&lt;/code&gt; 的两个第三方插件，分别用来显示补全和命令高亮&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/zsh-users/zsh-autosuggestions ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-autosuggestions
git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-syntax-highlighting
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后可以装 &lt;em&gt;autojump&lt;/em&gt; ,这个命令会解析你经常去的目录，随后使用 &lt;code&gt;j&lt;/code&gt; 命令 &lt;em&gt;jump&lt;/em&gt; 到这个目录里，相当好用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://gitee.com/lxgyChen/autojump
cd autojump &amp;amp;&amp;amp; ./install.py
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如我经常访问&lt;code&gt;/home/imicola/imicola_resource/SRE_Devops/Go&lt;/code&gt; 这个目录，我们就可以用 &lt;code&gt;j go&lt;/code&gt; 达到这个直接跳转到这个目录的效果&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;22:47:57 with imicola in ~ via ⬢ v25.2.1 via 🍞 v1.3.6
➜ j go
/home/imicola/imicola_resource/SRE_Devops/Go
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后是对 &lt;code&gt;.zshrc&lt;/code&gt; 的配置，将这个配置加入你的 &lt;code&gt;.zshrc&lt;/code&gt; 并且解决冲突后就可以使用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export ZSH=&quot;$HOME/.oh-my-zsh&quot;
ZSH_THEME=&quot;spaceship&quot;

plugins=(
	git
	sudo
	copypath
	copyfile
	autojump
	zsh-syntax-highlighting   # 官方要求：必须放最后
	zsh-autosuggestions
)

source $ZSH/oh-my-zsh.sh

# autojump 源码版补加载（omz 的 autojump 插件只负责初始化）
[[ -s $HOME/.autojump/etc/profile.d/autojump.sh ]] &amp;amp;&amp;amp; source $HOME/.autojump/etc/profile.d/autojump.sh

# spaceship 微调：常显时间戳、常显粉色用户名
SPACESHIP_TIME_SHOW=&quot;true&quot;
SPACESHIP_USER_SHOW=&quot;always&quot;
SPACESHIP_USER_COLOR=&quot;212&quot;

# alias
alias cls=&quot;clear&quot;
alias vi=&quot;nvim&quot;
alias vim=&quot;nvim&quot;
alias ls=&quot;eza --icons --group-directories-first&quot;

# fzf 键绑定（Ctrl+R 模糊历史、Ctrl+T 模糊文件，ALT+C 模糊目录）
source &amp;lt;(fzf --zsh)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有两个包没说，一个是 &lt;code&gt;eza&lt;/code&gt; ,相当与增强版的 &lt;code&gt;ls&lt;/code&gt;，使用 &lt;code&gt;alias ls=&quot;eza --icons --group-directories-first&quot;&lt;/code&gt; 则可以达到下图样式&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;eza-ls.png&quot; alt=&quot;eza 增强版 ls 的效果&quot; /&gt;&lt;/p&gt;
&lt;p&gt;还有一个是 &lt;code&gt;fzf&lt;/code&gt; ,作用是 Ctrl+R 模糊历史、Ctrl+T 模糊文件，ALT+C 模糊目录&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;随后是 &lt;em&gt;二次元男娘&lt;/em&gt; 时尚单品 &lt;code&gt;fastfetch&lt;/code&gt; 的配置&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;~/.config/fastfetch&lt;/code&gt; 中找到 &lt;code&gt;config.jsonc&lt;/code&gt; 然后配置它&lt;/p&gt;
&lt;p&gt;如果你想要 &lt;em&gt;imicola&lt;/em&gt; 的配置，直接抄下面这份 &lt;code&gt;config.jsonc&lt;/code&gt; 就行，其中，logo 的配置使用的是 &lt;a href=&quot;https://github.com/MurthiNext/img2text&quot;&gt;MurthiNext/img2text: 将图片转换为带有 ANSI 颜色代码的字符画。&lt;/a&gt; 这个开源项目喵&lt;/p&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;点开查看完整 config.jsonc&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;$schema&quot;: &quot;https://github.com/fastfetch-cli/fastfetch/raw/master/doc/json_schema.json&quot;,

  // ═══════════════════════════════════════════════════════════════
  //  Fastfetch Config — Tree-style with color-coded sections
  // ═══════════════════════════════════════════════════════════════
  //
  //  Color scheme:
  //    {#35} magenta  — Section headers
  //    {#33} yellow   — System section
  //    {#32} green    — Hardware section
  //    {#34} blue     — Software section
  //    {#36} cyan     — Network section
  //    {#31} red      — Extras section
  //
  //  To switch logos, change the &quot;type&quot; field:
  //    &quot;small&quot;  → Small Arch ASCII logo (current, works everywhere)
  //    &quot;chafa&quot;  → Custom image as colored ASCII art (needs source/width/height)
  //    &quot;kitty&quot;  → Kitty image protocol (Kitty terminal only)
  //    &quot;sixel&quot;  → Sixel graphics (WezTerm, etc.)
  //    &quot;arch&quot;   → Full-size Arch ASCII logo
  //
  //  To add more modules, follow the tree pattern:
  //    First item:  &quot;key&quot;: &quot;╭─ icon&quot;
  //    Middle item: &quot;key&quot;: &quot;├─ icon&quot;
  //    Last item:   &quot;key&quot;: &quot;╰─ icon&quot;
  //
  //  Available modules: fastfetch --list-modules
  // ═══════════════════════════════════════════════════════════════

  // ── Logo ───────────────────────────────────────────────
  // Option A: Small Arch ASCII logo (default, universal)
  &quot;logo&quot;: {
    &quot;type&quot;: &quot;auto&quot;,
    &quot;source&quot;: &quot;~/.config/fastfetch/b.txt&quot;,
    &quot;padding&quot;: {
      &quot;top&quot;: 1,
      &quot;left&quot;: 1,
      &quot;right&quot;: 3
    }
  },

  // Option B: Custom image via chafa (uncomment to use, comment Option A)
  // &quot;logo&quot;: {
  //   &quot;type&quot;: &quot;auto&quot;,
  //   &quot;source&quot;: &quot;~/.config/fastfetch/b.txt&quot;,
  //   &quot;width&quot;: 28,
  //   &quot;height&quot;: 18,
  //   &quot;padding&quot;: { &quot;top&quot;: 1, &quot;left&quot;: 1, &quot;right&quot;: 2 }
  // },

  // ── Display ────────────────────────────────────────────
  &quot;display&quot;: {
    &quot;separator&quot;: &quot;  &quot;,
    &quot;constants&quot;: [
      &quot;──────────────────────────────&quot;  // {$1} horizontal rule
    ],
    &quot;percent&quot;: {
      &quot;type&quot;: 3,         // 1=number, 2=bar, 3=both, 9=colored number
      &quot;width&quot;: 0         // 0 disables fixed-width padding (no leading spaces)
    },
    &quot;bar&quot;: {
      &quot;char&quot;: {
        &quot;elapsed&quot;: &quot;■&quot;,
        &quot;total&quot;: &quot;□&quot;
      }
    }
  },

  // ── Modules ────────────────────────────────────────────
  &quot;modules&quot;: [
    // ── Title ──────────────────────────────────────────
    {
      &quot;type&quot;: &quot;title&quot;,
      &quot;color&quot;: {
        &quot;user&quot;: &quot;35&quot;,
        &quot;at&quot;: &quot;90&quot;,
        &quot;host&quot;: &quot;35&quot;
      }
    },
    {
      &quot;type&quot;: &quot;separator&quot;,
      &quot;outputColor&quot;: &quot;90&quot;
    },

    // ── System ─────────────────────────────────────────
    {
      &quot;type&quot;: &quot;custom&quot;,
      &quot;format&quot;: &quot;{#35} {#1}System{#}&quot;
    },
    {
      &quot;type&quot;: &quot;os&quot;,
      &quot;key&quot;: &quot;╭─ {icon}&quot;,
      &quot;keyColor&quot;: &quot;33&quot;
    },
    {
      &quot;type&quot;: &quot;host&quot;,
      &quot;key&quot;: &quot;├─ 󰌢&quot;,
      &quot;keyColor&quot;: &quot;33&quot;
    },
    {
      &quot;type&quot;: &quot;kernel&quot;,
      &quot;key&quot;: &quot;├─ &quot;,
      &quot;keyColor&quot;: &quot;33&quot;
    },
    {
      &quot;type&quot;: &quot;uptime&quot;,
      &quot;key&quot;: &quot;├─ 󰅐&quot;,
      &quot;keyColor&quot;: &quot;33&quot;
    },
    {
      &quot;type&quot;: &quot;packages&quot;,
      &quot;key&quot;: &quot;╰─ 󰏖&quot;,
      &quot;keyColor&quot;: &quot;33&quot;
    },

    // ── Hardware ───────────────────────────────────────
    {
      &quot;type&quot;: &quot;custom&quot;,
      &quot;format&quot;: &quot;{#32} {#1}Hardware{#}&quot;
    },
    {
      &quot;type&quot;: &quot;cpu&quot;,
      &quot;key&quot;: &quot;╭─ 󰻠&quot;,
      &quot;keyColor&quot;: &quot;32&quot;,
      &quot;format&quot;: &quot;{name} ({cores-logical})&quot;
    },
    {
      &quot;type&quot;: &quot;gpu&quot;,
      &quot;key&quot;: &quot;├─ 󰍛&quot;,
      &quot;keyColor&quot;: &quot;32&quot;
    },
    {
      &quot;type&quot;: &quot;memory&quot;,
      &quot;key&quot;: &quot;├─ 󰑭&quot;,
      &quot;keyColor&quot;: &quot;32&quot;
    },
    {
      &quot;type&quot;: &quot;swap&quot;,
      &quot;key&quot;: &quot;├─ 󰓡&quot;,
      &quot;keyColor&quot;: &quot;32&quot;
    },
    {
      &quot;type&quot;: &quot;disk&quot;,
      &quot;key&quot;: &quot;╰─ &quot;,
      &quot;keyColor&quot;: &quot;32&quot;,
      &quot;folders&quot;: &quot;/&quot;
    },

    // ── Software ───────────────────────────────────────
    {
      &quot;type&quot;: &quot;custom&quot;,
      &quot;format&quot;: &quot;{#34}󰘳 {#1}Software{#}&quot;
    },
    {
      &quot;type&quot;: &quot;wm&quot;,
      &quot;key&quot;: &quot;╭─ &quot;,
      &quot;keyColor&quot;: &quot;34&quot;
    },
    {
      &quot;type&quot;: &quot;shell&quot;,
      &quot;key&quot;: &quot;├─ &quot;,
      &quot;keyColor&quot;: &quot;34&quot;
    },
    {
      &quot;type&quot;: &quot;terminal&quot;,
      &quot;key&quot;: &quot;╰─ &quot;,
      &quot;keyColor&quot;: &quot;34&quot;
    },

    // ── Network ────────────────────────────────────────
    {
      &quot;type&quot;: &quot;custom&quot;,
      &quot;format&quot;: &quot;{#36}󰛳 {#1}Network{#}&quot;
    },
    {
      &quot;type&quot;: &quot;localip&quot;,
      &quot;key&quot;: &quot;╰─ 󰩟&quot;,
      &quot;keyColor&quot;: &quot;36&quot;,
      &quot;compact&quot;: true,
      &quot;format&quot;: &quot;{ipv4} ({ifname})&quot;
    },

    // ── Extras ─────────────────────────────────────────
    {
      &quot;type&quot;: &quot;custom&quot;,
      &quot;format&quot;: &quot;{#31} {#1}Extras{#}&quot;
    },
    {
      &quot;type&quot;: &quot;battery&quot;,
      &quot;key&quot;: &quot;╭─ 󰁹&quot;,
      &quot;keyColor&quot;: &quot;31&quot;
    },
    {
      &quot;type&quot;: &quot;display&quot;,
      &quot;key&quot;: &quot;├─ 󰍹&quot;,
      &quot;keyColor&quot;: &quot;31&quot;
    },
    {
      &quot;type&quot;: &quot;locale&quot;,
      &quot;key&quot;: &quot;╰─ 󰒍&quot;,
      &quot;keyColor&quot;: &quot;31&quot;
    },

    // ── Bottom ─────────────────────────────────────────
    &quot;break&quot;,
    {
      &quot;type&quot;: &quot;colors&quot;,
      &quot;paddingLeft&quot;: 2,
      &quot;symbol&quot;: &quot;circle&quot;
    }
  ]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;h4&gt;pwsh配置&lt;/h4&gt;
&lt;p&gt;Linux 这边讲完了，回到 Windows 这边，pwsh 的配置思路和 zsh 不太一样: zsh 那边是 &lt;em&gt;oh-my-zsh&lt;/em&gt; 框架 + 主题 + 一堆插件，而 pwsh 没有这样统一的框架，我们直接用 &lt;em&gt;starship&lt;/em&gt; 这个跨 shell 的提示符工具接管外观，再补两个官方模块，最终效果和 zsh 那边几乎一个样——毕竟 &lt;em&gt;imicola&lt;/em&gt; 的 starship 配置就是照着 &lt;em&gt;spaceship&lt;/em&gt; 主题的样子调的&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;pwsh-starship.png&quot; alt=&quot;pwsh + starship 效果 w-90%&quot; title=&quot;效果如图喵&quot; /&gt;&lt;/p&gt;
&lt;p&gt;需要装的东西一共四个，两个走 &lt;code&gt;winget&lt;/code&gt;，两个走 pwsh 自己的模块仓库 PSGallery:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;winget install --id Starship.Starship
winget install --id junegunn.fzf

Install-Module PSFzf -Scope CurrentUser
Install-Module CompletionPredictor -Scope CurrentUser
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;(第一次用 PSGallery 会问你要不要信任这个仓库，回答 &lt;code&gt;A&lt;/code&gt; 就行)&lt;/p&gt;
&lt;p&gt;各自的分工:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;starship&lt;/em&gt;: 跨 shell 的提示符工具，负责外观，对应 zsh 那边的 &lt;em&gt;spaceship&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;PSFzf&lt;/em&gt;: fzf 的 pwsh 集成版，对应 zsh 那边的 &lt;code&gt;source &amp;lt;(fzf --zsh)&lt;/code&gt;，管 Ctrl+R 模糊历史 / Ctrl+T 模糊文件 / Alt+C 模糊目录，真正干活的还是 winget 装的那个 fzf.exe，模块只是壳&lt;/li&gt;
&lt;li&gt;&lt;em&gt;CompletionPredictor&lt;/em&gt;: 微软官方的历史预测模块，Tab 补全列表里那条灰色的&quot;你想输入的其实是这个&quot;就是它&lt;/li&gt;
&lt;li&gt;PSReadLine: pwsh 自带，连装都不用装，负责基础的行编辑和灰色历史预测&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;然后是配置文件。直接 &lt;code&gt;notepad $PROFILE&lt;/code&gt; 打开编辑
核心配置就这几行:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Invoke-Expression (&amp;amp;starship init powershell)
Import-Module PSFzf
Import-Module CompletionPredictor

# PSReadLine: 从历史生成预测，列表视图展示
Set-PSReadLineOption -PredictionSource History
Set-PSReadLineOption -PredictionViewStyle ListView

# Ctrl+R 先绑成清屏，给 PSFzf 挂掉时留个保底
Set-PSReadLineKeyHandler -Key Ctrl+r -Function ClearScreen

# PSFzf 键位: 后面的绑定会覆盖上面的 Ctrl+R，最终生效的是这里
Set-PsFzfOption -PSReadlineChordProvider &apos;Ctrl+t&apos;
Set-PsFzfOption -PSReadlineChordReverseHistory &apos;Ctrl+r&apos;
Set-PsFzfOption -PSReadlineChordSetLocation &apos;Alt+c&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;starship 的配置在 &lt;code&gt;~\.config\starship.toml&lt;/code&gt;，想让提示符和 &lt;em&gt;imicola&lt;/em&gt; 长一个样的话，直接抄下面这份 &lt;code&gt;starship.toml&lt;/code&gt;，核心思路如下:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;palette = &apos;catppuccin_mocha&apos;&lt;/code&gt;，直接用 Catppuccin Mocha 调色板(文件尾部自带整套色值)，和 wt 端的 Catppuccin 配色无缝衔接&lt;/li&gt;
&lt;li&gt;&lt;code&gt;format&lt;/code&gt; 显式排布一行信息: 时间 → 用户名 → 目录 → git 分支 → 语言版本 → 上条命令耗时，然后换行，第二行只剩一个箭头&lt;/li&gt;
&lt;li&gt;&lt;code&gt;character&lt;/code&gt; 里成功绿箭头、报错红箭头——对，就是 zsh 那个 &lt;code&gt;➜&lt;/code&gt;，两套 shell 刻意统一了视觉，切来切去没有陌生感&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;lt;details&amp;gt;
&amp;lt;summary&amp;gt;点开查看完整 starship.toml&amp;lt;/summary&amp;gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;$schema&quot; = &apos;https://starship.rs/config-schema.json&apos;

add_newline = false
palette = &apos;catppuccin_mocha&apos;

# Keep a clean, zsh-like one-line info prompt and a second-line character.
format = &quot;&quot;&quot;
$time\
$username\
$directory\
$git_branch\
$git_status\
$package\
$c\
$cpp\
$cmake\
$rust\
$golang\
$nodejs\
$php\
$java\
$kotlin\
$haskell\
$python\
$conda\
$cmd_duration\
$line_break\
$character&quot;&quot;&quot;

[time]
disabled = false
time_format = &quot;%T&quot;
style = &quot;fg:lavender&quot;
format = &apos;[$time]($style) &apos;

[username]
show_always = true
style_user = &quot;fg:pink&quot;
style_root = &quot;bold fg:red&quot;
format = &apos;with [$user]($style) &apos;

[directory]
style = &quot;fg:teal&quot;
format = &apos;in [$path]($style)[$read_only]($read_only_style) &apos;
read_only = &quot; 󰌾&quot;
read_only_style = &quot;fg:red&quot;
truncation_length = 3
truncation_symbol = &quot;…/&quot;

[directory.substitutions]
&quot;Documents&quot; = &quot;󰈙 &quot;
&quot;Downloads&quot; = &quot; &quot;
&quot;Music&quot; = &quot;󰝚 &quot;
&quot;Pictures&quot; = &quot; &quot;
&quot;Developer&quot; = &quot;󰲋 &quot;

[git_branch]
symbol = &quot; &quot;
style = &quot;fg:mauve&quot;
format = &apos;on [$symbol$branch]($style) &apos;

[git_status]
style = &quot;fg:yellow&quot;
format = &apos;([$all_status$ahead_behind]($style) )&apos;

[package]
symbol = &quot;󰏗 &quot;
style = &quot;fg:peach&quot;
format = &apos;is [$symbol$version]($style) &apos;

[c]
symbol = &quot; &quot;

[cpp]
symbol = &quot; &quot;

[cmake]
symbol = &quot; &quot;

[rust]
symbol = &quot;󱘗 &quot;

[golang]
symbol = &quot; &quot;

[nodejs]
symbol = &quot; &quot;

[php]
symbol = &quot; &quot;

[java]
symbol = &quot; &quot;

[kotlin]
symbol = &quot; &quot;

[haskell]
symbol = &quot; &quot;

[python]
symbol = &quot; &quot;

[conda]
symbol = &quot; &quot;
style = &quot;fg:sky&quot;
format = &apos;via [$symbol$environment]($style) &apos;
ignore_base = true

[cmd_duration]
min_time = 500
show_milliseconds = true
style = &quot;fg:overlay2&quot;
format = &apos;took [$duration]($style) &apos;
disabled = false
show_notifications = true
min_time_to_notify = 45000

[line_break]
disabled = false

[character]
disabled = false
success_symbol = &apos;[➜](bold fg:green) &apos;
error_symbol = &apos;[➜](bold fg:red) &apos;
vimcmd_symbol = &apos;[❮](bold fg:green) &apos;
vimcmd_replace_one_symbol = &apos;[❮](bold fg:lavender) &apos;
vimcmd_replace_symbol = &apos;[❮](bold fg:lavender) &apos;
vimcmd_visual_symbol = &apos;[❮](bold fg:yellow) &apos;

# Nerd Font Symbols preset mappings (selected core set + OS table)
[aws]
symbol = &quot; &quot;

[buf]
symbol = &quot; &quot;

[bun]
symbol = &quot; &quot;

[crystal]
symbol = &quot; &quot;

[dart]
symbol = &quot; &quot;

[deno]
symbol = &quot; &quot;

[docker_context]
symbol = &quot; &quot;

[elixir]
symbol = &quot; &quot;

[elm]
symbol = &quot; &quot;

[fennel]
symbol = &quot; &quot;

[fortran]
symbol = &quot; &quot;

[fossil_branch]
symbol = &quot; &quot;

[gcloud]
symbol = &quot; &quot;

[git_commit]
tag_symbol = &apos;  &apos;

[gradle]
symbol = &quot; &quot;

[guix_shell]
symbol = &quot; &quot;

[haxe]
symbol = &quot; &quot;

[hg_branch]
symbol = &quot; &quot;

[hostname]
ssh_symbol = &quot; &quot;

[julia]
symbol = &quot; &quot;

[lua]
symbol = &quot; &quot;

[memory_usage]
symbol = &quot;󰍛 &quot;

[meson]
symbol = &quot;󰔷 &quot;

[nim]
symbol = &quot;󰆥 &quot;

[nix_shell]
symbol = &quot; &quot;

[ocaml]
symbol = &quot; &quot;

[perl]
symbol = &quot; &quot;

[pijul_channel]
symbol = &quot; &quot;

[pixi]
symbol = &quot;󰏗 &quot;

[rlang]
symbol = &quot;󰟔 &quot;

[ruby]
symbol = &quot; &quot;

[scala]
symbol = &quot; &quot;

[status]
symbol = &quot; &quot;

[swift]
symbol = &quot; &quot;

[xmake]
symbol = &quot; &quot;

[zig]
symbol = &quot; &quot;

[os.symbols]
Alpaquita = &quot; &quot;
Alpine = &quot; &quot;
AlmaLinux = &quot; &quot;
Amazon = &quot; &quot;
Android = &quot; &quot;
AOSC = &quot; &quot;
Arch = &quot; &quot;
Artix = &quot; &quot;
CachyOS = &quot; &quot;
CentOS = &quot; &quot;
Debian = &quot; &quot;
DragonFly = &quot; &quot;
Elementary = &quot; &quot;
Emscripten = &quot; &quot;
EndeavourOS = &quot; &quot;
Fedora = &quot; &quot;
FreeBSD = &quot; &quot;
Garuda = &quot;󰛓 &quot;
Gentoo = &quot; &quot;
HardenedBSD = &quot;󰞌 &quot;
Illumos = &quot;󰈸 &quot;
Ios = &quot;󰀷 &quot;
Kali = &quot; &quot;
Linux = &quot; &quot;
Mabox = &quot; &quot;
Macos = &quot; &quot;
Manjaro = &quot; &quot;
Mariner = &quot; &quot;
MidnightBSD = &quot; &quot;
Mint = &quot; &quot;
NetBSD = &quot; &quot;
NixOS = &quot; &quot;
Nobara = &quot; &quot;
OpenBSD = &quot;󰈺 &quot;
openSUSE = &quot; &quot;
OracleLinux = &quot;󰌷 &quot;
Pop = &quot; &quot;
Raspbian = &quot; &quot;
Redhat = &quot; &quot;
RedHatEnterprise = &quot; &quot;
RockyLinux = &quot; &quot;
Redox = &quot;󰀘 &quot;
Solus = &quot;󰠳 &quot;
SUSE = &quot; &quot;
Ubuntu = &quot; &quot;
Unknown = &quot; &quot;
Void = &quot; &quot;
Windows = &quot;󰍲 &quot;
Zorin = &quot; &quot;

[palettes.catppuccin_mocha]
rosewater = &quot;#f5e0dc&quot;
flamingo = &quot;#f2cdcd&quot;
pink = &quot;#f5c2e7&quot;
mauve = &quot;#cba6f7&quot;
red = &quot;#f38ba8&quot;
maroon = &quot;#eba0ac&quot;
peach = &quot;#fab387&quot;
yellow = &quot;#f9e2af&quot;
green = &quot;#a6e3a1&quot;
teal = &quot;#94e2d5&quot;
sky = &quot;#89dceb&quot;
sapphire = &quot;#74c7ec&quot;
blue = &quot;#89b4fa&quot;
lavender = &quot;#b4befe&quot;
text = &quot;#cdd6f4&quot;
subtext1 = &quot;#bac2de&quot;
subtext0 = &quot;#a6adc8&quot;
overlay2 = &quot;#9399b2&quot;
overlay1 = &quot;#7f849c&quot;
overlay0 = &quot;#6c7086&quot;
surface2 = &quot;#585b70&quot;
surface1 = &quot;#45475a&quot;
surface0 = &quot;#313244&quot;
base = &quot;#1e1e2e&quot;
mantle = &quot;#181825&quot;
crust = &quot;#11111b&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&amp;lt;/details&amp;gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;碎碎念:&lt;/p&gt;
&lt;p&gt;有人问 &lt;em&gt;imicola&lt;/em&gt; ，在这个 LLM 都快接管互联网的时代下，花那么多心思去配置一个几十年前的 终端 和 命令行工具，是不是在浪费时间? AI 在使用命令行这一块使用已经远远超越人类程序员，我们不用命令行也能做到很多事情，甚至可以做到 我们 在图形化界面上都做不到的事情&lt;/p&gt;
&lt;p&gt;但是呢，这种甚至有一点开倒车的，花费心思去配置一个好看的命令行，实际上也是类似一种 &quot;消遣&quot; 的心态，配置就如同搭积木一般，看着一个黑乎乎的窗口成为自己得心应手的工具，算是一种娱乐行为吧（&lt;/p&gt;
</content:encoded></item><item><title>Prometheus 从指标暴露到抓取实战</title><link>https://blog.imicola.com/posts/prometheus%E4%BB%8E%E6%8C%87%E6%A0%87%E6%9A%B4%E9%9C%B2%E5%88%B0%E6%8A%93%E5%8F%96%E5%AE%9E%E6%88%98/</link><guid isPermaLink="true">https://blog.imicola.com/posts/prometheus%E4%BB%8E%E6%8C%87%E6%A0%87%E6%9A%B4%E9%9C%B2%E5%88%B0%E6%8A%93%E5%8F%96%E5%AE%9E%E6%88%98/</guid><description>以 Go 服务手动暴露 /metrics 为例，梳理指标与 Gauge/Counter 的定义、Prometheus 文本暴露格式、Pull 抓取模型与 Target/Job/Instance 概念，并给出最小可用的 prometheus.yml 抓取配置。</description><pubDate>Sun, 11 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;a href=&quot;https://github.com/prometheus/prometheus&quot;&gt;prometheus&lt;/a&gt;(普罗米修斯)是一个系统和服务监控系统，普罗米修斯可以按给定间隔从配置的目标收集指标，评估规则表达式，显示结果，并可在观察到指定条件时触发告警&lt;/p&gt;
&lt;h2&gt;Prometheus抓取什么&lt;/h2&gt;
&lt;p&gt;在生产环境中，Prometheus一般运行在云端中，通过HTTP协议抓取集群中各个节点暴露的端口，而不是自己作为一个Agent去收集集群节点的环境&lt;/p&gt;
&lt;p&gt;这就涉及到一个约定问题，普罗米修斯如何正确的读取到节点返回的响应?&lt;/p&gt;
&lt;h3&gt;metrics定义与Gauge文本格式&lt;/h3&gt;
&lt;p&gt;我们先明确三个定义:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指标（Metric）&lt;/strong&gt;：一个带有明确名称和单位的测量值，HTTP 接口只是把这次测量值交给外部读取的传输载体&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;仪表盘指标（Gauge）&lt;/strong&gt;：表示当前状态的瞬时测量值，数值可以上升也可以下降。可用内存随进程分配与释放而波动，符合 &lt;code&gt;Gauge&lt;/code&gt; 的特征&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;单调递增计数器（Counter）&lt;/strong&gt; 仅适合累计事件次数（通常只增不减，仅在进程重启时重置），比如累计CPU时间&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Exposition Format（指标暴露格式）&lt;/strong&gt;：提供一个能被监控程序读取的接口规范&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们这节用 go 语言抓取 &lt;code&gt;/proc/meminfo&lt;/code&gt; 中的 &lt;code&gt;MemAvailable&lt;/code&gt; 来展示，所以 &lt;code&gt;MemAvailable&lt;/code&gt; 的值(乘1024) 便是我们这次的指标&lt;/p&gt;
&lt;p&gt;对于 &lt;em&gt;Exposition Format&lt;/em&gt; ，我们自然的采用 &lt;em&gt;Prometheus 传统 HTTP 文本暴露格式&lt;/em&gt; 这个格式将在下面介绍&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;一次成功的 &lt;code&gt;/metrics&lt;/code&gt; 响应由响应头和三行响应体组成。&lt;/p&gt;
&lt;p&gt;其中响应头指定协议版本与编码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Content-Type: text/plain; version=0.0.4; charset=utf-8
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;响应体我们约定为三行文本:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# HELP sentinel_memory_available_bytes Estimated available memory in bytes.
# TYPE sentinel_memory_available_bytes gauge
sentinel_memory_available_bytes 2097152

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其基本构造:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# HELP [] {Explanation}
# TYPE [] {type}
[] {numerical value}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这三行的含义定义可以这样解释:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;# HELP 行说明指标含义&lt;/li&gt;
&lt;li&gt;# TYPE 行声明类型(如 gauge,counter)&lt;/li&gt;
&lt;li&gt;最后一行表示样本行，是这次暴露的数据&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;其中三行中的指标名称必须一致，同时每行的末尾都必须带换行符 &lt;code&gt;\n&lt;/code&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;Prometheus如何抓取&lt;/h2&gt;
&lt;p&gt;当我们实现了接口之后，我们就需要自己配置一下普罗米修斯来实现对节点内容的定期抓取了&lt;/p&gt;
&lt;p&gt;首先我们先了解分工和整体流程:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sequenceDiagram
    autonumber
    participant P as Prometheus Server (:9091)
    participant S as sentinel-go (:9090)

    Note over P: 主动拉取（Pull）模型：Prometheus 按周期主动 GET /metrics

    rect rgb(232, 244, 255)
    Note over P: T0 [0s]
    P-&amp;gt;&amp;gt;S: GET /metrics
    S--&amp;gt;&amp;gt;P: 200 OK (sentinel_memory_available_bytes 2097152)
    Note over P: TSDB 写入：(T0, 2097152), up=1
    end

    rect rgb(232, 251, 234)
    Note over P: T1 [5s]（周期调度）
    P-&amp;gt;&amp;gt;S: GET /metrics
    S--&amp;gt;&amp;gt;P: 200 OK (sentinel_memory_available_bytes 2085600)
    Note over P: TSDB 写入：(T1, 2085600), up=1
    end

    Note over P: 抓取时刻时间戳以 Prometheus 记录为准，受网络与调度影响存在微小毫秒抖动
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;谁发请求&lt;/strong&gt;：Prometheus 是主动拉取（Pull）模型。Prometheus 作为客户端，按照配置的时间周期（如每 5 秒一次）主动向被监控服务发送 HTTP &lt;code&gt;GET /metrics&lt;/code&gt; 请求。go 服务只需要被动监听端口并响应，不在代码内部维持抓取定时器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;谁保存历史&lt;/strong&gt;：Go 服务是无状态的采集端，处理完一次请求后数据即在内存中丢弃；Prometheus Server 内部集成时序数据库（TSDB）,每次拉取成功后会把当前样本值与抓取时刻的时间戳（精确到毫秒）持久化写入磁盘&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;数据库有什么好处&lt;/strong&gt;：写入 Prometheus TSDB 之后的数据成为&lt;strong&gt;时间序列（Time Series）&lt;/strong&gt;，可以通过 PromQL 跨越时间轴计算波动、对齐不同实例、进行单位换算与阈值比对。&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;Prometheus中的逻辑&lt;/h2&gt;
&lt;p&gt;在Prometheus中，获取到的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Target（目标）&lt;/strong&gt;：具体被抓取的网络端点，形式严格约定为 &lt;code&gt;host:port&lt;/code&gt;（例如 &lt;code&gt;127.0.0.1:9090&lt;/code&gt;）。一个 Target 对应一个暴露指标的 HTTP 进程实例&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Job（作业）&lt;/strong&gt;：一组执行相同业务逻辑或具有相同监控目的的 Target 集合；可以理解为一组意图相同的Target集合&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instance（实例）&lt;/strong&gt;：当 Prometheus 完成抓取时，会自动在时序数据上打上 &lt;code&gt;instance=&quot;&amp;lt;host:port&amp;gt;&quot;&lt;/code&gt; 标签。&lt;code&gt;job&lt;/code&gt; 与 &lt;code&gt;instance&lt;/code&gt; 共同标识了抓取来源目标，也就是Instance是Job下的一个具体角色&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;存放在Prometheus中的时间序列中单个样本的&lt;strong&gt;逻辑表示&lt;/strong&gt;可抽象为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;指标名{标签名=&quot;标签值&quot;, ...} 浮点数值 毫秒时间戳
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;比如&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sentinel_memory_available_bytes{job=&quot;sentinel&quot;, instance=&quot;127.0.0.1:9090&quot;} 2097152000 1760000000000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;解读下来就是我们的指标，然后是这个指标属于哪个job，来自哪个Target，具体的数值是什么，最后是时间戳&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;抓取配置与地址格式&lt;/h2&gt;
&lt;p&gt;Prometheus 通过根配置文件（通常为 &lt;code&gt;prometheus.yml&lt;/code&gt;）声明抓取行为&lt;/p&gt;
&lt;p&gt;下面我们给出一个最小配置实例：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;global:
  scrape_interval: 5s     # 抓取周期：每隔 5 秒发一次 GET 请求
  scrape_timeout: 4s      # 抓取超时：单次请求超过 4 秒未完成则判定为失败

scrape_configs:
  - job_name: &apos;sentinel&apos;
    metrics_path: &apos;/metrics&apos;    # 抓取路径（默认即为 /metrics，显式声明）
    static_configs:
      - targets: [&apos;127.0.0.1:9090&apos;]  # 必须写 host:port，不能带 http://
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中有几个要点需要我们知道：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Target 格式严禁包含协议与路径
&lt;ul&gt;
&lt;li&gt;即targets: [&apos;127.0.0.1:9090&apos;]不能写成 [&apos;http://127.0.0.1:9090&apos;] 或者 [&apos;127.0.0.1:9090/metrics&apos;]&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;超时时间不得超过抓取周期
&lt;ul&gt;
&lt;li&gt;如果超时时间大于抓取周期，当前周期的抓取还未结束就会与下一周期的抓取重叠堆积&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以参考 &lt;a href=&quot;https://prometheus.io/docs/prometheus/latest/configuration/configuration/&quot;&gt;Prometheus 配置官方规范&lt;/a&gt; 来编写扩展 &lt;code&gt;prometheus.yml&lt;/code&gt;文件，在编写完成后，需要进行一次 &lt;code&gt;promtool check config &amp;lt;文件名&amp;gt;&lt;/code&gt; 进行静态检查&lt;/p&gt;
&lt;hr /&gt;
</content:encoded></item><item><title>Go net/http 从入门到 CRUD 实战</title><link>https://blog.imicola.com/posts/go%E8%AF%AD%E8%A8%80net-http%E5%85%A5%E9%97%A8%E4%B8%8Ecrud%E5%AE%9E%E6%88%98/</link><guid isPermaLink="true">https://blog.imicola.com/posts/go%E8%AF%AD%E8%A8%80net-http%E5%85%A5%E9%97%A8%E4%B8%8Ecrud%E5%AE%9E%E6%88%98/</guid><description>从一个 GET /healthz 请求的完整链路出发，梳理 net/http 的核心组件与 HTTP Method 语义，再基于 Go 1.22 的方法路由实现一套完整的 RESTful CRUD 服务。</description><pubDate>Fri, 04 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;从一个请求的链路开始&lt;/h1&gt;
&lt;p&gt;当我们从客户端向服务器发送 HTTP 请求的时候，Go 语言有一套专门负责解析、构造的标准库 &lt;code&gt;net/http&lt;/code&gt;。本文先从一个简单的 &lt;code&gt;GET /healthz&lt;/code&gt; 实例引入 HTTP Server，接着梳理 HTTP Method 的语义，最后基于 Go 1.22 的方法路由实现一套完整的 CRUD 服务。&lt;/p&gt;
&lt;h2&gt;HTTP 服务链路&lt;/h2&gt;
&lt;p&gt;假设我们的客户端构造一个 8080 端口的 &lt;code&gt;GET /healthz&lt;/code&gt; 请求，而服务端需要返回 &lt;code&gt;HTTP 200 OK&lt;/code&gt;，这其中基本的链路如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端连接 8080 端口
→ Go HTTP Server 读取并解析 HTTP 请求
→ 路由器根据请求路径寻找 Handler
→ /healthz 匹配到 healthz 函数
→ healthz 读取请求 r
→ 通过 w 写回响应
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;想要完成上面的链路，需要涉及如下的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;net/http.ListenAndServe&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net/http.HandleFunc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net/http.HandlerFunc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net/http.ResponseWriter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;net/http.Request&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;我们接下来逐一讲解这些基本方法。&lt;/p&gt;
&lt;h3&gt;HandleFunc&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func http.HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request))
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;HandleFunc&lt;/code&gt; 是注册函数，负责在默认路由器中把 &lt;code&gt;pattern&lt;/code&gt; 表示的路径与 &lt;code&gt;handler&lt;/code&gt; 函数关联起来。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;http.HandleFunc(&quot;/healthz&quot;, healthz)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;表示将 &lt;code&gt;/healthz&lt;/code&gt; 与函数 &lt;code&gt;healthz&lt;/code&gt; 绑定并且注册到默认路由器中。这个操作并不会直接进入 &lt;code&gt;healthz&lt;/code&gt; 函数中，真正的调用发生在请求到达之后。&lt;/p&gt;
&lt;h3&gt;ListenAndServe&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func http.ListenAndServe(addr string, handler http.Handler) error
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个函数接受一个 TCP 地址，例如 &lt;code&gt;IPv4:Port&lt;/code&gt;。本地测试时可以写成 &lt;code&gt;:Port&lt;/code&gt;，其中省略的是具体主机地址，通常表示监听本机所有可用网络接口，并不等同于只监听 &lt;code&gt;localhost&lt;/code&gt;；客户端仍可使用 &lt;code&gt;localhost:Port&lt;/code&gt; 访问。&lt;/p&gt;
&lt;p&gt;这个函数的作用是启动一个 HTTP Server，在第一个参数指定的地址上监听，并把收到的请求交给第二个参数代表的 Handler 处理。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;这里有一点需要注意的是，当我们没有手写 mux 的时候这里可以填入 &lt;code&gt;nil&lt;/code&gt;，这代表请求会被 &lt;code&gt;DefaultServeMux&lt;/code&gt; 处理。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;HandlerFunc&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;HandlerFunc&lt;/code&gt; 是标准库定义的命名函数类型。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type HandlerFunc func(ResponseWriter, *Request)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;HandlerFunc&lt;/code&gt; 实现了 &lt;code&gt;Handler&lt;/code&gt; 接口，可以把签名匹配的普通函数适配为 HTTP Handler。原始 HTTP 数据到 &lt;code&gt;Request&lt;/code&gt; 对象的解析由 HTTP Server 完成，在进入 Handler 前已经完成。&lt;/p&gt;
&lt;h3&gt;Request&lt;/h3&gt;
&lt;p&gt;在函数内部，&lt;code&gt;Request&lt;/code&gt; 不负责执行底层解析，而是让我们读取解析后的请求信息。&lt;/p&gt;
&lt;p&gt;其常见的内容如下：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;r.Method      // GET、POST 等请求方法
r.URL         // 路径和查询参数
r.Header      // 请求头
r.Body        // 请求体
r.Context()   // 请求上下文和取消信号
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;ResponseWriter&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ResponseWriter&lt;/code&gt; 可以理解为你向客户端写回 HTTP 响应的出口。&lt;/p&gt;
&lt;p&gt;对于一个 HTTP 响应，主要分为三部分：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK                   → 状态行
Content-Type: application/json    → 响应头

{&quot;status&quot;:&quot;ok&quot;}                   → 响应体
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因此，代码中的函数应该围绕构建这三个部分进行：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;w.Header().Set(...) // 设置响应头
w.WriteHeader(...)  // 设置响应状态码
w.Write(...)        // 写入响应体
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于这三个函数，顺序是非常重要的，因为一旦调用 &lt;code&gt;WriteHeader&lt;/code&gt; 或 &lt;code&gt;Write&lt;/code&gt; 后响应头通常就已经被发送（go 会自动发送 &lt;code&gt;200 OK&lt;/code&gt;）。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note] 响应头
HTTP 响应头是一组键值形式的&lt;strong&gt;元数据&lt;/strong&gt;，用于描述响应，而不是响应的实际内容。&lt;/p&gt;
&lt;p&gt;例如：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Content-Type: application/json
Content-Length: 15
Cache-Control: no-cache
Allow: GET
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;Content-Type&lt;/code&gt; 的含义是告诉客户端响应体应该按什么格式解析。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;第一个完整的例子&lt;/h2&gt;
&lt;p&gt;所以对于实现一个简单的 &lt;code&gt;GET /healthz&lt;/code&gt; 监听响应程序，我们可以这样书写代码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;log&quot;
	&quot;net/http&quot;
)

func healthz(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodGet {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
	w.WriteHeader(http.StatusOK)
	w.Write([]byte(`{&quot;status&quot;:&quot;ok&quot;}`))
}

func main() {
	http.HandleFunc(&quot;/healthz&quot;, healthz)
	log.Println(&quot;Server start ok&quot;)
	err := http.ListenAndServe(&quot;:8080&quot;, nil)
	if err != nil {
		log.Fatal(err)
		return
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;理解 HTTP Method&lt;/h1&gt;
&lt;p&gt;在动手实现 CRUD 之前，需要先建立对 HTTP Method 的准确认识，理解 Method 与 CRUD 的映射关系。&lt;/p&gt;
&lt;h2&gt;Method 是什么&lt;/h2&gt;
&lt;p&gt;先建立最基础的模型。从语义层面看，一个 HTTP Request 可以先关注：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Method + Path + Header + Body
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中 &lt;code&gt;Method&lt;/code&gt; 可以先理解为客户端希望对目标资源执行的动作。例如 &lt;code&gt;GET /targets/1&lt;/code&gt; 表示请求读取 &lt;code&gt;/targets/1&lt;/code&gt; 所标识的资源。&lt;/p&gt;
&lt;p&gt;HTTP Method 和 CRUD 有常见对应关系，但两者并非严格等价。例如 POST 也可以表示提交任务或触发处理，并不一定创建资源。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;GET        → Read
POST       → Create（常见用法之一）
PUT/PATCH  → Update
DELETE     → Delete
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;GET&lt;/h2&gt;
&lt;p&gt;GET 表示获取资源的当前表示。例如 &lt;code&gt;GET /targets&lt;/code&gt; 通常读取 Target 集合，而 &lt;code&gt;GET /targets/1&lt;/code&gt; 读取 ID 为 1 的单个 Target。&lt;/p&gt;
&lt;p&gt;假设服务器中存在：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Target: 1
name = &quot;google&quot;
address = &quot;https://google.com&quot;

Target: 2
name = &quot;github&quot;
address = &quot;https://github.com&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;请求 &lt;code&gt;GET /targets&lt;/code&gt; 时，服务器可以返回 JSON 数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;HTTP/1.1 200 OK
Content-Type: application/json

[
  {
    &quot;id&quot;: 1,
    &quot;name&quot;: &quot;google&quot;,
    &quot;address&quot;: &quot;https://google.com&quot;
  },
  {
    &quot;id&quot;: 2,
    &quot;name&quot;: &quot;github&quot;,
    &quot;address&quot;: &quot;https://github.com&quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果请求 &lt;code&gt;GET /targets/1&lt;/code&gt;，则只返回 ID 为 1 的资源；资源不存在时通常返回 &lt;code&gt;404 Not Found&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;GET 是安全且幂等的方法：&quot;安全&quot;表示客户端请求的目标不是修改服务器状态；&quot;幂等&quot;表示多次相同请求的预期效果与一次相同。&lt;/p&gt;
&lt;h2&gt;POST&lt;/h2&gt;
&lt;p&gt;POST 表示客户端向目标资源提交数据，请服务器按照该资源的语义进行处理。在本项目中，它用于向集合创建一个新 Target：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;POST /targets
Content-Type: application/json

{
  &quot;name&quot;: &quot;google&quot;,
  &quot;address&quot;: &quot;https://google.com&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;创建成功后通常返回 &lt;code&gt;201 Created&lt;/code&gt; 和创建后的资源，也可以通过 &lt;code&gt;Location&lt;/code&gt; 响应头告诉客户端新资源的 URI。&lt;/p&gt;
&lt;p&gt;设计资源风格的 API 时，一般使用 &lt;code&gt;POST /targets&lt;/code&gt;，而不是 &lt;code&gt;POST /createTarget&lt;/code&gt;，因为 Method 已经表达了操作语义。不过这是一种常见 API 设计方式，不是 HTTP 协议强制规定。&lt;/p&gt;
&lt;p&gt;POST 通常不是幂等方法：重复发送同一个创建请求，可能创建多个不同资源。&lt;/p&gt;
&lt;h2&gt;PUT&lt;/h2&gt;
&lt;p&gt;PUT 通常表示：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;用客户端提供的完整表示替换目标资源的当前表示。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如客户端发送：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PUT /targets/1
Content-Type: application/json

{
  &quot;name&quot;: &quot;google&quot;,
  &quot;address&quot;: &quot;https://google.com&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;则服务器中的资源会被完整替换为：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;google&quot;,
  &quot;address&quot;: &quot;https://google.com&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PUT 是幂等方法：多次发送相同 PUT 请求，其预期服务器状态与发送一次相同。在 &lt;code&gt;sentinel-go&lt;/code&gt; 当前约定中，更新不存在的 ID 返回 &lt;code&gt;404 Not Found&lt;/code&gt;；HTTP 标准也允许某些 API 使用 PUT 创建指定 URI 的资源，因此具体行为需要在接口契约中写清楚。&lt;/p&gt;
&lt;h2&gt;PATCH&lt;/h2&gt;
&lt;p&gt;PATCH 也表示更新。与 PUT 的完整替换不同，PATCH 请求体描述要应用到资源的一组局部修改，因此通常只提供需要变化的字段。&lt;/p&gt;
&lt;p&gt;例如客户端发送：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;PATCH /targets/1
Content-Type: application/json

{
  &quot;name&quot;: &quot;bing&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;更新后的资源可能是：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;id&quot;: 1,
  &quot;name&quot;: &quot;bing&quot;,
  &quot;address&quot;: &quot;https://google.com&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;PUT 和 PATCH 成功后可以返回更新后的资源和 &lt;code&gt;200 OK&lt;/code&gt;，也可以在不返回正文时使用 &lt;code&gt;204 No Content&lt;/code&gt;；具体选择应保持一致并写入接口契约。PATCH 不保证天然幂等，其幂等性取决于补丁格式和操作语义。&lt;/p&gt;
&lt;h2&gt;DELETE&lt;/h2&gt;
&lt;p&gt;DELETE 用于删除目标资源。当客户端发送 &lt;code&gt;DELETE /targets/{id}&lt;/code&gt; 时，就是请求删除该 ID 对应的 Target；成功且不返回正文时通常使用 &lt;code&gt;204 No Content&lt;/code&gt;，资源不存在时通常返回 &lt;code&gt;404 Not Found&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;DELETE 是幂等方法。这里的&quot;幂等&quot;指多次相同请求的预期服务器状态与执行一次相同，而不是要求每次响应状态码完全相同；第一次删除可能返回 &lt;code&gt;204&lt;/code&gt;，再次删除可能返回 &lt;code&gt;404&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;安全性与幂等性速查&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;安全&lt;/th&gt;
&lt;th&gt;幂等&lt;/th&gt;
&lt;th&gt;项目中的常见用途&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;查询 Target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;通常否&lt;/td&gt;
&lt;td&gt;创建 Target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;完整替换 Target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PATCH&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;不一定&lt;/td&gt;
&lt;td&gt;局部修改 Target&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;删除 Target&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h1&gt;动手实现 CRUD&lt;/h1&gt;
&lt;p&gt;理解了请求链路和 Method 语义之后，就可以在 &lt;code&gt;net/http&lt;/code&gt; 的基础上，根据 HTTP Method 对资源执行创建、读取、更新和删除操作。在开始 CRUD 之前，需要先理解路由器的选择。&lt;/p&gt;
&lt;h2&gt;自定义路由器&lt;/h2&gt;
&lt;p&gt;最简单的例子使用了 &lt;code&gt;http.ListenAndServe(&quot;:8080&quot;, nil)&lt;/code&gt;。其中第二个参数为 &lt;code&gt;nil&lt;/code&gt; 时，服务器会使用全局的 &lt;code&gt;http.DefaultServeMux&lt;/code&gt;。我们也可以显式创建并传入自己的路由器：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mux := http.NewServeMux()
mux.HandleFunc(&quot;/healthz&quot;, healthz)
http.ListenAndServe(&quot;:8080&quot;, mux)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样，请求链会更加清晰：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;客户端
  │
  │ GET /healthz
  ↓
HTTP Server（读取并解析请求）
  │
  ↓
mux（根据 Method + Path 匹配 Handler）
  │
  ↓
healthz
  │
  ↓
ResponseWriter
  │
  ↓
客户端
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要注意：如果路由通过 &lt;code&gt;http.HandleFunc&lt;/code&gt; 注册，它会进入 &lt;code&gt;DefaultServeMux&lt;/code&gt;；如果服务器最终使用的是新建的 &lt;code&gt;mux&lt;/code&gt;，请求就匹配不到这些路由。因此，注册路由和启动服务器必须使用同一个 &lt;code&gt;ServeMux&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;Go 1.22 之后的方法路由&lt;/h2&gt;
&lt;p&gt;在一些旧教程中，我们会看到下面的写法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mux.HandleFunc(&quot;/targets&quot;, targetsHandler)

func targetsHandler(w http.ResponseWriter, r *http.Request) {
	switch r.Method {
	case http.MethodGet:
		// 查询
	case http.MethodPost:
		// 创建
	default:
		// 405
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;从 Go 1.22 开始，标准库 &lt;code&gt;ServeMux&lt;/code&gt; 的 pattern 支持 Method 和路径通配符，因此可以写成：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mux.HandleFunc(&quot;GET /targets&quot;, listTarget)
mux.HandleFunc(&quot;POST /targets&quot;, addTarget)

mux.HandleFunc(&quot;GET /targets/{id}&quot;, getTarget)
mux.HandleFunc(&quot;PUT /targets/{id}&quot;, updateTarget)
mux.HandleFunc(&quot;DELETE /targets/{id}&quot;, deleteTarget)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;本文示例统一使用复数资源路径 &lt;code&gt;/targets&lt;/code&gt;，而当前 &lt;code&gt;sentinel-go&lt;/code&gt; 代码仍使用 &lt;code&gt;/target&lt;/code&gt;。两种路径都能被路由器处理，但同一个项目应统一代码、README、测试和笔记中的接口契约，避免调用时混用。&lt;/p&gt;
&lt;p&gt;HTTP Server 负责读取并解析原始请求，&lt;code&gt;ServeMux&lt;/code&gt; 接收解析后的 &lt;code&gt;*http.Request&lt;/code&gt;，再根据 Method、Host 和 Path 选择 Handler。换句话说，&lt;code&gt;ServeMux&lt;/code&gt; 是请求多路复用器，而不是 HTTP 报文解析器。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;{id}&lt;/code&gt; 只匹配一个路径段，可以通过 &lt;code&gt;r.PathValue(&quot;id&quot;)&lt;/code&gt; 读取，返回值类型是 &lt;code&gt;string&lt;/code&gt;。另外，&lt;code&gt;&quot;GET /targets/{id}&quot;&lt;/code&gt; 也会匹配 &lt;code&gt;HEAD&lt;/code&gt; 请求；当 Path 能匹配、Method 不能匹配时，&lt;code&gt;ServeMux&lt;/code&gt; 会返回 &lt;code&gt;405 Method Not Allowed&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;处理路径参数：ID 问题&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;r.PathValue()&lt;/code&gt; 返回字符串。如果资源 ID 使用整数表示，就需要进行转换，并将转换失败视为客户端请求错误：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;id, err := strconv.Atoi(r.PathValue(&quot;id&quot;))
if err != nil {
	http.Error(w, &quot;invalid id&quot;, http.StatusBadRequest)
	return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;PathValue&lt;/code&gt; 读取的是 &lt;code&gt;ServeMux&lt;/code&gt; 已匹配路径通配符的值。例如路由 pattern 为 &lt;code&gt;&quot;GET /targets/{id}&quot;&lt;/code&gt; 时，请求 &lt;code&gt;/targets/12&lt;/code&gt; 得到的 &lt;code&gt;id&lt;/code&gt; 是字符串 &lt;code&gt;&quot;12&quot;&lt;/code&gt;。如果请求没有经过包含该通配符的路由匹配，结果会是空字符串。&lt;/p&gt;
&lt;p&gt;生成 ID 时不能简单使用 &lt;code&gt;len(targets) + 1&lt;/code&gt;，因为删除中间元素后，新 ID 可能与仍然存在的 ID 冲突。当前练习可以维护独立的递增计数器；并发处理请求时，计数器和 map 必须放在同一套同步策略下保护。&lt;/p&gt;
&lt;h2&gt;如何返回 JSON&lt;/h2&gt;
&lt;p&gt;可以使用标准库中的 &lt;code&gt;json.NewEncoder(w).Encode(value)&lt;/code&gt; 构造 JSON 响应，并通过结构体标签控制字段名称：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Target struct {
	ID   int    `json:&quot;id&quot;`
	Name string `json:&quot;name&quot;`
	Addr string `json:&quot;address&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;写入正文前需要设置响应头和状态码：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)
w.WriteHeader(http.StatusOK)
if err := json.NewEncoder(w).Encode(value); err != nil {
	// 记录错误；响应提交后不能再修改状态码。
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;第一次调用 &lt;code&gt;WriteHeader&lt;/code&gt;，或者第一次写入响应体时，HTTP 状态码就会提交。状态码提交后再调用 &lt;code&gt;http.Error&lt;/code&gt;，不能把已经发送的 &lt;code&gt;200&lt;/code&gt; 改成 &lt;code&gt;500&lt;/code&gt;。如果必须保证&quot;编码失败时返回 500&quot;，可以先用 &lt;code&gt;json.Marshal&lt;/code&gt; 在内存中完成编码，成功后再写响应头和正文。&lt;/p&gt;
&lt;p&gt;当需要返回多个资源时，可以先构造切片，再编码成 JSON 数组：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;list := make([]Target, 0, len(targets))
for _, target := range targets {
	list = append(list, target)
}

if err := json.NewEncoder(w).Encode(list); err != nil {
	// 记录错误
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;如何解析 JSON&lt;/h2&gt;
&lt;p&gt;请求体可以通过 Decoder 解析到结构体中：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var req TargetRequest

if err := json.NewDecoder(r.Body).Decode(&amp;amp;req); err != nil {
	http.Error(w, &quot;invalid json&quot;, http.StatusBadRequest)
	return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里需要传入 &lt;code&gt;&amp;amp;req&lt;/code&gt;，因为 Decoder 必须修改调用方持有的变量，才能把解析结果写入结构体字段。解析和校验通过后，再构造资源并写入服务器的存储：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;target := Target{
	ID:   nextID,
	Name: req.Name,
	Addr: req.Addr,
}

targets[nextID] = target
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;拒绝未知字段与业务校验&lt;/h2&gt;
&lt;p&gt;如果希望拒绝目标结构体中不存在的 JSON 字段，可以使用：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var req TargetRequest
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()

if err := decoder.Decode(&amp;amp;req); err != nil {
	http.Error(w, &quot;invalid json&quot;, http.StatusBadRequest)
	return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DisallowUnknownFields&lt;/code&gt; 只负责拒绝未知字段，不会判断缺失字段、空字符串或地址是否合法；这些业务规则仍需单独校验。&lt;code&gt;Decode&lt;/code&gt; 本身会报告 JSON 语法错误和类型不匹配。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;if req.Name == &quot;&quot; || req.Addr == &quot;&quot; {
	http.Error(w, &quot;name and address are required&quot;, http.StatusBadRequest)
	return
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;只调用一次 &lt;code&gt;Decode&lt;/code&gt; 还可能接受正文末尾的第二个 JSON 值。需要严格限制请求正文时，可以在第一次解析成功后再次解析并确认得到 &lt;code&gt;io.EOF&lt;/code&gt;，同时根据服务边界考虑限制请求体大小。&lt;/p&gt;
&lt;h2&gt;map 中的结构体是值副本&lt;/h2&gt;
&lt;p&gt;当 &lt;code&gt;targets&lt;/code&gt; 的类型是 &lt;code&gt;map[int]Target&lt;/code&gt; 时，下面读取出的 &lt;code&gt;target&lt;/code&gt; 是结构体副本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;target, ok := targets[id]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;修改副本后必须显式写回：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;target.Name = req.Name
target.Addr = req.Addr
targets[id] = target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;否则 PUT 响应可能返回新值，但后续 GET 仍然读到旧值。&lt;/p&gt;
&lt;h2&gt;Handler 中不要调用 log.Fatal&lt;/h2&gt;
&lt;p&gt;非法 ID、非法 JSON 和资源不存在都只是一次请求的失败，不应终止整个服务器。&lt;code&gt;log.Fatal&lt;/code&gt; 记录日志后会调用 &lt;code&gt;os.Exit(1)&lt;/code&gt;，因此 Handler 应返回合适的 &lt;code&gt;4xx&lt;/code&gt;；服务器内部错误一般记录日志并返回 &lt;code&gt;500&lt;/code&gt;。&lt;/p&gt;
&lt;h2&gt;当前实现边界&lt;/h2&gt;
&lt;p&gt;当前 &lt;code&gt;sentinel-go&lt;/code&gt; 使用内存 &lt;code&gt;map&lt;/code&gt; 保存 Target，因此服务重启后数据会丢失。&lt;code&gt;net/http&lt;/code&gt; 会并发处理请求，而普通 &lt;code&gt;map&lt;/code&gt; 与自增 ID 计数器不能安全地被多个请求同时读写；后续需要引入互斥保护并使用 &lt;code&gt;go test -race&lt;/code&gt; 验证。自动化测试、服务超时和优雅退出也尚未完成。&lt;/p&gt;
&lt;h1&gt;参考资料&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc9110.html&quot;&gt;RFC 9110：HTTP Semantics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc5789.html&quot;&gt;RFC 5789：PATCH Method for HTTP&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/doc/go1.22&quot;&gt;Go 1.22 Release Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://go.dev/blog/routing-enhancements&quot;&gt;Routing Enhancements for Go 1.22&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://pkg.go.dev/net/http&quot;&gt;&lt;code&gt;net/http&lt;/code&gt; package documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>接口</title><link>https://blog.imicola.com/posts/%E6%8E%A5%E5%8F%A3/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E6%8E%A5%E5%8F%A3/</guid><description>Go语言接口的设计与实践</description><pubDate>Sat, 04 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;语法&lt;/h1&gt;
&lt;p&gt;我们使用 &lt;code&gt;type [接口名称] interface{}&lt;/code&gt; 来声明一个接口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Sayer interface {
	Say(messge string) error
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当我们需要实现一个接口的继承的时候，我们可以直接实现这个接口里的方法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Dog struct {
    name string
}

func (dog *Dog)Say(messge string) error{
	fmt.println(dog.name + &quot;说：&quot; + messge)
	return nil
}

// 在实现的时候我们可以
var sa Sayer
d := &amp;amp;Dog{&quot;name&quot;}
sa = d
sa.Say()       //接口可以调用d的方法了，此时我们实现了接口
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;工程约束&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;命名约定：
&lt;ul&gt;
&lt;li&gt;单一方法接口：以&lt;code&gt;er&lt;/code&gt;结尾，例如&lt;code&gt;Reader&lt;/code&gt;,&lt;code&gt;Writer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;不要预先定义：接口应该由使用者定义，而不是生产者定义&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接口设计：&lt;strong&gt;越小越好&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Go语言鼓励组合小接口而不是去创建一个庞大的接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;接受接口，返回结构体
&lt;ul&gt;
&lt;li&gt;在设计一个函数的时候，应该尽量接受接口，以提高函数的通用性&lt;/li&gt;
&lt;li&gt;函数应该返回具体的结构体&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;接口能干什么&lt;/h1&gt;
&lt;p&gt;下面我们用一个简单的需求展现&lt;code&gt;interface&lt;/code&gt;的功能&lt;/p&gt;
&lt;h2&gt;需求描述：多渠道告警系统&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;我们的任务是编写一个告警小程序。这个程序需要支持多种通知方式（如邮件、短信），并且要能够轻松扩展新的通知方式（比如以后要加一个“钉钉告警”），而不需要修改核心的业务逻辑代码。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;接口定义:&lt;br /&gt;
定义一个名为 Notifier 的接口，它必须包含一个方法：&lt;code&gt;Notify(message string) error&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;具体实现&lt;br /&gt;
实现两个结构体，并让它们都满足 Notifier 接口：&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;EmailNotifier&lt;/code&gt;: 结构体里包含一个 &lt;code&gt;Address&lt;/code&gt; 字段。调用 &lt;code&gt;Notify&lt;/code&gt; 时，打印：“向邮件地址 [xxx] 发送了消息：[内容]”。&lt;br /&gt;
SMSNotifier: 结构体里包含一个 PhoneNumber 字段。调用 Notify 时，打印：“向手机号 [xxx] 发送了短信：[内容]”。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;ol&gt;
&lt;li&gt;核心业务逻辑（关键点）&lt;br /&gt;
编写一个函数 &lt;code&gt;SendAlerts&lt;/code&gt;：
&lt;ul&gt;
&lt;li&gt;参数：接受一个 Notifier 接口的切片（[]Notifier）和一条消息字符串。&lt;/li&gt;
&lt;li&gt;功能：循环遍历切片，调用每个元素的 Notify 方法。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;在 main 函数中：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建一个包含一个 &lt;code&gt;EmailNotifier &lt;/code&gt;和一个 &lt;code&gt;SMSNotifier&lt;/code&gt; 的切片。&lt;/li&gt;
&lt;li&gt;调用 &lt;code&gt;SendAlerts&lt;/code&gt; 函数，把这个切片传进去。&lt;br /&gt;
进阶思考:&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;如果现在想增加一个 WeChatNotifier，你是否需要修改 SendAlerts 函数的代码？&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;参考：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
	&quot;fmt&quot;
)

type Notifier interface {
	Notify(message string) error
}

type EmailNotifier struct {
	Address string
}

type SMSNotifier struct {
	Phonenumber string
}

func (emi *EmailNotifier) Notify(message string) error {
	fmt.Println(&quot;向邮件地址&quot; + emi.Address + &quot;发送了信息:&quot; + message)
	return nil
}

func (sms *SMSNotifier) Notify(message string) error {
	fmt.Println(&quot;向手机号&quot; + sms.Phonenumber + &quot;发送了信息&quot; + message)
	return nil
}

func SendAlerts(notify []Notifier) error {
	for _, n := range notify {
		n.Notify(&quot;Testmessage&quot;)
	}
	return nil
}

// 新增WeChatNotifier
type WeChatNotifier struct {
	WechatID string
}

func (wec *WeChatNotifier) Notify(message string) error {
	fmt.Println(&quot;向微信号:&quot; + wec.WechatID + &quot;推送：&quot; + message)
	return nil
}

func main() {
	nois := []Notifier{&amp;amp;EmailNotifier{&quot;test1&quot;}, &amp;amp;SMSNotifier{&quot;test2&quot;}}
	SendAlerts(nois)
	nois = append(nois, &amp;amp;WeChatNotifier{&quot;test3&quot;})
	SendAlerts(nois)
}
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>你可见过红色的飘带游荡在星空</title><link>https://blog.imicola.com/posts/%E4%BD%A0%E5%8F%AF%E8%A7%81%E5%88%B0%E7%BA%A2%E8%89%B2%E7%9A%84%E9%A3%98%E5%B8%A6%E6%B8%B8%E8%8D%A1%E5%9C%A8%E6%98%9F%E7%A9%BA/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E4%BD%A0%E5%8F%AF%E8%A7%81%E5%88%B0%E7%BA%A2%E8%89%B2%E7%9A%84%E9%A3%98%E5%B8%A6%E6%B8%B8%E8%8D%A1%E5%9C%A8%E6%98%9F%E7%A9%BA/</guid><description>《挽救计划》影评</description><pubDate>Sun, 26 Apr 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;[!important]+&lt;br /&gt;
本评价可能含有《挽救计划》的剧透，请有打算观影的朋友酌情观看&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;Chapter1:出发&lt;/h1&gt;
&lt;p&gt;有一名朋友,最近沉迷星际拓荒，在相关视频下刷到《挽救计划》这部影片，便萌生了一探究竟的想法，顺便带上了我。作为一部小说改编的超长科幻电影:spoiler[长达整整2个半小时],在影院亮灯的一瞬间，我的感受是 &quot;酣畅淋漓&quot;，在我的评价中已经达到佳作水准。这部片子最好的有点如果让我总结的话，就是 &quot;逻辑完整且剧情正常&quot;。&lt;/p&gt;
&lt;p&gt;客观来说《挽救计划》的制作水准绝对是出色的，虽然没有很多《流浪地球》中注重&quot;大场面&quot;来进行宏大叙事的视觉冲击，但是其将很多细节打磨的非常好，以一种 &quot;纪实叙事&quot; 将信息反馈给幕前的观众，而这一特性在与外星人从接触到熟悉这一段中将会体现的更加充分。&lt;/p&gt;
&lt;h1&gt;Chapter2:第一次接触&lt;/h1&gt;
&lt;p&gt;&quot;第一次接触&quot; 是一个专有名词，表示人类对地外智慧生命的第一次接触与交流。在有 &quot;外星人&quot; 这样题材的科幻中，将 &quot;外星人&quot; 与 &quot;人类&quot; 的关系塑造清楚且逻辑正常其实是一个非常困难的事情。很容易陷入一种 &quot;人类中心主义&quot; 的陷阱之中，而这个片子巧妙的将两个文明的第一次接触从两个个体的接触展开。&lt;/p&gt;
&lt;p&gt;这其实是一个高明的写法,描述两个文明中&quot;文明的冲突&quot;比两个个体的&quot;个体的冲突&quot;会难写的多，因为在个人中可以放大个体所展现的 &quot;非理性&quot; 的部分:spoiler[爱与和平有什么不好的!]将猜疑与揣测转化为一种纯粹的处于个体内心的&quot;冲动感&quot;,文明会去需要考虑牺牲自己是不是值得的但是个体并不会，在这部片子中主角和洛基直接先后的救赎就是这样的体现。空旷的太空与孤身一人:spoiler[(一石头?)]的孤独感让两个个体成为互相支撑他人的存在，而这种属于个体的强烈情感不仅可以让导演更好的发挥出属于 &quot;文明的温暖面&quot; 的叙事，也能让我们更好体会这部片子的主旨：&quot;在面对未知的文明时我们是否有勇气拯救对面&quot;&lt;/p&gt;
&lt;h1&gt;Chapter3:红色的飘带&lt;/h1&gt;
&lt;p&gt;评价科幻作品，我们通常观察其三个内核:逻辑,想象力,深层人文属性。就像&quot;喜剧的内核是悲剧一般&quot;,科幻作品最终讨论的 &quot;在面对一个可能存在的情形中，人(人类)会怎么做&quot;。《星际穿越》让我们体会到了人类在面对绝对未知时候展现的勇气，《流浪地球》告诉我们人类团结的力量能有多大。这部剧则从人的视角出发，传达了&quot;文明的温暖面&quot;这样的主旨。&lt;/p&gt;
&lt;p&gt;逻辑叙事方面，本作的插叙做的非常优秀。:spoiler[我一开始只是以为是纯粹的插叙，后面仔细一想才发现这其实是主角被&quot;绑&quot;到飞船后逐渐找回记忆的过程]同时，在科学方面的严谨性做的也不错,将噬星体从被发现到捕获利用，到主角在天仓五中收集捕食者等描述，逻辑是合理的且具有想象力的。&lt;/p&gt;
&lt;h1&gt;Chapter4:星河中的文明&lt;/h1&gt;
&lt;p&gt;其实仔细一想，这篇文章特意提及噬星体已经感染了几乎所有文明，这就和人类共同面对一个困难一样。全宇宙的文明都共同面对一个问题。所以到这样的一个层面，洛基说的那句 &quot;拯救地球，拯救波江星&quot;其实就是两个人的抉择，而这样的抉择从两人:spoiler[外星人也是人(]互相选择为对方牺牲的决定而体现。这正是一种善良与温暖，而就是这样的善良与温暖同时救了地球和波江星。&lt;/p&gt;
&lt;p&gt;环境塑造方面，这部片子可谓是致敬了一堆片，而且其对于太空遥远距离的 &quot;孤独感&quot; 的氛围塑造也是不得不品的，而真是这种出色的孤独塑造，让两个在太空流浪的文明个体有了交流和依靠。&lt;/p&gt;
&lt;p&gt;总的来说，这是一部质量上乘的，故事逻辑清晰完整且能让我一下子全神贯注投入到其故事中的优秀科幻作品，好评喵&lt;/p&gt;
</content:encoded></item><item><title>基础知识</title><link>https://blog.imicola.com/posts/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/</guid><description>这是docker 系列文章的第三期喵</description><pubDate>Sat, 28 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;容器&lt;/h1&gt;
&lt;p&gt;试想一下，假如你正在和团队一起开发一个Web应用，主要有三个组件构成—— 一个 React 前端，一个 Python API 和一个 PostgreSQL 数据库，如果你想在这个项目上工作，你就必须安装 Node,Python 和 PostgreSQL&lt;/p&gt;
&lt;p&gt;那我们如何确保自己和团队中的其他开发人员使用相同的版本？或者如何保证我们的 CI/CD 系统相同？或者生产环境中使用的版本相同?&lt;/p&gt;
&lt;p&gt;这个时候我们就可以选择进入容器&lt;/p&gt;
&lt;p&gt;什么是容器？简单来说，容器是为我们的应用程序的每个组件提供的隔离进程。每个组件——前端 React 应用程序、Python API 引擎和数据库——都在其自己的独立环境中运行，与您机器上的其他所有内容完全隔离。&lt;/p&gt;
&lt;p&gt;一般来说容器具有以下特点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;自包含&lt;/strong&gt; : 每个容器都有其所运行需要的一切，不依赖主机的任何依赖&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;隔离&lt;/strong&gt;：  每个容器都是在隔离状态运行的，降低其对主机和其他容器的影响&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;独立&lt;/strong&gt;:  每个容器都是独立管理的，删除一个容器不会影响其他容器&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;便携&lt;/strong&gt;:  容器可以在任何地方运行，在开发机器和服务器等地方都是以相同方式工作&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;容器实践&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;我们的实践都默认在 CLI 环境下进行，这是为了更好的适应 Shell环境和服务器环境，当然如果一个工作可以在vscode远程链接上使用，我们也会讲。&lt;s&gt;绝对不是imicola是用wsl的&lt;/s&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;启动第一个容器&lt;/h3&gt;
&lt;p&gt;打开命令行终端输入:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker run -d -p 8080:80 docker/welcome-to-docker
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]- 命令的解析&lt;br /&gt;
在这个命令中， &lt;code&gt;docker run&lt;/code&gt; 是一个复合命令，其结合了 &lt;code&gt;docker create&lt;/code&gt; 和 &lt;code&gt;docker start&lt;/code&gt; 两个命令，同时一些参数表示如下：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; 表示后台运行，不加这个参数会让docker输出日志而不是返回终端环境，此时如果按下 &amp;lt;Ctrl-c&amp;gt; 会导致容器终止&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p 8080:80&lt;/code&gt; 表示将本机8080端口映射到docker的80端口上&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;此时我们便完成了第一个容器的启动&lt;/p&gt;
&lt;h3&gt;查看正在运行的容器&lt;/h3&gt;
&lt;p&gt;要想查看正在运行的容器，我们可以使用 &lt;code&gt;docker ps&lt;/code&gt; 命令，这个命令会输出所有正在运行的容器&lt;/p&gt;
&lt;p&gt;我们将会看到类似的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                      NAMES
 a1f7a4bb3a27   docker/welcome-to-docker   &quot;/docker-entrypoint.&quot;   11 seconds ago   Up 11 seconds   0.0.0.0:8080-&amp;gt;80/tcp       gracious_keldysh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个容器运行的是一个简单的web服务器，当我们处理更复杂的项目的时候，我们会在不同的容器中运行不同的部分&lt;/p&gt;
&lt;h3&gt;访问前端&lt;/h3&gt;
&lt;p&gt;当我们启动容器之后，我们便将容器的一个端口暴露在我们的机器上，对于这个容器，我们可以用 &lt;a href=&quot;http://localhost:8080/&quot;&gt;http://localhost:8080&lt;/a&gt; 来访问&lt;/p&gt;
&lt;h3&gt;关闭容器&lt;/h3&gt;
&lt;p&gt;要关闭一个容器，我们可以使用 &lt;code&gt;docker stop&lt;/code&gt; 命令，这个命令需要使用容器的ID，使用 &lt;code&gt;docker ps&lt;/code&gt; 获取即可&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Tip]+&lt;br /&gt;
当我们通过ID引用容器的时候，不需要提供完整的ID，只需要提供足够多的ID部分保证其唯一性即可，如我们可以使用 &lt;code&gt;docker stop a1f&lt;/code&gt; 来停止我们的示例容器&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;镜像&lt;/h1&gt;
&lt;p&gt;如果说容器是一个隔离的进程，那它会从什么地方获取文件和配置，又如何共享这些环境？这便是容器镜像发挥作用的地方&lt;/p&gt;
&lt;p&gt;容器镜像是一个标准化的软件包，包含了运行容器所需的一切文件、运行库和配置&lt;/p&gt;
&lt;p&gt;镜像有两个重要的原则:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;镜像是不可变的，一旦镜像被创建就无法修改它，你只能创建新的镜像或者在原有的镜像上添加更改&lt;/li&gt;
&lt;li&gt;容器镜像由层组成，每一层代表一组文件系统更改，这些更改可以添加，删除，或者修改文件&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这两个原则让我们可以扩展现有镜像或在现有镜像基础上添加内容。例如，我们正在构建一个 Python 应用程序，我们可以从 &lt;a href=&quot;https://hub.docker.com/_/python&quot;&gt;Python 镜像&lt;/a&gt; 开始，并添加额外的层来安装您应用程序的依赖项并添加代码。这使我们可以专注于我们的应用程序，而不是 Python 本身。&lt;/p&gt;
&lt;h2&gt;镜像实践&lt;/h2&gt;
&lt;h3&gt;搜索并下载镜像&lt;/h3&gt;
&lt;p&gt;我们可以用 &lt;code&gt;docker search&lt;/code&gt; 命令搜索镜像:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker search docker/welcome-to-docker
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时我们将看到类似的输出，这个输出向我们展示有关 Docker Hub 上可用相关镜像的信息&lt;/p&gt;
&lt;p&gt;然后，我们可以使用 &lt;code&gt;docker pull&lt;/code&gt; 拉取镜像，就如同使用 &lt;code&gt;git clone&lt;/code&gt; 拉取仓库一般&lt;/p&gt;
&lt;h3&gt;了解镜像&lt;/h3&gt;
&lt;p&gt;使用 &lt;code&gt;docker image ls&lt;/code&gt; 命令可以列出我们已经下载的镜像，使用 &lt;code&gt;docker image history&lt;/code&gt; 命令可以列出镜像的层&lt;/p&gt;
&lt;p&gt;我们会看到类似的输出：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
648f93a1ba7d   4 months ago   COPY /app/build /usr/share/nginx/html # buil...  1.6MB     buildkit.dockerfile.v0
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  CMD [&quot;nginx&quot; &quot;-g&quot; &quot;daemon...  0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT           0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  EXPOSE 80                    0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  ENTRYPOINT [&quot;/docker-entr...  0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop) COPY file:9e3b2b63db9f8fc7...  4.62kB
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop) COPY file:57846632accc8975...  3.02kB
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop) COPY file:3b1b9915b7dd898a...  298B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop) COPY file:caec368f5a54f70a...  2.12kB
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop) COPY file:01e75c6dd0ce317d...  1.62kB
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c set -x     &amp;amp;&amp;amp; addgroup -g 101 -S ...  9.7MB
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  ENV PKG_RELEASE=1            0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  ENV NGINX_VERSION=1.25.3     0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  LABEL maintainer=NGINX Do...  0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop)  CMD [&quot;/bin/sh&quot;]              0B
&amp;lt;missing&amp;gt;      5 months ago   /bin/sh -c #(nop) ADD file:ff3112828967e8004...  7.66MB
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此输出显示所有层,他们的大小以及创建该层的命令&lt;/p&gt;
&lt;p&gt;而对于构建镜像的知识我们会在后面的文章中详细介绍&lt;/p&gt;
&lt;h1&gt;注册表与镜像仓库&lt;/h1&gt;
&lt;p&gt;既然我们已经知道了什么是容器以及镜像，简洁的说镜像就如同git仓库项目一般，那么既然是一个项目，必然有其存储的地方&lt;/p&gt;
&lt;p&gt;我们当然可以在我们的计算机中存储我们的容器镜像，但是如果我们需要分享或者需要协同工作呢？此时我们就需要镜像仓库&lt;/p&gt;
&lt;p&gt;镜像仓库是存储和共享容器的集中位置，其可以是私有的或公共的。&lt;a href=&quot;https://hub.docker.com/&quot;&gt;Docker Hub&lt;/a&gt; 就是一个任何人都可以使用的公共仓库:spoiler[我看未必]，也是默认的仓库&lt;/p&gt;
&lt;p&gt;除了Docker Hub以外，我们也有许多的容器注册表，包括&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://aws.amazon.com/ecr/&quot;&gt;Amazon Elastic Container Registry(ECR)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://azure.microsoft.com/en-in/products/container-registry&quot;&gt;Azure Container Registry (ACR)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://cloud.google.com/artifact-registry&quot;&gt;Google Container Registry (GCR)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://help.aliyun.com/zh/acr/&quot;&gt;Alibaba Cloud Container Registry(ACR)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://docs.github.com/zh/packages/working-with-a-github-packages-registry/working-with-the-container-registry&quot;&gt;GitHub Container Registry(GHCR)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;注册表实践&lt;/h2&gt;
&lt;p&gt;:spoiler[对于中国用户而言,下载 Docker 注册表镜像和上传是有些限制的,在之前可以使用镜像来解决这些问题，但是目前来看镜像站已经被wall完了,所以需要下面内容默认你会使用魔法]&lt;/p&gt;
&lt;h3&gt;注册Docker Hub账户并且创建仓库&lt;/h3&gt;
&lt;p&gt;如果你还没有创建账户，请前往 &lt;a href=&quot;https://hub.docker.com/&quot;&gt;Docker Hub&lt;/a&gt; 页面注册一个新的 Docker 账户。&lt;/p&gt;
&lt;p&gt;之后选择创建仓库 (Create a Repository)并按需求填入名称，描述与可访问性&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]- 关于可访问性&lt;br /&gt;
docker 对个人用户相当慷慨，为免费用户提供无限的公开仓库创建权限和单个私人仓库创建权限，对于个人开发者，这完全足够，如果你需要更多的私人仓库，可以选择升级Plan或者使用GHCR&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;创建并推送镜像&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;创建镜像一般会编写Dockerfile，这个知识我们将在后面的内容中了解&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果你是Docker Desktop用户，你需要先登录Docker Desktop然后按下面的方法来推送镜像&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;运行以下命令来构建 Docker 镜像，将 &lt;code&gt;YOUR_DOCKER_USERNAME&lt;/code&gt; 替换为您的用户名，将 &lt;code&gt;DOCKER-NAME&lt;/code&gt; 改为你的容器名称&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker build -t &amp;lt;YOUR_DOCKER_USERNAME&amp;gt;/&amp;lt;DOCKER-NAME&amp;gt; .
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;运行&lt;code&gt;docker image ls&lt;/code&gt;命令以列出新创建的 Docker 镜像&lt;/li&gt;
&lt;li&gt;通过运行&lt;code&gt;docker run -d -p 8080:8080 &amp;lt;YOUR_DOCKER_USERNAME&amp;gt;/&amp;lt;DOCKER-NAME&amp;gt;&lt;/code&gt; 命令启动容器以测试镜像&lt;/li&gt;
&lt;li&gt;使用 &lt;a href=&quot;https://docker.cadn.net.cn/reference/cli_docker_image_tag&quot;&gt;&lt;code&gt;docker tag&lt;/code&gt;&lt;/a&gt; 命令来标记 Docker 镜像。Docker 标签允许您对镜像进行标记和版本控制&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker tag &amp;lt;YOUR_DOCKER_USERNAME&amp;gt;/&amp;lt;DOCKER-NAME&amp;gt; &amp;lt;YOUR_DOCKER_USERNAME&amp;gt;/&amp;lt;DOCKER-NAME&amp;gt;:1.0 
&lt;/code&gt;&lt;/pre&gt;
&lt;ol&gt;
&lt;li&gt;最后，通过使用 &lt;a href=&quot;https://docker.cadn.net.cn/reference/cli_docker_image_push&quot;&gt;&lt;code&gt;docker push&lt;/code&gt;&lt;/a&gt; 命令将新构建的镜像推送到您的 Docker Hub 仓库：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;docker push &amp;lt;YOUR_DOCKER_USERNAME&amp;gt;/&amp;lt;DOCKER-NAME&amp;gt;:1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样你就成功将一个镜像推送到了Docker Hub中&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]- 那对于不使用docker desktop的用户呢？&lt;/p&gt;
&lt;p&gt;我们一般会使用 Docker Engine自带的命令行工具来完成镜像的推送与拉取&lt;/p&gt;
&lt;p&gt;首先你需要登录仓库，使用 &lt;code&gt;docker login&lt;/code&gt; 命令登录docker hub，如果是私有仓库 &lt;code&gt;docker login &amp;lt;仓库地址&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;之后也是使用 &lt;code&gt;docker tag&lt;/code&gt; 为镜像打标签，再使用 &lt;code&gt;docker push&lt;/code&gt; 命令推送镜像即可&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>简单利用 Docker 进行开发</title><link>https://blog.imicola.com/posts/%E7%AE%80%E5%8D%95%E5%88%A9%E7%94%A8-docker-%E8%BF%9B%E8%A1%8C%E5%BC%80%E5%8F%91/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E7%AE%80%E5%8D%95%E5%88%A9%E7%94%A8-docker-%E8%BF%9B%E8%A1%8C%E5%BC%80%E5%8F%91/</guid><description>这是docker 系列文章的第二期喵</description><pubDate>Fri, 27 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;说明&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;本项目跑在 WSL(Arch) 下,使用 docker desktop，有一些踩坑指北&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;现在我们已经安装了 Docker Desktop，准备好进行一些应用程序开发了。具体来说，您将执行以下操作：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;克隆并启动一个开发项目&lt;/li&gt;
&lt;li&gt;对后端和前端进行更改&lt;/li&gt;
&lt;li&gt;立即查看更改&lt;/li&gt;
&lt;/ul&gt;
&lt;h1&gt;启动项目&lt;/h1&gt;
&lt;p&gt;在开始使用之前，我们需要先将项目配置文件克隆到本地文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;git clone https://github.com/docker/getting-started-todo-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后进入项目文件夹&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;cd getting-started-todo-app
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;随后我们便可以使用 &lt;code&gt;docker compose&lt;/code&gt; 启动开发环境&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;docker compose watch
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]+ 什么是 docker compose ?&lt;/p&gt;
&lt;p&gt;Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。它使用 YAML 文件来配置应用程序的服务、网络和卷等，然后通过一个简单的命令就可以启动整个应用程序的所有服务。&lt;/p&gt;
&lt;p&gt;简单来说 docker compose 就是将多个容器编排在一起从而实现一个服务的配置工具，通过docker compose我们可以原子化每一个容器，从而不会将所有环境都配置在一个大容器中&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;然后就是神秘的地方了&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;docker会先拉取 &lt;code&gt;traefik&lt;/code&gt;,&lt;code&gt;mysql&lt;/code&gt;,&lt;code&gt;phpmyadmin&lt;/code&gt;这三个镜像，如果你没有较好的网络，可能会导致拉取失败，从小&lt;/li&gt;
&lt;li&gt;随后是进行 building，会下载一些包比如 &lt;code&gt;npm&lt;/code&gt; 等，然后进行 &lt;code&gt;npm install&lt;/code&gt;构建整个容器&lt;/li&gt;
&lt;li&gt;但是最后在启动后端和 &lt;code&gt;traefik&lt;/code&gt; 的时候会返回报错&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;此时我们使用 &lt;code&gt;docker ps -a&lt;/code&gt; 指令列出所有的容器&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;显然， &lt;code&gt;traefik&lt;/code&gt; 和 &lt;code&gt;backend&lt;/code&gt; 启动失败，imicola这里猜测是因为前面的端口不可用，导致无法后端无法链接到前端，同时没办法通过网络映射到主机端口，导致返回错误，从而中断了容器启动&lt;/p&gt;
&lt;p&gt;&lt;s&gt;真是复杂的网络环境啊&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;询问 ai 后得知在这个报错中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Error response from daemon: ports are not available: exposing port TCP 0.0.0.0:80 -&amp;gt; 127.0.0.1:0: /forwards/expose returned unexpected status: 500
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;docker desktop尝试将容器内的端口映射到 Window 主机的80端口中&lt;/p&gt;
&lt;p&gt;显然window的80端口是非常繁忙的，通过&lt;code&gt;netstat -ano | findstr :80&lt;/code&gt;我们可以看到是那个进程在占用这个端口&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; netstat -ano | findstr :80
  TCP    0.0.0.0:80             0.0.0.0:0              LISTENING       4
  TCP    10.56.96.140:47703     111.31.2.195:8081      TIME_WAIT       0
  TCP    10.56.96.140:50794     120.233.22.121:8080    ESTABLISHED     20960
  TCP    10.56.96.140:58477     1.95.20.105:80         ESTABLISHED     22052
  TCP    127.0.0.1:8080         0.0.0.0:0              LISTENING       14976
  TCP    [::]:80                [::]:0                 LISTENING       4
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很好，是PID 4，那大概率是一个系统进程了，查命令管理器发现，PID4对应的是 &lt;code&gt;System&lt;/code&gt; , &lt;s&gt;那谁也抢不过你了&lt;/s&gt;&lt;/p&gt;
&lt;p&gt;那我们就让端口避让，通过修改 &lt;code&gt;compose.yaml&lt;/code&gt; 配置可以修改对应端口,在44行处找到proxy post配置，将其修改尝试&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;  proxy:
	image: traefik:v3.6
    command: --providers.docker
    ports:
      - 8081:80  #将80:80
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;接着在命令行中输入 &lt;code&gt;docker compose watch&lt;/code&gt;,这次构建顺利，此时服务已经启动，在浏览器中输入 &lt;code&gt;http://localhost:8081/&lt;/code&gt; 即可访问服务&lt;/p&gt;
&lt;p&gt;可以看到这是一个 &lt;em&gt;TODO List&lt;/em&gt; 的Web应用，接下来我们了解如何直接编辑项目获得实时的开发预览&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-2.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h1&gt;环境里有什么&lt;/h1&gt;
&lt;p&gt;这个 docker 项目里都有什么呢？从高层次来看，有一些容器，每一个容器都为这个web程序提供特定的功能&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;React前端: 对应 &lt;code&gt;getting-started-todo-app-client-1&lt;/code&gt;，一个运行 React 开发服务器的 Node 容器，使用 Vite 构建&lt;/li&gt;
&lt;li&gt;Node后端: 对应 &lt;code&gt;getting-started-todo-app-backend-1&lt;/code&gt;，后端提供一个API，用于检索，创建，和删除代办事项的能力&lt;/li&gt;
&lt;li&gt;MySQL数据库： 对应 &lt;code&gt;getting-started-todo-app-mysql-1&lt;/code&gt; 用于存储项目列表的数据库&lt;/li&gt;
&lt;li&gt;phpMyAdmin: 对应 &lt;code&gt;getting-started-todo-app-phpmyadmin-1&lt;/code&gt; 一个用于与数据库交互的基于Web的界面，可通过 &lt;a href=&quot;http://db.localhost/&quot;&gt;http://db.localhost&lt;/a&gt;访问&lt;/li&gt;
&lt;li&gt;Traefik 代理: 对应 &lt;code&gt;getting-started-todo-app-proxy-1&lt;/code&gt; 一个应用代理程序，可以将请求路由到正确的服务，它将所有针对 &lt;code&gt;localhost/api/*&lt;/code&gt; 的请求发送到后端，将针对 &lt;code&gt;localhost/*&lt;/code&gt; 的请求发送到前端，然后将针对 &lt;code&gt;db.localhost&lt;/code&gt; 的请求发送到 phpMyAdmin。这使得可以使用主机一个端口访问所有应用程序（而不是为每个服务使用不同的端口）。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;在这个环境中，作为开发者的我们无需安装或配置任何服务，无需填充数据库架构，无需配置数据库凭据，也无需其他操作。我们只需要 Docker Desktop。其余的一切都会自动运行。(&lt;s&gt;难说&lt;/s&gt;)&lt;/p&gt;
&lt;h1&gt;对应用进行更改&lt;/h1&gt;
&lt;p&gt;使用 docker 进行开发也能获得如同原生开发一般即时观测的效果，我们只需要和往常一样修改项目中的内容即可，下面是一些例子&lt;/p&gt;
&lt;h2&gt;后端 - 更改问候语&lt;/h2&gt;
&lt;p&gt;页面顶部的问候语通过 &lt;code&gt;/api/greeting&lt;/code&gt; 进行填充，目前它总是只返回 &quot;Hello world&quot;,如果想改变它，我们可以在 &lt;code&gt;backend/src/routes/getGreeting.js&lt;/code&gt;文件中修改这个js文件顶部的 const 变量，当然我们也可以将其变化为一个数组吗，然后随机调用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const GREETING = [
    &quot;Hello world&quot;,
    &quot;喵喵喵&quot;,
    &quot;Edit with docker&quot;,
];

module.exports = async (req, res) =&amp;gt; {
    res.send({
        greeting: GREETING[Math.floor(Math.random() * GREETING.length)],
    });
};

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;刷新界面即可看到，已经更新&lt;/p&gt;
&lt;p&gt;![[Pasted image 20260327181221.png]]&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;前端 - 界面美化&lt;/h2&gt;
&lt;p&gt;我觉得这个界面不好看怎么办？ 让ai进行美化！(&lt;s&gt;或者自己干&lt;/s&gt;)&lt;/p&gt;
&lt;p&gt;前端的文件位于 &lt;code&gt;/client&lt;/code&gt; 中，按正常思路对前端进行美化，刷新页面观察效果即可&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-3.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;依旧ai审美&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;关闭docker&lt;/h1&gt;
&lt;p&gt;对于 docker compose,我们可以使用 &lt;code&gt;docker compose down&lt;/code&gt; 将容器彻底删除 (不会删除镜像)，这是推荐的方式，可以释放容器的空间，因为保留了镜像，下次使用的时候直接构建就可以了&lt;/p&gt;
&lt;p&gt;如果不想删除容器，方便下次快速启动，我们可以使用 &lt;code&gt;docker compose stop&lt;/code&gt; 命令&lt;/p&gt;
</content:encoded></item><item><title>Docker 入门</title><link>https://blog.imicola.com/posts/docker%E5%85%A5%E9%97%A8/</link><guid isPermaLink="true">https://blog.imicola.com/posts/docker%E5%85%A5%E9%97%A8/</guid><description>docker 是好东西喵，这是docker系列的第一期喵</description><pubDate>Thu, 26 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;我觉得任何人在学习docker之前都应该去看看官方文档&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;什么是Docker？&lt;/h1&gt;
&lt;p&gt;在官方文档中这么写到:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Docker 是一个用于开发、交付和运行应用程序的开放平台。Docker 使您能够将应用程序与基础设施分离，从而快速交付软件。借助 Docker，您可以像管理应用程序一样管理基础设施。通过利用 Docker 的代码交付、测试和部署方法，您可以显著缩短编写代码与在生产环境中运行代码之间的延迟。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;实际上，Docker是容器的一种，其核心思路是将应用程序及其所需的全部环境（代码、运行库、配置文件等）打包成一个独立的单元，这样就实现了应用程序在任何地方都能以相同的方式运行&lt;/p&gt;
&lt;p&gt;这样有什么好处呢？&lt;/p&gt;
&lt;p&gt;Docker解决的是 “项目在我的电脑上能跑，但是在服务器/你的电脑上就跑不了” 的问题&lt;/p&gt;
&lt;h1&gt;Docker 架构&lt;/h1&gt;
&lt;p&gt;Docker使用 客户端-服务器 架构，Docker 客户端与 Docker 守护进程通信，后者负责构建、运行和分发 Docker 容器的主要工作&lt;/p&gt;
&lt;p&gt;Docker 客户端和守护进程可以运行在同一个系统上，或者我们可以将 Docker 客户端连接到远程 Docker 守护进程&lt;/p&gt;
&lt;h2&gt;Docker 守护进程&lt;/h2&gt;
&lt;p&gt;Docker 守护进程 (&lt;code&gt;dockerd&lt;/code&gt;) 监听 Docker API 请求并管理 Docker 对象，例如镜像、容器、网络和卷。守护进程还可以与其他守护进程通信以管理 Docker 服务。&lt;/p&gt;
&lt;h2&gt;Docker 客户端&lt;/h2&gt;
&lt;p&gt;Docker 客户端是用户与Docker交互的主要方式，当我们使用诸如 &lt;code&gt;docker run&lt;/code&gt; 等命令的时候，客户端会将这些命令发送给 &lt;code&gt;dockerd&lt;/code&gt;,然后由 &lt;code&gt;dockerd&lt;/code&gt; 执行这些命令&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;Docker 对象&lt;/h1&gt;
&lt;p&gt;当我们使用 Docker 的时候，会创建或使用镜像，容器，网络，卷，插件等其他对象，这一小节简述了一些对象&lt;/p&gt;
&lt;h2&gt;镜像&lt;/h2&gt;
&lt;p&gt;镜像是一个 &lt;strong&gt;只读&lt;/strong&gt; 模板，包含用于创建 Docker 容器的指令。通常来说，镜像基于另一个镜像，并且进行一些额外的定制。例如，我们可以构建一个基于 &lt;code&gt;Ubuntu&lt;/code&gt; 镜像的镜像，但安装了 &lt;code&gt;Apache Web&lt;/code&gt; 服务器和我们的应用程序，以及我们应用程序所需要的详细配置&lt;/p&gt;
&lt;p&gt;我们可能会创建自己的镜像，或者只使用他人创建并发布在注册表中的镜像。要构建自己的镜像，可以创建一个 Dockerfile，使用简单的语法定义创建和运行镜像所需的步骤。Dockerfile 中的每条指令都会在镜像中创建一个层。当我们修改 Dockerfile 并重新构建镜像时，只有那些发生变化的层会被重新构建。这也是镜像相比于其他虚拟化技术更加轻量、小巧和快速的部分原因。&lt;/p&gt;
&lt;h2&gt;容器&lt;/h2&gt;
&lt;p&gt;容器是 &lt;em&gt;镜像的可运行实例&lt;/em&gt;。我们可以使用 Docker CLI 或者 API 进行创建，启动，停止，移动和删除容器。我们同样可以将容器连接到一个或多个网络，为其挂载存储，甚至根据当前状态为其创建新的镜像&lt;/p&gt;
&lt;p&gt;默认情况下，容器与其他容器及其主机相对隔离。当然，我们可以控制容器的网络、存储或其他底层子系统与其他容器或主机的隔离程度。&lt;/p&gt;
&lt;p&gt;容器由其镜像以及在创建或启动时提供的任何配置选项定义。当容器被移除时，未存储在持久存储中的任何状态更改都会消失。&lt;/p&gt;
</content:encoded></item><item><title>计算机网络-自顶向下方法导读</title><link>https://blog.imicola.com/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C--%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E6%96%B9%E6%B3%95/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C--%E8%87%AA%E9%A1%B6%E5%90%91%E4%B8%8B%E6%96%B9%E6%B3%95/</guid><description>对计算机网络的导读,从章节上拆分本书</description><pubDate>Sun, 22 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;计算机网络&lt;/h1&gt;
&lt;p&gt;《计算机网络:自顶向下》这本书是一本学习计算机网络的好书，和国内常见的教材不一样的地方，这本书从顶层开始向下进行剖析解析，从应用层到物理链路，而不是上来就从底层开始讲述，这篇文章主要是对整本书的一个基础导读，帮助我们梳理清楚这个快800多页大厚本究竟在说些什么&lt;/p&gt;
&lt;p&gt;我们将按照章节对这本书进行拆分，由于原书对很多特别细致的知识点都有提及(&lt;s&gt;甚至讲了双绞铜线这种&lt;/s&gt;)，所以我们不会完全列举出来，而是将我认为比较重要的知识点与名词列出来&lt;/p&gt;
&lt;h1&gt;chapter-1 Introduction&lt;/h1&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;p&gt;第一章可以看作是整本书的缩影，其结构是由内向外的，其主要讲述了下面的这部分内容：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;定义基础&lt;/strong&gt;: 从「具体构成」(软件和硬件) 和 「提供的服务」(基础设施) 两个角度定义了什么是互联网,并且引出 [[协议]] 的概念&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络边缘&lt;/strong&gt;: 介绍了我们日常生活中的设备，以及他们是如何通过 「接入网」 接入互联网的&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络核心&lt;/strong&gt;: 深入网络内部，探讨负责转发数据的路由器和交换机网络。重点对比了「分组交换」与「电路交换」，并解释了互联网是由众多 ISP（网络服务提供商）组成的 &lt;em&gt;网络的网络&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;性能指标&lt;/strong&gt;： 这一段内容讲解了数据在网络之间传输的时候为什么会产生延迟，为什么会丢包？以及什么因素决定了网络的吞吐量&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议分层与封装&lt;/strong&gt;: 这一节内容讲述为了应对网络的复杂性，网络被划分为 5 个层次:应用层、传输层、网络层、链路层、物理层  (这也是本书的结构),并且介绍了数据在不同的层之间如何进行封装&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络安全威胁&lt;/strong&gt;: 简单的讲述了一些互联网面临的常见的攻击手段&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;知识点与名词&lt;/h2&gt;
&lt;p&gt;第一章中我们需要理解的知识点将被列在下面:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;协议&lt;/strong&gt;: 原书1.1.3
&lt;ul&gt;
&lt;li&gt;协议(protocol)定义了在两个或多个通信实体之间交换的报文的格式和顺序，以及报文的发送/接收或其他事件所采取的操作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;端系统/主机&lt;/strong&gt;：原书1.2
&lt;ul&gt;
&lt;li&gt;位于网络边缘的设备：&lt;strong&gt;客户端&lt;/strong&gt; 通常是发起通信的设备，&lt;strong&gt;服务端&lt;/strong&gt; 是等待联系的设备&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分组交换&lt;/strong&gt;：原书1.3.1
&lt;ul&gt;
&lt;li&gt;互联网传输数据的核心方式: 发送方将长报文分割成小的数据块「即分组或数据包」，这些分组在网络中独立传输并且贡献链路宽带&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;存储转发&lt;/strong&gt;: 原书1.3.1
&lt;ul&gt;
&lt;li&gt;多数分组交换机（路由器）采用的机制。路由器必须接收到完整的数据包后，才能开始向输出链路转发它的第一个比特&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四种网络延迟&lt;/strong&gt;: 原书1.4.1
&lt;ul&gt;
&lt;li&gt;处理延迟
&lt;ul&gt;
&lt;li&gt;检查包头，决定将数据包导向何处所需要的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;排队延迟
&lt;ul&gt;
&lt;li&gt;数据包在路由器输出队列中等待传输的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;传输延迟
&lt;ul&gt;
&lt;li&gt;将数据包的所有比特推向链路所需的时间&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;传播延迟
&lt;ul&gt;
&lt;li&gt;比特在物理媒介中从一个路由器传播到下一个路由器所需的时间（取决于物理距离和光速）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;丢包&lt;/strong&gt;： 原书1.4.2
&lt;ul&gt;
&lt;li&gt;由于路由器的队列容量是有限的，当到达的流量过大导致队列满了时，新到达的包或队列中的包就会被丢弃&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;吞吐量与瓶颈链路&lt;/strong&gt;： 原书1.4.4
&lt;ul&gt;
&lt;li&gt;吞吐量是目标端接收数据的速率。在一条端到端的路径中，吞吐量往往受限于那条传输速率最慢的链路（即瓶颈链路）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;协议栈与封装&lt;/strong&gt; 原书1.5.2
&lt;ul&gt;
&lt;li&gt;数据从应用层向下传递时，每一层都会给它加上自己的「头部信息（Header）」，这就是封装。就像把信件装进快递信封，再装入集装箱一样&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h1&gt;Chapter-2 Application Layer&lt;/h1&gt;
&lt;p&gt;本章主要讲述的内容是在网络站点等这些 「上层方面」的应用层上网络的一些知识&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;应用层原理基础&lt;/strong&gt;: (2.1)
&lt;ul&gt;
&lt;li&gt;首先定义了网络应用是如何构建的，以及它们是如何通过“套接字（Socket）”这个门与底层的传输层（TCP/UDP）交互的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四大核心应用协议&lt;/strong&gt;：(2.2 - 2.5)
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Web与HTTP&lt;/strong&gt;(2.2):现代互联网的基石&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;电子邮件&lt;/strong&gt;(2.3): SMTP、IMAP等&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 目录服务&lt;/strong&gt;(2.4): 将人类可读的域名转换为机器可读的IP&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2P 文件分发&lt;/strong&gt;(2.5): 探讨去中心化的架构(如bt种子)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;内容分发网络 CDN&lt;/strong&gt;: (2.6)
&lt;ul&gt;
&lt;li&gt;为了解决海量视频/内容带来的带宽和延迟瓶颈，引出了 CDN&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Socket 编程Lab&lt;/strong&gt;: (2.7)
&lt;ul&gt;
&lt;li&gt;最后，回归代码，展示如何用 TCP 和 UDP 的 Socket API 写出真实的客户端与服务端&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;知识点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;网络应用架构和Socket接口&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;客户端-服务端架构&lt;/strong&gt; (Client-Server) ：服务器拥有固定的IP地址且始终在线，由客户端主动向服务器发起请求&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;套接字&lt;/strong&gt; (Socket) ： 进程通过网络发送和接受消息的软件接口&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP 与 UDP 的选择&lt;/strong&gt; : TCP提供面向链接，可靠的数据传输和拥塞控制；UDP 提供无连接，不可靠但无延迟的传输&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web 与 HTTP&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;无状态&lt;/strong&gt; (Stateless) 与 &lt;strong&gt;Cookies&lt;/strong&gt;：HTTP 服务器本身不保存客户端的状态。为了识别用户身份（如登录状态），引入了 Cookie 机制，在 HTTP 请求和响应的 Header 中传递状态&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;持久连接&lt;/strong&gt; (Persistent Connections)：HTTP/1.1 默认使用持久连接，允许在同一个 TCP 连接上发送多个 HTTP 请求和响应，避免了每次请求都消耗一次 TCP 握手的 RTT（往返时间）&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web 缓存 (代理服务器/Proxy)&lt;/strong&gt; : Web 缓存代表原始服务器满足 HTTP 请求，极大降低了客户端的响应延迟和机构接入链路的带宽压力&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTP/2 与 队头阻塞 (HOL Blocking)&lt;/strong&gt; ： 在 HTTP/1.1 中，如果一个大对象（如大视频）排在队列前面，会阻塞后面小对象的传输（队头阻塞）。HTTP/2 通过将消息分割成小帧 (Frames) 并在单个 TCP 连接上交错传输，解决了这个问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;核心功能&lt;/strong&gt;: 将人类易读的主机名转换为32位的IP地址&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;分布式层次数据库&lt;/strong&gt;: DNS 不是一台集中式服务器，而是由 根 DNS 服务器 (Root)、顶级域 DNS 服务器 (TLD) 和 权威 DNS 服务器 (Authoritative) 组成的层级结构&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 缓存&lt;/strong&gt;：为了减少延迟，DNS 广泛使用缓存技术。本地 DNS 服务器可以缓存它收到的任何解析结果，极大地减少了查询流量&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;视频流与CDN内容分发网络
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DASH&lt;/strong&gt; ： 客户端根据当前网络带宽动态选择请求不同清晰度（比特率）的视频块&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CDN&lt;/strong&gt; ： 为了避免单点故障和跨大洲传输的高延迟，CDN 在全球部署服务器集群&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DNS 重定向&lt;/strong&gt; ： CDN 利用 DNS 机制来拦截用户的请求，并将请求智能地重定向到距离用户最近或负载最低的 CDN 缓存节点&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;一个简单实例&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;[!NOTE]&lt;br /&gt;
我们来设想一个场景:&lt;br /&gt;
当我们在浏览器中输入 &lt;code&gt;www.bilibili.com&lt;/code&gt; 来访问B站并且播放视频，这些组件会这样进行工作:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;DNS介入: 浏览器首先需要知道 bilibili 网页服务器的 IP。它调用 DNS 发起查询&lt;/li&gt;
&lt;li&gt;CDN和DNS进行联动: Bilibili 不会返回单一的IP，其DNS服务器会利用 DNS 重定向技术，根据当前发起请求的主机地理位置，返回距离我们最近的CDN节点的IP地址&lt;/li&gt;
&lt;li&gt;TCP 握手与 HTTP 请求：浏览器拿到 IP 后，通过 Socket API 发起 TCP 三次握手，然后发送 HTTP GET 请求&lt;/li&gt;
&lt;li&gt;Cookie 维持状态：HTTP 请求头中带有 Cookie，让 Bilibili 知道你已经登录，并且推荐你喜欢的视频&lt;/li&gt;
&lt;li&gt;DASH 动态流媒体：当你开始播放时，视频播放器使用 DASH 协议，基于你当前的网络状况，不断通过 HTTP 请求不同画质的视频分片&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h1&gt;Chapter 3 Transport Layer&lt;/h1&gt;
&lt;p&gt;第三章的主要层级是传输层,其主要逻辑是:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;从抽象服务模型到通用的可靠性理论,再到TCP/UDP实现,最后探讨网络拥塞和现代协议演进&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这一章会学很多的传输协议,比如 TCP,UDP 等,属于是比较硬核的一章,可能需要花费更多的时间来吃透&lt;/p&gt;
&lt;h2&gt;结构&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;传输层服务与多路复用&lt;/strong&gt; (3.1 - 3.2)
&lt;ul&gt;
&lt;li&gt;网络层只负责把数据送到&quot;主机&quot;,而传输层负责将数据分发给主机上具体的&quot;应用进程&quot;(通过端口号实现多路复用/分解)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;无连接传输 UDP&lt;/strong&gt; (3.3)
&lt;ul&gt;
&lt;li&gt;一种&quot;裸奔&quot;的协议,仅提供多路复用和简单的错误检测,不保证可靠性,但延迟极低&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;可靠数据传输原理 RDT&lt;/strong&gt; (3.4)
&lt;ul&gt;
&lt;li&gt;这是一个纯理论推导章节.探讨在一个会丢包,会出错的底层网络上,如何通过确认(ACK),定时器,序列号和重传机制,一步步构建出绝对可靠的传输通道&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;面向链接的传输 TCP&lt;/strong&gt; (3.5)
&lt;ul&gt;
&lt;li&gt;理论落地.详细讲解 TCP 的报文结构,三次握手/四次挥手,RTT (往返时间) 估算,以及如何通过接收窗口(rwnd)实现流量控制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拥塞控制原理与 TCP 拥塞控制&lt;/strong&gt; (3.6-3.7)
&lt;ul&gt;
&lt;li&gt;探讨网络拥堵的代价.重点解析 TCP 如何通过拥塞窗口(cwnd)慢启动,拥塞避免(AIMD),快速恢复等算法&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;传输层的演进 QUIC&lt;/strong&gt; (3.8)
&lt;ul&gt;
&lt;li&gt;介绍运行在 UDP 之上的现代协议 QUIC,它解决了 TCP 队头阻塞问题,并实现了更快的安全连接建立(HTTP/3 的基础)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;知识点&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;端口与多路分解
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UDP 的 2 元组&lt;/strong&gt;:UDP 套接字仅由 (目的 IP,目的端口) 标识&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP 的 4 元组&lt;/strong&gt;:TCP 套接字由 (源 IP,源端口,目的 IP,目的端口) 唯一标识
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;逻辑链接&lt;/strong&gt;:为什么你的单台 Nginx Web 服务器在 80 端口上能同时处理成千上万个客户端连接? 因为每个客户端的源 IP 或源端口不同,TCP 依靠这 4 元组将流量精准分解到不同的线程/套接字中.这也解释了为什么客户端压测时会遇到&quot;端口耗尽(Port Exhaustion)&quot;问题&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;TCP 连接管理
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;三次握手 (3-way Handshake)&lt;/strong&gt;:客户端发送 SYN,服务端回复 SYNACK,客户端回复 ACK&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;四次挥手 &amp;amp; TIME_WAIT 状态&lt;/strong&gt;:连接关闭时,主动关闭方会进入 &lt;code&gt;TIME_WAIT&lt;/code&gt; 状态(通常持续 1-2 分钟),以确保最后一个 ACK 能到达对方&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SYN 泛洪攻击与 SYN Cookies&lt;/strong&gt;:恶意客户端只发 SYN 不发最后一步的 ACK,耗尽服务器资源.通过 SYN Cookies 技术,服务器初始不分配资源,而是用哈希值作为初始序列号,从而防御此类攻击&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;核心机制:序列号,ACK 与快速重传
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;累积确认 (Cumulative ACK)&lt;/strong&gt;:TCP 的 ACK 数字表示&quot;我正在等待接收的下一个字节号&quot;,这意味着该数字之前的所有字节都已成功接收&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;快速重传 (Fast Retransmit)&lt;/strong&gt;:如果网络丢包,发送方通常要等一个漫长的超时时间(Timeout)才能重传.但如果发送方连续收到 3 个冗余 ACK(即同一个 ACK 数字出现 4 次),它会立即判定后续报文已丢失,在超时发生前就执行重传,极大降低了延迟&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&quot;流量控制&quot; 和 &quot;拥塞控制&quot;
&lt;ul&gt;
&lt;li&gt;这是最容易混淆的两个概念,它们共同决定了 TCP 实际上能发多快:发送速率受限于 &lt;code&gt;min(rwnd, cwnd)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;流量控制&lt;/strong&gt; (Flow Control - 保护接收方):接收方在 TCP 头部通过 &lt;code&gt;rwnd&lt;/code&gt; (Receive Window) 字段告诉发送方自己接收缓冲区还有多少可用空间,防止发送方发得太快把接收方&quot;淹没&quot;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拥塞控制&lt;/strong&gt; (Congestion Control - 保护网络):发送方自己维护一个 &lt;code&gt;cwnd&lt;/code&gt; (Congestion Window),根据探测到的网络拥堵情况(丢包)来调整发送速度&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;慢启动&lt;/strong&gt; (Slow Start):连接刚建立时,&lt;code&gt;cwnd&lt;/code&gt; 呈指数级增长,以最快速度探明网络可用带宽&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;拥塞避免&lt;/strong&gt; (AIMD):到达阈值后,转为线性增加(每 RTT 增加 1个 MSS);一旦发生丢包(如收到 3 个冗余 ACK),窗口直接减半&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;TCP CUBIC&lt;/strong&gt;:现代操作系统常用的拥塞控制算法.丢包后,它能根据三次函数曲线,更快地将发送速率恢复到丢包前的水平&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;QUIC
&lt;ul&gt;
&lt;li&gt;传统 HTTPS 需要先经历 TCP 握手,再经历 TLS(加密)握手,延迟高;且 TCP 保证严格按序到达,会产生&quot;队头阻塞(HOL blocking)&quot;. QUIC 跑在 UDP 上,在应用层实现了独立的可靠性和拥塞控制(类似 TCP NewReno),允许多个数据流多路复用,丢包只影响单个流&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h1&gt;Chapter 4 &amp;amp; 5 The Network Layer&lt;/h1&gt;
&lt;p&gt;第四章和第五章两章构成一个完整的 「网络层」框架，本书将网络层拆分成为了两个独立的章节 「数据平面」 和 「控制平面」，而这种分离正好也是现代网络框架的一个核心概念&lt;/p&gt;
&lt;h2&gt;Chapter 4 Data Plane&lt;/h2&gt;
&lt;p&gt;第四章聚焦单台路由器的局部动作，即当一个数据包达到路由器输入链路的时候，如何将其移动到正确的链路，这些动作通常都在纳秒之间完成且由硬件实现&lt;/p&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;路由器内部结构&lt;/strong&gt; (4.2)：
&lt;ul&gt;
&lt;li&gt;深入了解 输入/输出 端口的处理，内部交换结构（如内存，总线，交叉开关结构）以及排队和调度机制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IP 协议与寻址&lt;/strong&gt; (4.3):
&lt;ul&gt;
&lt;li&gt;详细讲解了 IPv4 和 IPv6 数据报的格式。我们需要重点掌握IP编址、子网划分与无类别域间路由（CIDR）,以及网络地址转换（NAT）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;通用转发与 SDN 基础&lt;/strong&gt; (4.4):
&lt;ul&gt;
&lt;li&gt;探讨了基于 OpenFlow 的“匹配加动作（Match-plus-action）”模型。这打破了传统仅基于目标IP转发的限制，是现代软件定义网络（SDN）的数据平面基础&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;Chapter 5 Control Plane&lt;/h2&gt;
&lt;p&gt;在这一章中，我们主要聚焦于网络范围内的全局逻辑，即如何协调各台路由器，计算出数据包从源主机到目标主机的端到端路径&lt;/p&gt;
&lt;h3&gt;结构&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;路由算法理论&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;我们会在这章中学习两个经典算法，需要全局网络信息的链路状态算法（LS，例如 Dijkstra 算法），以及分布式迭代的距离向量算法（DV，例如 Bellman-Ford 算法）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;互联网路由协议&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OSPF&lt;/strong&gt;: 互联网中最常用的自治系统（AS）&lt;strong&gt;内部&lt;/strong&gt;路由协议，基于链路状态算法&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BGP&lt;/strong&gt;： 治系统之间的路由协议。BGP 是将互联网中成千上万个 ISP 粘合在一起的“胶水”&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SDN 控制平面&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;绍了逻辑上集中的控制器（如 OpenDaylight 或 ONOS）如何维护全局网络状态，并通过协议（如 OpenFlow）计算并下发流表给底层的交换机/路由器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;网络运维与管理协议&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;包括用于网络诊断的 &lt;strong&gt;ICMP&lt;/strong&gt;（Ping 和 Traceroute 工具就是基于此协议实现的），以及用于设备监控与自动化配置的传统 &lt;strong&gt;SNMP&lt;/strong&gt; 和现代数据建模语言 &lt;strong&gt;NETCONF/YANG&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;hr /&gt;
&lt;h2&gt;知识点&lt;/h2&gt;
&lt;p&gt;对于两个章节来说，第四章的核心思想是解决单个路由器如何处理和转发数据包的问题，第五章的核心思想是网络范围内的逻辑协调，负责计算转发表并且管理网络设备&lt;/p&gt;
&lt;h3&gt;Data Plane&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;转发与路由的核心区别&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;转发&lt;/strong&gt;是数据平面的局部动作，发生在纳秒级，负责将到达路由器输入链路的数据包转移到正确的输出链路；&lt;strong&gt;路由&lt;/strong&gt;是控制平面的网络全局过程，发生在秒级，负责计算数据包从源到目的地的端到端路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;路由器内部架构&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;包含输入端口（执行查找与物理/链路层功能）、交换结构（如通过内存、总线或交叉开关移动数据包）、输出端口以及路由处理器&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;最长前缀匹配&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;在传统基于目的地的转发中，路由器在转发表中寻找与数据包目的IP地址匹配的最长前缀，以此决定输出接口&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IPv4 编址与 CIDR (无类别域间路由)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;理解IP地址的层次结构、子网（Subnet）的划分，以及形式如 &lt;code&gt;a.b.c.d/x&lt;/code&gt; 的网络前缀掩码配置&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DHCP (动态主机配置协议)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;设备通过即插即用的方式自动获取IP地址、子网掩码、默认网关和DNS服务器地址的机制&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NAT (网络地址转换)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;允许内部网络使用私有IP地址，通过在NAT转换表中映射IP地址和端口号，解决IPv4地址短缺问题并隐藏内部网络结构&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;广义转发与 SDN (软件定义网络) 数据平面&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;基于 OpenFlow 等协议的 &quot;匹配加动作 (Match-plus-action)&quot; 模型。数据面不仅能基于目的IP转发，还能同时匹配链路层、网络层和传输层的多个头部字段，并执行转发、丢弃（防火墙）、重写头部（NAT）或负载均衡等丰富的动作&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h3&gt;Control Plane&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;两种控制平面架构&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每路由器控制&lt;/strong&gt;：
&lt;ul&gt;
&lt;li&gt;传统网络中，每个路由器内部运行路由算法，与网络中其他路由器的路由组件相互通信来计算转发表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;逻辑集中式控制&lt;/strong&gt;:
&lt;ul&gt;
&lt;li&gt;SDN采用的方法，由一个远程的集中式控制器计算并向各个路由器（或分组交换机）下发流表&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;两大经典路由算法&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;链路状态算法 (LS, Link-State)&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;距离向量算法 (DV, Distance-Vector)&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AS 内部路由 (OSPF)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;开放最短路径优先（Open Shortest Path First），是自治系统（AS）内部广泛使用的路由协议，基于链路状态算法，支持区域划分和安全认证&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AS 之间路由 (BGP)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;边界网关协议（Border Gateway Protocol），是连接互联网中成千上万个ISP的“粘合剂”。BGP 基于 CIDR 前缀进行路由通告，其核心特点是&lt;strong&gt;高度依赖策略 (Policy)&lt;/strong&gt;（如通过 AS-PATH 和 NEXT-HOP 属性决定流量控制和商业关系），而不是单纯追求最短或最快路径&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ICMP (互联网控制报文协议)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;用于主机和路由器之间传递网络层错误信息。SRE 常用的 Ping 和 Traceroute 工具均直接基于 ICMP 实现&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>简单的端口查询Lab</title><link>https://blog.imicola.com/posts/%E7%AE%80%E5%8D%95%E7%9A%84%E7%AB%AF%E5%8F%A3%E6%9F%A5%E8%AF%A2lab/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E7%AE%80%E5%8D%95%E7%9A%84%E7%AB%AF%E5%8F%A3%E6%9F%A5%E8%AF%A2lab/</guid><description>这是Linux和go语言结合的一个实验</description><pubDate>Sat, 14 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;这是一个简单的Lab,目前用于测试新功能canvas&lt;/p&gt;
</content:encoded></item><item><title>🔒 爱国公司CloseAI食用指南</title><link>https://blog.imicola.com/posts/%E7%88%B1%E5%9B%BD%E5%85%AC%E5%8F%B8closeai%E9%A3%9F%E7%94%A8%E6%8C%87%E5%8D%97/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E7%88%B1%E5%9B%BD%E5%85%AC%E5%8F%B8closeai%E9%A3%9F%E7%94%A8%E6%8C%87%E5%8D%97/</guid><description>该文章内容已加密，请输入密码后查看。</description><pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;🔒 本文已加密保护，请访问博客网站输入密码阅读全文。&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>🔒 继续在21世纪活下去</title><link>https://blog.imicola.com/posts/%E7%BB%A7%E7%BB%AD%E5%9C%A821%E4%B8%96%E7%BA%AA%E6%B4%BB%E4%B8%8B%E5%8E%BB/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E7%BB%A7%E7%BB%AD%E5%9C%A821%E4%B8%96%E7%BA%AA%E6%B4%BB%E4%B8%8B%E5%8E%BB/</guid><description>该文章内容已加密，请输入密码后查看。</description><pubDate>Fri, 13 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;🔒 本文已加密保护，请访问博客网站输入密码阅读全文。&lt;/em&gt;&lt;/p&gt;</content:encoded></item><item><title>文件读写</title><link>https://blog.imicola.com/posts/%E6%96%87%E4%BB%B6%E8%AF%BB%E5%86%99/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E6%96%87%E4%BB%B6%E8%AF%BB%E5%86%99/</guid><description>Go语言文件读写操作详解</description><pubDate>Sat, 07 Mar 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;文件读写&lt;/h1&gt;
&lt;h2&gt;小文件&lt;/h2&gt;
&lt;p&gt;如果你能确定你读取的文件或者你写入的数据量，那么你可以使用 &lt;code&gt;os&lt;/code&gt; 提供的 &lt;code&gt;ReadFile&lt;/code&gt; 和 &lt;code&gt;WriteFile&lt;/code&gt; 函数&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 文件读入
data,err := os.ReadFile(&quot;example.txt&quot;)
if err != nil{
	log.Fatal(err)
}
fmt.Println(string(data))

// 文件写出
content := []byte(&quot;Hello Go&quot;)
err := os.WriteFile(&quot;out.txt&quot;,content,0644)
if err != nil{
	log.Fatal(err)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;逐行或按块处理&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;使用 &lt;code&gt;bufio.Scanner&lt;/code&gt; 逐行读取&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;file , err := os.Open(&quot;large_file.txt&quot;)
if err != nil{
	log.Fatal(err)
}
defer file.Close() //注意关闭文件

scanner := bufio.NewScanner(file)
for scanner.Scan(){
	string s = scanner.Text()
	// 处理
}
if err := scanner.Err(); err != nil{
	log.Fatal(err)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;文件的写入操作&lt;/h2&gt;
&lt;p&gt;如果你需要对文件进行更精细的控制（比如追加内容），需要使用 &lt;code&gt;os.OpenFile&lt;/code&gt;。&lt;/p&gt;
&lt;h3&gt;追加内容到文件&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;// O_APPEND: 追加模式, O_CREATE: 如果不存在则创建, O_WRONLY: 只写模式
file, err := os.OpenFile(&quot;log.txt&quot;, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
    log.Fatal(err)
}
defer file.Close()

if _, err := file.WriteString(&quot;New log entry\n&quot;); err != nil {
    log.Fatal(err)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;使用 &lt;code&gt;bufio.Writer&lt;/code&gt; 提高性能&lt;/h3&gt;
&lt;p&gt;在频繁进行小量写入时，使用缓冲区可以显著减少系统调用，提升性能。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;writer := bufio.NewWriter(file)
writer.WriteString(&quot;Buffered string&quot;)
writer.Flush() // 必须调用 Flush 确保数据从缓冲区写入磁盘
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>进程、前后台、服务与例行性任务</title><link>https://blog.imicola.com/posts/%E8%BF%9B%E7%A8%8B%E5%89%8D%E5%90%8E%E5%8F%B0%E6%9C%8D%E5%8A%A1%E4%B8%8E%E4%BE%8B%E8%A1%8C%E6%80%A7%E4%BB%BB%E5%8A%A1/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E8%BF%9B%E7%A8%8B%E5%89%8D%E5%90%8E%E5%8F%B0%E6%9C%8D%E5%8A%A1%E4%B8%8E%E4%BE%8B%E8%A1%8C%E6%80%A7%E4%BB%BB%E5%8A%A1/</guid><description>一些Linux的基本知识，感谢Linux101课程</description><pubDate>Sat, 21 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;进程&lt;/h1&gt;
&lt;p&gt;进程是运行中的程序的实例，是操作系统进行资源分配和调度的基本单位，当然这些更加底层的事情我们会留到操作系统这门课的时候进行讲解&lt;/p&gt;
&lt;p&gt;我们在window上可以通过任务管理器很轻松的看见运行在电脑上的进程，在linux中我们也有观察系统进程的工具&lt;/p&gt;
&lt;h2&gt;Linux进程管理器&lt;/h2&gt;
&lt;p&gt;很多人会推荐 &lt;code&gt;htop&lt;/code&gt; 这一个工具来进行进程管理，这个工具是&lt;a href=&quot;https://imicola.github.io/%E5%9F%BA%E7%A1%80%E5%85%A5%E9%97%A8/MIT-Missing-Semester/%E6%80%A7%E8%83%BD%E5%88%86%E6%9E%90#top&quot;&gt;top&lt;/a&gt;命令的改良，可以很方便的查看当前所有的进程以及系统CPU，内存占用等信息，而且其占用低&lt;/p&gt;
&lt;p&gt;我更推荐 &lt;code&gt;btop&lt;/code&gt; 工具，在功能性上其完全不输 &lt;code&gt;htop&lt;/code&gt;，只需要付出一点点的占用代价就能获得更加直观与全面的分析，同时更加美观~~(颜值即实用)~~，可以通过 &lt;code&gt;btop&lt;/code&gt; 界面上展示的快捷键快速对进程等系统资源进行操作与分析&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;s&gt;真是美美又观观啊&lt;/s&gt;&lt;/p&gt;
&lt;h2&gt;ps&lt;/h2&gt;
&lt;p&gt;ps（&lt;strong&gt;p&lt;/strong&gt;rocess &lt;strong&gt;s&lt;/strong&gt;tatus）是常用的输出进程状态的工具，直接调用 ps 仅显示本终端中运行的相关进程，如果要显示所有进程，对应命令是 &lt;code&gt;ps aux&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;一点小技巧，你可以使用 &lt;code&gt;ps aux --sort=-%cpu&lt;/code&gt;来对&lt;code&gt;ps&lt;/code&gt;的输出进行CPU占用排序，这在cpu高占用排查和编写脚本的时候会很有用&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;进程标识符&lt;/h2&gt;
&lt;p&gt;对于操作系统中的每一个进程，都有一个唯一标识来区分这些进程，这个唯一表示即 &lt;strong&gt;进程标识符PID&lt;/strong&gt;,当我们想挂起，继续，或者终止程序的时候，我们都需要使用 PID 作为索引&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]&lt;br /&gt;
当我们观察按PID进行排序的界面时候，我们很难不注意到 PID 的排序总是遵循一种规律，比如 PID1 是 &lt;code&gt;systemd&lt;/code&gt; ，PID2 是 &lt;code&gt;init-systemd&lt;/code&gt;&lt;br /&gt;
&lt;img src=&quot;Pasted%20image%2020260223134513.png&quot; alt=&quot;alt text&quot; /&gt;
这里其实涉及 PID 分配机制，其有两个特性&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;顺序性&lt;/strong&gt;: 只要计数器没达到上限，新进程的PID总是比之前的大，系统内部维护一个计数器，每创建一个进程，这个计数器就+1并且将其分配给这个进程&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;回绕&lt;/strong&gt;: Linux系统中有一个 &lt;code&gt;pid_max&lt;/code&gt;，通常是32768或更高，当计数器达到这个上限后，系统便会回到这个低位寻找已经被注销的空闲PID进行重新分配&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;有时候我们会注意到PID之间的跳跃，如从 PID8 到 PID42,然后又马上到180+&lt;/p&gt;
&lt;p&gt;这种情况有两个可能&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;瞬时进程：在系统启动的时候会启动很多的脚本或者初始化任务，它们执行完后就消失了，但是其消耗了中间的 PID 计数&lt;/li&gt;
&lt;li&gt;多线程/子进程: 在一些现代软件和&lt;s&gt;天才工程师&lt;/s&gt;的程序中，会fork出大量的子进程，这些进程会占用 PID&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;
&lt;h2&gt;进程优先级与状态&lt;/h2&gt;
&lt;p&gt;其实你知道吗？一个CPU核心一次只能执行一个进程，而现代民用CPU一般就20多个核心，而电脑中进程动辄上百个，为了让这些进程看起来是在「同时」执行的，操作系统会用非常快的速度在这些进程中来回切换，而这也就引入了进程优先级和进程状态的概念&lt;/p&gt;
&lt;h3&gt;优先级与 nice 值&lt;/h3&gt;
&lt;p&gt;对于很多的程序来说，其各个占用的资源不同，所以需要一个设定来指定程序的&lt;strong&gt;优先级&lt;/strong&gt;。在 &lt;code&gt;htop&lt;/code&gt;和&lt;code&gt;top&lt;/code&gt;中有两个与优先级有关的值 Priority(PRI) 和 nice(NI)&lt;/p&gt;
&lt;p&gt;Nice值越高代表一个进程对其他进程越 &lt;em&gt;nice&lt;/em&gt;(友好)，其对应的优先级也就越低。nice值最高为 19，最低为-20。通常来说程序的默认nice值应该都是0。我们可以通过 &lt;code&gt;htop&lt;/code&gt;，&lt;code&gt;btop&lt;/code&gt;等来调整进程的nice值&lt;/p&gt;
&lt;p&gt;用户可以使用 &lt;code&gt;nice&lt;/code&gt; 命令在运行程序时指定优先级，而 &lt;code&gt;renice&lt;/code&gt; 命令则可以重新指定优先级。当然，若想调低 nice 值，还需要 &lt;code&gt;sudo&lt;/code&gt;（毕竟不能随便就把自己的优先级设置得更高，不然对其他的用户不公平）。&lt;/p&gt;
&lt;h3&gt;进程状态&lt;/h3&gt;
&lt;p&gt;进程不是一直在系统中运行的，进程可以被简单的分为3类&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;运行态(Running): 即正在运行底层程序&lt;/li&gt;
&lt;li&gt;就绪态(Ready)： 可以运行但是正在排队等待的程序&lt;/li&gt;
&lt;li&gt;阻塞态(Blocked)： 正在等待其他资源(网络，磁盘等)而无法立刻开始运行的程序&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;而在linux中，系统对于进程的分类会更加复杂一些，下面表显示了进程状态&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;状态&lt;/th&gt;
&lt;th&gt;缩写表示&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Running&lt;/td&gt;
&lt;td&gt;R&lt;/td&gt;
&lt;td&gt;正在运行/可以立刻运行&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sleeping&lt;/td&gt;
&lt;td&gt;S&lt;/td&gt;
&lt;td&gt;可以被中断的睡眠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk Sleep&lt;/td&gt;
&lt;td&gt;D&lt;/td&gt;
&lt;td&gt;不可被中断的睡眠&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Traced / Stopped&lt;/td&gt;
&lt;td&gt;T&lt;/td&gt;
&lt;td&gt;被跟踪/被挂起的进程&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zombie&lt;/td&gt;
&lt;td&gt;Z&lt;/td&gt;
&lt;td&gt;僵尸进程&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h2&gt;用户进程控制&lt;/h2&gt;
&lt;p&gt;控制进程的前提是与进程进行通话，由于进程之间不会共享内存空间，所以就无法直接发送信息，于是就需要操作系统的链接，因此，信号机制就产生了&lt;/p&gt;
&lt;h3&gt;信号&lt;/h3&gt;
&lt;p&gt;信号是 Unix 系列系统中进程之间相互通信的一种机制。发送信号的 Linux 命令叫作 &lt;code&gt;kill&lt;/code&gt;。被称作 &quot;kill&quot; 的原因是：早期信号的作用就是关闭（杀死）进程。&lt;/p&gt;
&lt;p&gt;下面是一些常用的信号：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;信号名称&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;编号&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;默认动作&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;详细含义&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HUP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Hangup&lt;/strong&gt;:最初用于拨号终端断开。现在常用于通知守护进程&lt;strong&gt;重新加载配置文件&lt;/strong&gt;（不重启进程）。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;INT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Interrupt&lt;/strong&gt;:来自键盘的中断信号，通常是用户按下了 &lt;strong&gt;Ctrl+C&lt;/strong&gt;。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;QUIT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;终止+Core&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Quit&lt;/strong&gt;:类似 INT，但由 &lt;em&gt;Ctrl+|&lt;/em&gt; 触发，并且会生成 Core Dump 文件用于调试。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;KILL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Kill&lt;/strong&gt;：&lt;strong&gt;强制杀死进程&lt;/strong&gt;。该信号不能被捕获、阻塞或忽略，进程无法在退出前进行清理。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TERM&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;终止&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Terminate&lt;/strong&gt;:&lt;strong&gt;优雅退出&lt;/strong&gt;。这是 &lt;code&gt;kill&lt;/code&gt; 命令默认发送的信号，它告诉进程“请你保存数据并关闭”，进程可以捕获并处理。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TSTP&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;暂停&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Terminal stop&lt;/strong&gt;:用户通过 &lt;strong&gt;Ctrl+Z&lt;/strong&gt; 发送的暂停信号，可以被捕获。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CONT&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;18&lt;/td&gt;
&lt;td&gt;继续&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Continue&lt;/strong&gt;。让被暂停(STOP/TSTP)的进程恢复运行。由 &lt;code&gt;fg&lt;/code&gt; 或 &lt;code&gt;bg&lt;/code&gt;发出&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;前后台切换&lt;/h3&gt;
&lt;p&gt;一般来说，我们创建的进程默认都是跑在前台，如果想要在程序运行的时候还可以做其他事情而不是看着程序跑，那我们就需要将程序放到后台来跑&lt;/p&gt;
&lt;p&gt;在shell中输入命令的时候，我们可以在命令结尾添加&lt;code&gt;&amp;amp;&lt;/code&gt;来告诉命令在后台运行&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;10:42:03 with imicola in ~ is 📦 None …
➜ ./test &amp;amp;
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;p&gt;如果想让前台的程序切换到后台，我们需要按下Ctrl + Z发送 &lt;code&gt;SIGTSTP&lt;/code&gt; 让进程挂起，再输入 &lt;code&gt;jobs&lt;/code&gt; 命令，就可以看到被挂起的程序以及其代号&lt;/p&gt;
&lt;h2&gt;终止进程&lt;/h2&gt;
&lt;h3&gt;终止前端进程&lt;/h3&gt;
&lt;p&gt;对于运行在前端的程序，我们只需要在终端界面按下 &lt;code&gt;Ctrl + c&lt;/code&gt; 就会向程序发送 &lt;code&gt;SIGINT&lt;/code&gt; 信号告诉程序需要退出&lt;/p&gt;
&lt;h3&gt;xtop中发送信号&lt;/h3&gt;
&lt;p&gt;在所有类top命令中，一般都会有发送信号的方式，比如 &lt;code&gt;top&lt;/code&gt; 和 &lt;code&gt;htop&lt;/code&gt; 命令中，我们通过按下 &lt;code&gt;k&lt;/code&gt; 键来向进程发送信号，在 &lt;code&gt;btop&lt;/code&gt; 命令中，&lt;code&gt;kill&lt;/code&gt;键将对选择的进程发送 &lt;code&gt;SIGTERM&lt;/code&gt; 退出信号，&lt;code&gt;s&lt;/code&gt;可以选择发送的信号&lt;/p&gt;
&lt;h3&gt;kill&lt;/h3&gt;
&lt;p&gt;如前所述，Linux 上最常用的发送信号的程序就是 kill&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[imicola@LAPTOP-1R3FN2QL ~]$ kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 2) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
3) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
4) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
5) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
6) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
7) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
8) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
9) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
10) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
11) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
12) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
13) SIGRTMAX-1  64) SIGRTMAX
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Tip]&lt;br /&gt;
不同shell的kill命令略有不同，如zsh中&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;11:49:27 with imicola in ~ is 📦 None …
➜ kill -l
HUP INT QUIT ILL TRAP IOT BUS FPE KILL USR1 SEGV USR2 PIPE ALRM TERM STKFLT CHLD CONT STOP TSTP TTIN TTOU URG XCPU XFSZ VTALRM PROF WINCH POLL PWR SYS
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;p&gt;如果不加任何参数，只有 PID，&lt;code&gt;kill&lt;/code&gt; 命令将自动使用 15（SIGTERM）作为信号参数。&lt;/p&gt;
&lt;hr /&gt;
&lt;h2&gt;脱离终端&lt;/h2&gt;
&lt;p&gt;如果你之前使用过 SSH 连接到远程服务器执行任务，那么你会发现，在shell中执行的任务会在SSH断开后被关闭，因为SSH断联时会向进程发送 SIGHUB 信号&lt;/p&gt;
&lt;h3&gt;nohub&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;nohub&lt;/code&gt;,字面含义，就是 「不要被SIGHUB影响」&lt;/p&gt;
&lt;p&gt;在命令之前输入 nohub 即可,运行时候的输出会被重定向到 nohub.out中，也可以通过重定向自定义输出文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ nohub ping localhost
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;tmux&lt;/h3&gt;
&lt;p&gt;在之前的 [[命令行环境#终端多路复用|终端多路复用]] 中我们就有稍微了解过一些 tmux，我们其实可以将 &lt;code&gt;tmux&lt;/code&gt; 看作一个脱离目前终端但是前端在终端上的终端模拟器，其解决了会话保持和窗口复用的问题，在这里我们不过多赘述&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;服务&lt;/h1&gt;
&lt;p&gt;在操作系统（OS）的语境下，「&lt;strong&gt;服务&lt;/strong&gt;」（Service）通常指那些在后台运行、不直接与用户交互，但为系统或其他应用程序提供特定功能的程序或进程。&lt;/p&gt;
&lt;h2&gt;守护进程&lt;/h2&gt;
&lt;p&gt;当进程启动时脱离终端会话，避免因为终端的关闭而消失，这类一直默默工作与后台的进程被称为 &lt;strong&gt;守护进程&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;服务管理&lt;/h3&gt;
&lt;p&gt;目前绝大多数Linux发行版的init方案都是 systemd，其管理系统服务的命令是 &lt;code&gt;systemctl&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;在管理服务前，我们需要清楚有什么服务，使用 &lt;code&gt;systemctl status&lt;/code&gt;可以观察系统服务运行状态&lt;/p&gt;
&lt;p&gt;对于服务的启动，终止，重载命令等，可以参考下面的说明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;$ tldr systemctl
systemctl
Control the systemd system and service manager.

    - List failed units:  # 列出运行失败的服务
    systemctl --failed

    - Start/Stop/Restart/Reload a service:  # 开启/关闭/重启/重载服务。Reload 代表重载配置文件而不重启进程。
    systemctl start/stop/restart/reload {{unit}}

    - Show the status of a unit:  # 显示服务状态
    systemctl status {{unit}}

    - Enable/Disable a unit to be started on bootup:  # 设置（Enable）/取消（Disable）服务开机自启
    systemctl enable/disable {{unit}}

    - Mask/Unmask a unit, prevent it to be started on bootup:  # 阻止/取消阻止服务被 enable
    systemctl mask/unmask {{unit}}

    - Reload systemd, scanning for new or changed units:  # 重载 systemd，需要在创建或修改服务文件后执行
    systemctl daemon-reload
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h3&gt;自定义服务&lt;/h3&gt;
&lt;p&gt;如果我想将一个基于web的应用作为局域网的web服务以便在其他设备上访问，那我们应该如何将其注册为 systemd服务呢？&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]&lt;br /&gt;
要想实现服务的注册，我们可以通过编写 &lt;code&gt;.service&lt;/code&gt; 文件来实现，下面以 &lt;code&gt;Jupyter Notebook&lt;/code&gt; 来示例&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[Unit]
Description=Jupyter Notebook    # 该服务的简要描述

[Service]
PIDFile=/run/jupyter.pid        # 用来存放 PID 的文件
ExecStart=/usr/local/bin/jupyter-notebook --allow-root
                                # 使用绝对路径标明的命令及命令行参数
WorkingDirectory=/root          # 服务启动时的工作目录
Restart=always                  # 重启模式，这里是无论因何退出都重启
RestartSec=10                   # 退出后多少秒重启

[Install]
WantedBy=multi-user.target      # 依赖目标，这里指进入多用户模式后再启动该服务
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，将写好的配置文件放入 &lt;code&gt;/etc/systemd/system/jupyter.service&lt;/code&gt;中，运行 &lt;code&gt;systemctl daemon-reload&lt;/code&gt;，就可以使用 systemctl 来管理这个服务了&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h2&gt;例行性任务&lt;/h2&gt;
&lt;p&gt;所谓的例行性任务，指的是基于时间的一次或者多次的周期性任务，在linux中实现定时任务工作的程序主要有 [[at]] 和 [[crontab]]&lt;/p&gt;
</content:encoded></item><item><title>目录访问与文件操作</title><link>https://blog.imicola.com/posts/%E7%9B%AE%E5%BD%95%E8%AE%BF%E9%97%AE%E4%B8%8E%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E7%9B%AE%E5%BD%95%E8%AE%BF%E9%97%AE%E4%B8%8E%E6%96%87%E4%BB%B6%E6%93%8D%E4%BD%9C/</guid><description>一些Linux的基本知识，感谢Linux101课程</description><pubDate>Mon, 09 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;操作文件与目录&lt;/h1&gt;
&lt;p&gt;在 linux 中进行操作文件与目录是使用 Linux 的最基础的一个功能，在linux中不同于Windows和macOS下在图形界面简单拖拽文件就能完成文件移动，在linux中我们更加倾向于在命令行中完成操作&lt;/p&gt;
&lt;h2&gt;查看当前命令行目录&lt;/h2&gt;
&lt;p&gt;要想进行文件管理和操作，我们得先知道我们在哪？一般而言，shell环境会显示我们当前的位置，这点我们在 [[Shell#在shell中导航|在shell中导航]] 这一块中有提到，同时，使用&lt;code&gt;pwd&lt;/code&gt;命令也可以获取我们目前shell所在的目录&lt;/p&gt;
&lt;h2&gt;查看文件夹内容&lt;/h2&gt;
&lt;p&gt;我们主要通过 &lt;code&gt;ls&lt;/code&gt; 命令来查看当前文件夹下的内容，ls默认会隐藏目录下  &lt;code&gt;.&lt;/code&gt; 开头的文件和文件夹，如果需要将其全部输出可以使用 &lt;code&gt;ls -a&lt;/code&gt; 来查看所有文件&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有一些项目里还会提供树状图结构的目录，这其实可以通过&lt;code&gt;tree&lt;/code&gt;命令生成，在使用前需要安装&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]&lt;br /&gt;
&lt;a href=&quot;https://github.com/eza-community/eza&quot;&gt;eza&lt;/a&gt;是&lt;code&gt;ls&lt;/code&gt;的现代化代替品，输出会更加好看&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;sudo pacman -S eza
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;eza&lt;/code&gt;几乎可以代替 &lt;code&gt;ls&lt;/code&gt; 作为文件目录输出索引，传入的参数和 &lt;code&gt;ls&lt;/code&gt; 也几乎一致，故我们可以用&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;alias ls=eza
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;来直接将 &lt;code&gt;ls&lt;/code&gt; 替换为 &lt;code&gt;eza&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;Pasted%20image%2020260209180009.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h1&gt;查看文件内容&lt;/h1&gt;
&lt;h2&gt;cat&lt;/h2&gt;
&lt;p&gt;当我们需要在终端中查看一个文件的内容的时候我们只需要 &lt;code&gt;cat &amp;lt;文件名&amp;gt;&lt;/code&gt; 即可&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]
为什么叫cat&lt;br /&gt;
当然和猫咪没有关系，cat 这里是 con&lt;strong&gt;cat&lt;/strong&gt;enate（连接）的缩写，因为 cat 工具实际的功能是连接多个文件，然后输出。但是当只有一个文件的时候，cat 就会直接输出这个文件，所以 cat 最常见的用途就是输出单个文件。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;less&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;less&lt;/code&gt; 和 &lt;code&gt;cat&lt;/code&gt; 的区别在于， &lt;code&gt;cat&lt;/code&gt; 会一次性将所有内容打印到终端中并退出，而less一次只显示一页并且支持向前后滚动和搜索功能，通常情况下&lt;code&gt;less&lt;/code&gt;会比&lt;code&gt;cat&lt;/code&gt;方便许多&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上在Linux还有 &lt;code&gt;more&lt;/code&gt; 和 &lt;code&gt;most&lt;/code&gt; 两个和 &lt;code&gt;less&lt;/code&gt; 类似的指令，但是流行程度不如 &lt;code&gt;less&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;编辑文件内容&lt;/h1&gt;
&lt;p&gt;这部分内容可见[[编辑器(vim)]]&lt;/p&gt;
&lt;h1&gt;复制剪切文件和目录&lt;/h1&gt;
&lt;p&gt;这部分常用的指令是[[cp]]和[[mv]]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;cp&lt;/code&gt; 命令用于对文件进行复制&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将 源文件 复制到当前目录下，其名称为后一个参数
cp [OPT] &amp;lt;源文件名称&amp;gt; &amp;lt;新文件名称&amp;gt;
# 将 SOURCE 文件拷贝到 DIRECTORY 目录下，SOURCE 可以为不止一个文件 
cp [OPTION] SOURCE... DIRECTORY
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;mv&lt;/code&gt;的使用方法与&lt;code&gt;cp&lt;/code&gt;类似，效果类似Windows下的剪切&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# 将 源文件 剪切到当前目录下，其名称为后一个参数(其实就是重命名功能)
mv [OPT] &amp;lt;源文件名称&amp;gt; &amp;lt;新文件名称&amp;gt;
# 将 SOURCE 文件拷贝到 DIRECTORY 目录下，SOURCE 可以为不止一个文件 
mv [OPTION] SOURCE... DIRECTORY
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
&lt;h1&gt;删除文件&lt;/h1&gt;
&lt;blockquote&gt;
&lt;p&gt;[!danger]&lt;br /&gt;
当你在终端使用 &lt;code&gt;rm&lt;/code&gt; 命令时候，这个操作将是不可逆的，你将会永远失去你删除的文件，特别是在 &lt;code&gt;root&lt;/code&gt; 权限和使用 &lt;code&gt;-r&lt;/code&gt; 参数的时候&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;使用命令 &lt;code&gt;rm&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm [OPT] &amp;lt;file&amp;gt;
# 删除文件file1.txt
rm file.txt
# 删除test目录下所有文件
rm -r test/
# 尝试强制删除 test1/、test2/、file1.txt 文件和目录。这些文件或者目录可能不存在、有写保护或者没有权限读写
rm -rf test1/ test2/ file1.txt
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;除非你的计算机存在特殊备份措施或恢复措施，否则 &lt;code&gt;rm&lt;/code&gt; 进行的删除命令是无法还原的&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!Warning]&lt;br /&gt;
在使用 &lt;code&gt;rm&lt;/code&gt; 删除的时候，请务必注意目录拼写&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;rm -rf /home/imicola/f # 删除f
rm -rf / home/imicola/f # 删除根目录下的所有内容和 home/imicola/f 及其中的文件
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]&lt;/p&gt;
&lt;p&gt;&lt;code&gt;rm&lt;/code&gt; 指令之所以危险是因为其是不可逆的，我们可以使用更加现代化且安全的 &lt;code&gt;trash-cli&lt;/code&gt; 来代替 &lt;code&gt;rm&lt;/code&gt; 指令。这和在 Windows 下删除文件进入回收站的逻辑是一致的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;h1&gt;创建文件&lt;/h1&gt;
&lt;p&gt;使用 &lt;code&gt;touch&lt;/code&gt; 命令创建文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;touch file1 file2
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;touch命令的时间功能是修改文件的访问时间和修改时间，类似于摸了一下文件，使其访问和修改时间发生了变化&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1&gt;搜索文件和目录&lt;/h1&gt;
&lt;pre&gt;&lt;code&gt;# 在 PATH路径中根据 表达式 搜索文件
find [opt] PATH [表达式]
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]&lt;br /&gt;
在当前目录中搜索名为 report.pdf 的wenj&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find . -name &apos;report.pdf&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;全盘搜索大于 1G 的文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;find / -size +1G
&lt;/code&gt;&lt;/pre&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]&lt;br /&gt;
对于文件搜索，我们同样有更好用的代替，如果是对于搜索文件，可以尝试 &lt;code&gt;fzf&lt;/code&gt; 进行交互式模糊搜索，如果是希望有格式有条件的筛选，可以选择 &lt;code&gt;fd&lt;/code&gt;，更多有关这部分的内容可以看[[文件查询]]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;统计文件或文件夹大小&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;du&lt;/code&gt; 命令可以实现文件和文件夹大小的统计，一般而言我们可以直接使用 &lt;code&gt;du -sh&lt;/code&gt; 来显示目标文件的总大小，用 &lt;code&gt;du -h -d 1&lt;/code&gt; 显示当前目录下的总大小&lt;/p&gt;
&lt;p&gt;此外，使用 &lt;code&gt;ncdu&lt;/code&gt; 可以用交互式的方式来显示文件和目录的大小，并会按文件大小排序&lt;/p&gt;
&lt;hr /&gt;
&lt;h1&gt;模式匹配&lt;/h1&gt;
&lt;h2&gt;通配&lt;/h2&gt;
&lt;p&gt;shell的通配字符主要应用在&lt;strong&gt;文件路径的展开上&lt;/strong&gt;,需要注意的是,通配不是[[正则表达式]],而是更简单的快速选择文件的符号&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt; 匹配一个字符
&lt;ul&gt;
&lt;li&gt;如 &lt;code&gt;file?.txt&lt;/code&gt;匹配&lt;code&gt;file1.txt&lt;/code&gt;,&lt;code&gt;fileA.txt&lt;/code&gt;等&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*&lt;/code&gt; 匹配任意字符
&lt;ul&gt;
&lt;li&gt;如 &lt;code&gt;*.py&lt;/code&gt; 匹配所有 &lt;code&gt;.py&lt;/code&gt; 结尾的文件&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;[]&lt;/code&gt; 匹配方括号中的任意一个字符
&lt;ul&gt;
&lt;li&gt;如 &lt;code&gt;[abc].txt&lt;/code&gt; 匹配 &lt;code&gt;a.txt,b.txt,c.txt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;{}&lt;/code&gt; 列表生成,即&lt;strong&gt;扩展&lt;/strong&gt;为逗号分隔的字符串列表
&lt;ul&gt;
&lt;li&gt;如 &lt;code&gt;{test1,test2}.txt&lt;/code&gt; 会被扩展成 &lt;code&gt;test1.txt test2.txt&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Go语言下的pprof</title><link>https://blog.imicola.com/posts/go%E8%AF%AD%E8%A8%80%E4%B8%8B%E7%9A%84pprof/</link><guid isPermaLink="true">https://blog.imicola.com/posts/go%E8%AF%AD%E8%A8%80%E4%B8%8B%E7%9A%84pprof/</guid><description>性能分析在Go下是有必要谈及的</description><pubDate>Fri, 06 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;对于go语言的pprf分析我们有两种情况，一个是针对长期开启的服务进行探测，还有一种是针对脚本进行分析&lt;/p&gt;
&lt;h2&gt;针对服务型&lt;/h2&gt;
&lt;p&gt;我们可以在代码中使用 &lt;code&gt;net/http/pprof&lt;/code&gt; 包。你只需要在代码中匿名引入，它会自动在 &lt;code&gt;8080&lt;/code&gt;（或其他你定义的）端口开启一个 &lt;code&gt;/debug/pprof&lt;/code&gt; 路由。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 在main函数中启动
go func() {
   http.ListenAndServe(&quot;localhost:6060&quot;, nil)
    }()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样go会在&lt;code&gt;6060&lt;/code&gt;端口下打开一个网页(&lt;s&gt;虽然很简陋&lt;/s&gt;)，访问&lt;code&gt;http://localhost:6060/debug/pprof/&lt;/code&gt;就可以看到这样一个的分析&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;image-1.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;h2&gt;脚本&lt;/h2&gt;
&lt;p&gt;对于脚本和一些简单go程序，会运行一段时间直接退出，这个时候就不适合打开端口监控了，我们可以直接在脚本中插入埋点&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import (
    &quot;os&quot;
    &quot;runtime/pprof&quot;
    &quot;runtime&quot;
    &quot;log&quot;
)

func main() {
    // --- CPU Profile ---
    cpuFile, err := os.Create(&quot;cpu.prof&quot;)
    if err != nil {
        log.Fatal(err)
    }
    pprof.StartCPUProfile(cpuFile)
    defer pprof.StopCPUProfile()

    // 运行你的脚本逻辑
    runScriptLogic()

    // --- Memory Profile ---
    memFile, err := os.Create(&quot;mem.prof&quot;)
    if err != nil {
        log.Fatal(err)
    }
    defer memFile.Close()
    
    // 显式运行一次 GC，确保内存数据准确
    runtime.GC() 
    if err := pprof.WriteHeapProfile(memFile); err != nil {
        log.Fatal(err)
    }
}

func runScriptLogic() {
    // 模拟耗时或内存密集操作
    sum := 0
    for i := 0; i &amp;lt; 1000000; i++ {
        sum += i
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个时候我们运行结束后会发现目录下多了&lt;code&gt;cpu.prof&lt;/code&gt; 和 &lt;code&gt;mem.prof&lt;/code&gt; 文件，我们使用pprf就可以进行分析：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go tool pprof cpu.prof
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;更推荐的测试方法&lt;/h3&gt;
&lt;p&gt;如果我们需要测试的内容可以封装为一个函数，那么使用 Go 的测试工具链进行性能分析是最简单、最专业的做法， 不需要在业务代码里写任何 &lt;code&gt;pprof&lt;/code&gt; 代码&lt;/p&gt;
&lt;p&gt;我们需要编写一个benchmark文件&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;package main

import &quot;testing&quot;

func BenchmarkMyTask(b *testing.B) {
    for i := 0; i &amp;lt; b.N; i++ {
        runScriptLogic() // 调用你要分析的函数
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后通过 &lt;code&gt;test&lt;/code&gt; 工具链对整个程序进行测试并生成分析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go test -bench . -cpuprofile cpu.prof -memprofile mem.prof
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后通过pprf进行分析&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go tool pprof -http=:8080 cpu.prof // 在8080端口打开网页，需要安装Graphviz
&lt;/code&gt;&lt;/pre&gt;
</content:encoded></item><item><title>Go语言语法</title><link>https://blog.imicola.com/posts/go%E8%AF%AD%E8%A8%80%E8%AF%AD%E6%B3%95/</link><guid isPermaLink="true">https://blog.imicola.com/posts/go%E8%AF%AD%E8%A8%80%E8%AF%AD%E6%B3%95/</guid><description>Go语言语法介绍</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;语言特性&lt;/h1&gt;
&lt;p&gt;&lt;em&gt;less can be more&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;这是go语言的核心思想&lt;/p&gt;
&lt;p&gt;在go语言中，没有继承，多态，甚至是类&lt;/p&gt;
&lt;p&gt;在go的文档中说Go语言几乎和C/C++一样快&lt;s&gt;实则不然&lt;/s&gt;&lt;/p&gt;
&lt;h1&gt;语法&lt;/h1&gt;
&lt;h2&gt;变量定义&lt;/h2&gt;
&lt;p&gt;在go语言中，主要采用类型后置的命名方式：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var age int = 25 \\ 经典定义方式
var age = 25 \\ 自动推断，类似C++中 auto
age := 25 \\ 简短定义，等效上一条
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在Golang中支持批量的定义&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;var(
	a string
	b int
	c bool
	d float32
)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;我们需要注意Golang的命名规则，&lt;strong&gt;即首字母大写的命名在包外是可以访问的&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;输入输出流(CLI下)&lt;/h2&gt;
&lt;p&gt;在go语言中，我们有两种对输入输出流进行控制的语法，分别是 &lt;code&gt;fmt&lt;/code&gt; 包和 &lt;code&gt;bufio&lt;/code&gt; 包&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[!tip]&lt;br /&gt;
这两个包没有绝对的谁好谁坏的区别，对于&lt;code&gt;fmt&lt;/code&gt;包而言，其适合格式化,对于 &lt;code&gt;bufio&lt;/code&gt; 来说则更加侧重于性能&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;&lt;code&gt;fmt&lt;/code&gt;包的标准输入输出：&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;import (
	&quot;fmt&quot;
)

func main() {
	a := 114
	// 标准输出
	fmt.Print(&quot;hello go&quot;)       // 普通的打印
	fmt.Println(&quot;hello Gooooo&quot;) // 等效于在Print中加\n
	fmt.Printf(&quot;%d&quot;, a)         //  类似于C的printf
	// 标准输入
	fmt.Scan(&amp;amp;a) // 从标准输入中读取，注意需要传入地址
	var str string
	fmt.Scanln(&amp;amp;str)
	fmt.Println(a)
	fmt.Println(str)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;&lt;code&gt;bufio&lt;/code&gt;的输入输出&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func f2() {
	// 标准读入
	reader := bufio.NewReader(os.Stdin) // 创建一个输入流,类似于Java
	var x int
	var str string
	fmt.Fscan(reader, &amp;amp;x) // 搭配Fscan进行读入
	fmt.Fscan(reader, &amp;amp;str)
	// 标准输出
	writer := bufio.NewWriter(os.Stdout) // 创建一个输出流
	fmt.Fprintln(writer, x)
	fmt.Fprintln(writer, str)
	writer.Flush() // 必须要刷新缓冲区否则可能不会显示输出
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;基本数据类型&lt;/h2&gt;
&lt;p&gt;Golang的语言更加明确的数字类型命名&lt;/p&gt;
&lt;p&gt;在go语言中，有以下几种基本的数据类型:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;整型&lt;/li&gt;
&lt;li&gt;浮点型&lt;/li&gt;
&lt;li&gt;复数&lt;/li&gt;
&lt;li&gt;布尔&lt;/li&gt;
&lt;li&gt;字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;整型&lt;/h3&gt;
&lt;p&gt;在go语言中整型可以分为两大类,其后面的数字表示了其占用的二进制位&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;无符号&lt;/th&gt;
&lt;th&gt;有符号&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uint8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int8&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uint16&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int16&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uint32&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int32&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;uint64&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;int64&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3&gt;浮点型&lt;/h3&gt;
&lt;p&gt;go语言支持两种浮点型，&lt;code&gt;float32&lt;/code&gt; 和 &lt;code&gt;float64&lt;/code&gt;，不存在&lt;code&gt;double&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;对于&lt;code&gt;float32&lt;/code&gt;其范围大概为$3.4 \times 10^{38}$，&lt;code&gt;float64&lt;/code&gt;的范围大约为$1.8 \times 10^{308}$,两个浮点数都有常量&lt;code&gt;Math.MaxFloat32 &amp;amp;&amp;amp; Math.MaxFloat64&lt;/code&gt;&lt;/p&gt;
&lt;h3&gt;复数&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;complex32&lt;/code&gt; 和 &lt;code&gt;complex64&lt;/code&gt;&lt;br /&gt;
其中后缀数字表示实部和虚部的位数&lt;/p&gt;
&lt;h3&gt;布尔&lt;/h3&gt;
&lt;p&gt;在go语言中我们用 &lt;code&gt;bool&lt;/code&gt; 对布尔值进行声明&lt;/p&gt;
&lt;p&gt;在Go语言中，没有1/0对布尔值的隐式转换，在建立布尔值的时候只能使用&lt;code&gt;true&lt;/code&gt;或者&lt;code&gt;false&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;布尔型无法参与数值运算，也无法与其他类型进行转换。&lt;/p&gt;
&lt;h3&gt;字符串&lt;/h3&gt;
&lt;p&gt;在go语言中，使用字符串和使用原生数据类型是一样的。在Golang的字符串内部使用的是UTF-8编码，所以我们可以直接使用非ASCII字符&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 := &quot;hello&quot;
s2 := &quot;你好&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于字符串而言有许多和其他语言一致的&lt;code&gt;\&lt;/code&gt;转译字符&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;转义&lt;/th&gt;
&lt;th&gt;含义&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;\r&lt;/td&gt;
&lt;td&gt;回车符（返回行首）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\n&lt;/td&gt;
&lt;td&gt;换行符（直接跳到下一行的同列位置）&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;\t&lt;/td&gt;
&lt;td&gt;制表符&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&apos;&lt;/td&gt;
&lt;td&gt;单引号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&quot;&lt;/td&gt;
&lt;td&gt;双引号&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;|&lt;/td&gt;
&lt;td&gt;反斜杠&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;在go语言中也内置了和python一样好用的多行字符串&lt;/p&gt;
&lt;p&gt;要实现一个多行字符串时，我们需要使用 &lt;code&gt;反引号&lt;/code&gt; 字符 `&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s1 := `第一行
第二行
第三行
`
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;反引号中的换行被作为字符串中的换行，所有的转义符号均无效，文本会原样输出&lt;/p&gt;
&lt;h4&gt;字符串常用操作&lt;/h4&gt;
&lt;p&gt;Go语言的字符串是一个不可变字节序列，本身并不是一个类，但是Go提供了一些函数来处理字符串&lt;/p&gt;
&lt;p&gt;查找与包含类：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;功能&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;方法&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;说明&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;是否包含&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strings.Contains(s, substr)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回 &lt;code&gt;bool&lt;/code&gt;，判断 &lt;code&gt;substr&lt;/code&gt; 是否在 &lt;code&gt;s&lt;/code&gt; 中&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;查找位置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strings.Index(s, substr)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回子串第一次出现的索引，不存在返回 &lt;code&gt;-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;最后位置&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strings.LastIndex(s, substr)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;返回子串最后一次出现的索引&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;前缀判断&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strings.HasPrefix(s, prefix)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断是否以某字符串开头&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;后缀判断&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strings.HasSuffix(s, suffix)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;判断是否以某字符串结尾&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;统计次数&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;strings.Count(s, substr)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;计算子串在字符串中出现的次数&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;分割与合并：&lt;/p&gt;
&lt;p&gt;在处理CSV或路径的时候会十分有用，主要有两个函数：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;分割 &lt;code&gt;Split&lt;/code&gt; : 将字符串切分成片&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;s := &quot;abc,edf,ghi&quot;
item := strings.Split(s, &quot;,&quot;)
fmt.Println(item)
//output: [abc edf ghi]
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;合并: 将切片连接成字符串&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sil := []string{&quot;abc&quot;,&quot;def&quot;,&quot;ghi&quot;}
s2 := strings.Join(sil, &quot;-&quot;)
fmt.Println(s2)
//output: abc-def-ghi
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;字符&lt;/h4&gt;
&lt;p&gt;组成每个字符串的元素叫做“字符”，可以通过遍历或者单个获取字符串元素获得字符。 字符用单引号（’）包裹起来&lt;/p&gt;
&lt;p&gt;在string中有两种字符类型：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;uint8&lt;/code&gt;类型，或者叫 byte 型，代表了ASCII码的一个字符。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;rune&lt;/code&gt;类型，代表一个 UTF-8字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;实际上，&lt;code&gt;rune&lt;/code&gt; 是一个int32&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4&gt;字符串的修改&lt;/h4&gt;
&lt;p&gt;因为go的字符串是一个不可变字符组，所以当我们需要修改字符串的时候需要我们将字符串变为&lt;code&gt;rune[]&lt;/code&gt;或者&lt;code&gt;byte[]&lt;/code&gt;，然后在最后的时候转化回去&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func changeString() {
	s1 := &quot;hello&quot;
	// 强制类型转换
	byteS1 := []byte(s1)
	byteS1[0] = &apos;H&apos;
	fmt.Println(string(byteS1))

	s2 := &quot;你好世界&quot;
	runeS2 := []rune(s2)
	runeS2[0] = &apos;不&apos;
	fmt.Println(string(runeS2))
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;但需要注意的是，这种转化会带来内存上的分配和拷贝问题，会对性能有一些开销问题&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;类型转换&lt;/h3&gt;
&lt;p&gt;在Go语言中，只有强制类型转化，没有隐式类型转化，其语法为 &lt;code&gt;T(表达式)&lt;/code&gt;，其中T表示要转化的类型&lt;/p&gt;
&lt;h2&gt;流程语句&lt;/h2&gt;
&lt;p&gt;在go语言中，流程语句的书写习惯会更加偏向Python，比如&lt;code&gt;if&lt;/code&gt;语句无需括号扩起，&lt;code&gt;for range&lt;/code&gt;语法等&lt;/p&gt;
&lt;h3&gt;if语句&lt;/h3&gt;
&lt;p&gt;在Go语言中if语句有这些特点:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可省略条件表达式括号。&lt;/li&gt;
&lt;li&gt;持初始化语句，可定义代码块局部变量。&lt;/li&gt;
&lt;li&gt;代码块左括号必须在条件表达式尾部。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func f7() {
	a := &quot;喵&quot;
	if  b := &quot;喵&quot;; a == b {
		fmt.Println(&quot;喵喵&quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;循环&lt;/h3&gt;
&lt;p&gt;在Go语言中只有&lt;code&gt;for&lt;/code&gt;循环，对于&lt;code&gt;for&lt;/code&gt;循环有几种变体：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f8() {
	s1 := []int{1, 2, 3, 4}
	// 传统意义下的循环
	for i := 0; i &amp;lt; 4; i++ {
		fmt.Print(s1[i])
		fmt.Print(&quot; &quot;)
	}
	// 代替while循环
	n := 4
	for n &amp;gt; 0 {
		n--
		fmt.Print(s1[n])
		fmt.Print(&quot; &quot;)
	}
	// range 循环
	for idx, v := range s1 {
		fmt.Println(v)
		/*
			range会遍历指定的切片
			需要注意的是，这里的v是在循环第一步就被拷贝了的
			所以对这个v进行修改不会改变s1中的元素
			如果需要修改可以使用idx进行修改
		*/
		if idx == 2 {
			s1[idx] = 1
		}
	}
	// range 次数循环
	for range 4 {
		fmt.Println(&quot;喵&quot;)
	}
	p := 5
	for range p*2 - 1 {
		fmt.Println(&quot;喵&quot;)
	}
	// 死循环
	for {
		fmt.Println(&quot;喵&quot;)
	}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;数组与切片&lt;/h1&gt;
&lt;p&gt;在Go中，数组类似和传统意义上的数组一致，是一种定死底层内存的数据结构，对于切片而言，其可以说是一种可扩展容量的数组&lt;/p&gt;
&lt;h3&gt;数组&lt;/h3&gt;
&lt;p&gt;数组是具有固定长度且编号连续的数据序列，具有以下特点&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;固定长度&lt;/strong&gt;:在声明的时候必须指定长度且不可更改&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;值类型&lt;/strong&gt;：在 Go 中，数组是值类型。如果你把一个数组赋值给另一个数组，它是完全拷贝一份数据。&lt;/li&gt;
&lt;li&gt;注意长度也是构成数组类型的一部分，&lt;code&gt;[3]int&lt;/code&gt; 和 &lt;code&gt;[4]int&lt;/code&gt; 是两种完全不同的类型&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;切片&lt;/h3&gt;
&lt;p&gt;在Go语言中，有一种和C++中vector类似的数据结构，切片的声明如下:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;s := []int{0}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;切片是对一个&lt;strong&gt;连续片段的引用&lt;/strong&gt;，其本身并不储存任何数据，本质是一个很小的结构体，包含三个字段:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;指针：指向底层数组中切片开始的位置&lt;/li&gt;
&lt;li&gt;长度：切片当前包含的元素的个数&lt;/li&gt;
&lt;li&gt;容量：从切片起始位置到底层数组末尾的元素个数&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;注意切片是引用的，当我们将一个切片赋值给另外一个变量的时候我们实际上传入的是引用，所以当我们修改其中一个的时候，另外一个也会变&lt;/p&gt;
&lt;h4&gt;切片的初始化&lt;/h4&gt;
&lt;p&gt;原始的切片声明没办法实现复杂的初始化，我们可以用&lt;code&gt;make&lt;/code&gt;语法对切片进行初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f5() {
	n := 3
	s1 := make([]int, n) 
	for idx := range s1 {
		s1[idx] = idx + 1
	}
	fmt.Println(s1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们利用make函数还可以进行类似C++中预先分配内存的操作&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f6() {
	n := 3
	s1 := make([]int, 0, 2*n)
	idx := 1
	for range n * 2 {
		s1 = append(s1, idx)
		idx++
	}
	fmt.Println(s1)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h4&gt;切片的复制&lt;/h4&gt;
&lt;p&gt;正如我们前面所说，如果我们直接将一个切片传入另外一个切片的时候，直接赋值的引用，要实现切片的赋值可以参考下面的两个语法&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f4() {
	s1 := []int{1, 2, 3}
	// 使用append在一个空的数组后直接追加之前的元素
	s2 := append([]int{}, s1...)
	// 使用copy函数
	s3 := make([]int, len(s1)) // 注意这里s3的长度需要大于等于s1
	copy(s3, s1)
	s3[0] = 2
	s2[0] = 114
	fmt.Println(s1, s2, s3)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;map&lt;/h1&gt;
&lt;p&gt;Go语言的map通过make语法声明：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mp := make(map[int]string)
mp[5] = &quot;喵&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们需要注意的是map中的键并非有序的，这个map更类似C++中的unordered_map&lt;/p&gt;
&lt;p&gt;在go语言中，map的声明格式为 &lt;code&gt;map[KeyType]ValType&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;我们推荐使用&lt;code&gt;make&lt;/code&gt;语法初始化&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;m := make(map[string]int)
m[&quot;age&quot;] = 25
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;map的comma ok&lt;/h2&gt;
&lt;p&gt;在获取 &lt;code&gt;map&lt;/code&gt; 的值时，如果键不存在，Go 会返回该类型的零值（如 &lt;code&gt;int&lt;/code&gt;返回&lt;code&gt;0&lt;/code&gt;）。为了区分“值为0”和“键不存在”，我们通常这样做：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;val, ok := m[&quot;key&quot;] 
if ok { 
	fmt.Println(&quot;找到值:&quot;, val) 
} else { 
	fmt.Println(&quot;键不存在&quot;) 
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;map的遍历&lt;/h2&gt;
&lt;p&gt;我们可以使用&lt;code&gt;for range&lt;/code&gt;对map进行遍历，但是我们需要注意的是，对map进行遍历的时候输出的顺序是随机的&lt;/p&gt;
&lt;h3&gt;map的删除&lt;/h3&gt;
&lt;p&gt;我们可以用&lt;code&gt;delete&lt;/code&gt;函数删除map的一个键值对,语法规则是&lt;code&gt;delete(map,key)&lt;/code&gt;其中，map表示要删除键值对的map；key:表示要删除的键值对的键&lt;/p&gt;
&lt;h1&gt;结构体&lt;/h1&gt;
&lt;p&gt;和其他语言一样，在Go语言中，结构体是将多个不同的数据结构组合在一起的自定义类型&lt;/p&gt;
&lt;p&gt;因为在Go语言中不存在&lt;code&gt;Class&lt;/code&gt;,所以&lt;code&gt;OOP&lt;/code&gt;类型的语法都是通过结构体进行的&lt;/p&gt;
&lt;h2&gt;声明&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;type User struct{
	ID     int
	name   string
	Email  string
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对于结构体的实例化的方法有以下四种：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// 实例化方法
	// 1.直接实例化
	var u0 User
	// 2.结构化实例(推荐)
	u1 := User{id: 1, name: &quot;喵&quot;, Email: &quot;?&quot;}
	// 3.指针化实例
	u2 := &amp;amp;User{} // 返回的是一个空结构体指针
	// 4. 使用new实例化
	u3 := new(User)
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;在Go语言中，结构体在内存中是连续分布的，这使得结构体的访问速度非常快，对CPU缓存是友好的&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;结构体标签&lt;/h2&gt;
&lt;p&gt;在创建结构体元素的时候，我们可以为结构体打上标签&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type User struct {
	id    int    `json:&quot;id&quot;`
	name  string `json:&quot;name&quot;`
	Email string `json:&quot;Email&quot;`
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种标签可以通过标准库中的&lt;code&gt;reflect&lt;/code&gt;在运行时被解析，用于序列化，数据库映射等&lt;/p&gt;
&lt;h2&gt;方法&lt;/h2&gt;
&lt;p&gt;与其他语言不同，Go语言中结构体的方法不能放在结构体内部，而是脱离于结构体之外的&lt;/p&gt;
&lt;p&gt;它的语法是在 func 关键字后加一个接收者(Receiver)&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;类型&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;语法&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;能否修改结构体的值&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;值接受者&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func (u User) f()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;否&lt;/td&gt;
&lt;td&gt;拷贝一份副本，适合只读操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;指针接受者&lt;/td&gt;
&lt;td&gt;&lt;code&gt;func (u *User) f()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;是&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;推荐方式&lt;/strong&gt;，效率高且能修改状态&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;pre&gt;&lt;code&gt;func (u *User) SetName(newName string) {
    u.Name = newName
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;组合与嵌套（伪继承）&lt;/h2&gt;
&lt;p&gt;在Go语言中不支持类的继承，而是提倡通过 &lt;em&gt;匿名嵌套&lt;/em&gt; 来实现继承&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Animal struct {
	Name string
}

func (a *Animal) Getname() {
	fmt.Println(a.Name)
}

type Dog struct {
	Animal  // 此时dog已经继承Animal的方法和成员
}

func main() {
	d := Dog{}
	d.Name = &quot;狗&quot;
	d.Getname()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种继承方式被成为&lt;strong&gt;组合&lt;/strong&gt;，在一个结构体里不止能有一个父类，可以有很多父类&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type C struct{
	A
	B
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h1&gt;函数&lt;/h1&gt;
&lt;p&gt;Go语言摒弃了常规函数的类继承负担，设计的非常纯粹&lt;/p&gt;
&lt;h2&gt;函数声明和语法&lt;/h2&gt;
&lt;p&gt;Go语言的函数定义使用 &lt;code&gt;func&lt;/code&gt; 关键字，其特点是变量名在前，类型在后&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func add(a int,b int) int{
	return a + b
}

// 如果有类型一致的可以简写
func add(a,b int) int{
	return a + b
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;多返回值&lt;/h2&gt;
&lt;p&gt;不同于C++或Java只能返回一个类型，Go语言原生支持返回多个值&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;常规返回&lt;/strong&gt;:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func divide(a,b float64) (float64,error){
	if b == 0{
		return 0,errors.New(&quot;算术错误&quot;)
	}
	return a / b,nil
}
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;命名返回&lt;/strong&gt;：我们可以为返回值命名，这些初始的命名会被初始化为0值&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;func getRectProps(width,height float64) (area, perimeter float64){
	area = weight*height
	perimeter = (weight+height)*2
	return     // 裸返回，自动返回开头写的两个变量
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;变长参数&lt;/h2&gt;
&lt;p&gt;如果我们不确定会传入多少个参数，可以使用 &lt;code&gt;...&lt;/code&gt; 语法,在函数内部，这个参数会被当成切片处理&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func sum(nums ...int) int{
	tot := 0
	for _, n := range nums{
		tot += n
	}
	return tot
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;匿名函数和闭包&lt;/h2&gt;
&lt;p&gt;Go语言中支持匿名函数且这些函数可以作为闭包存在，即可以捕获并且记住其定义时所在作用域的变量&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func f1() func() int {
	i := 0
	res := func() int {
		i++
		return i
	}
	return res
}

func main() {
	counter := f1()        // 获取一个闭包
	fmt.Println(counter()) // 输出: 1
	fmt.Println(counter()) // 输出: 2
	fmt.Println(counter()) // 输出: 3
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在上面的代码中你可能注意到了我们给&lt;code&gt;res&lt;/code&gt;和&lt;code&gt;counter&lt;/code&gt;都赋值了函数，这代表在Go语言中函数是可以被作为变量赋值，作为参数传递和作为返回值的&lt;/p&gt;
&lt;h2&gt;异常处理&lt;/h2&gt;
&lt;p&gt;在Go语言中，没有类似 &lt;code&gt;try-catch&lt;/code&gt; 语句的异常捕获机制，其错误不是被抛出的，而是被当作普通的变量返回的&lt;/p&gt;
&lt;p&gt;在go中我们有两种放回错误的方式：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;errors.New(&quot;message&quot;)&lt;/code&gt;:最基础的方式,每次调用都会创建一个全新的地址，即便错误信息一模一样，它们也不相等&lt;/li&gt;
&lt;li&gt;&lt;code&gt;fmt.Errorf(&quot;error is: %v&quot;, detail)&lt;/code&gt;:用于格式化错误信息&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Linux下进行开发</title><link>https://blog.imicola.com/posts/linux%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%BC%80%E5%8F%91/</link><guid isPermaLink="true">https://blog.imicola.com/posts/linux%E4%B8%8B%E8%BF%9B%E8%A1%8C%E5%BC%80%E5%8F%91/</guid><description>为什么linux是神? 首先是犯下傲慢之罪的Windows,混乱的环境配置和抽象的编译链接；其次是犯下懒惰之罪的MacOS,虽然环境配置和编译链接相对简单，但价格过于昂贵,沦为高雅的玩具。Linux不语，只是一味为开发者提供便利的环境和工具。</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;h1&gt;Linux开发环境&lt;/h1&gt;
&lt;p&gt;相比与 Windows，在Linux下进行开发有如下好处：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;安装简单: 所有开发链在Linux中都可以通过一个命令实现，非常简单&lt;/li&gt;
&lt;li&gt;配置简单: 大多数的Linux开发工具的配置都可以通过编写配置文件完成，而且可定制性高于Windows&lt;/li&gt;
&lt;li&gt;管理简单：不用在忍受Windows下恐怖的环境配置和抽象的编译链接&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这篇文章主要讲解三个方面的知识:开发软件，编译工具，工程工具&lt;/p&gt;
&lt;h1&gt;开发软件&lt;/h1&gt;
&lt;h2&gt;代码编写工具与IDE&lt;/h2&gt;
&lt;h3&gt;图形化Linux界面&lt;/h3&gt;
&lt;p&gt;如果你是图形化的Linux界面，你可以自行安装代码编写工具，目前主流工具都对主流发行版有支持，比如Visual Studio Code提供多种包，也可以通过软件仓库进行安装&lt;/p&gt;
&lt;p&gt;同时JetBrains系列的软件也对Linux支持度也很高，你完全可以在官网中或者各个发行版的软件仓库中找到支持的版本并且下载&lt;/p&gt;
&lt;h3&gt;WSL&lt;/h3&gt;
&lt;p&gt;如果你使用的是WSL，即 &lt;em&gt;Windows Subsystem for Linux&lt;/em&gt;,其实更建议使用 Windows 上的代码编写工具，通过SSH或者WSL链接来获得更好的体验，虽然现在WSL2也原生支持wslg用来匹配GUI，但是其优化程度不如Windows&lt;/p&gt;
&lt;p&gt;对于Visual Studio Code，有原生链接wsl的工具，输入&lt;code&gt;ctrl + shift + P&lt;/code&gt;后输入 &lt;code&gt;链接到WSL&lt;/code&gt; 即可以链接&lt;/p&gt;
&lt;h1&gt;编译工具&lt;/h1&gt;
&lt;p&gt;我们在程序课上就学习过，要想让我们写的程序语言变为真正的可执行文件，还需要通过编译器的编译&lt;/p&gt;
&lt;p&gt;在Windows中，一般会给一份打包好的安装程序或者编译好的程序压缩包，而使用编译器需要配置环境变量等一系列操作，如果有多个编译程序(多版本管理)的时候还可能会出现编译混乱的情况&lt;/p&gt;
&lt;p&gt;在Linux中其实解决了很多这样的问题，下面我们通过介绍几个例子&lt;/p&gt;
&lt;h2&gt;C++环境&lt;/h2&gt;
&lt;p&gt;在 Windows 上，你可能需要下载几个 GB 的 Visual Studio，或者手动配置 MinGW 的 &lt;code&gt;bin&lt;/code&gt; 路径。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux 的做法：&lt;/strong&gt; 只需一行命令安装 &lt;code&gt;build-essential&lt;/code&gt;（以 Ubuntu 为例），它会自动帮你装好 &lt;code&gt;gcc&lt;/code&gt;、&lt;code&gt;g++&lt;/code&gt;、&lt;code&gt;make&lt;/code&gt; 以及标准库头文件。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt install build-essential
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;便利点：&lt;/strong&gt; 安装完即用，不需要去系统属性里手动编辑 &lt;code&gt;PATH&lt;/code&gt;。所有的头文件都在 &lt;code&gt;/usr/include&lt;/code&gt;，库文件在 &lt;code&gt;/usr/lib&lt;/code&gt;，编译器永远能找到它们。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Python环境&lt;/h2&gt;
&lt;p&gt;Windows 中管理 3.8、3.10、3.12 等多个python版本是比较棘手的&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux 的做法：&lt;/strong&gt; 大多数 Linux 发行版自带 Python。如果你需要管理多个版本，&lt;code&gt;pyenv&lt;/code&gt; 是神器：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;# 安装特定版本
pyenv install 3.10.12
# 在当前文件夹切换版本（仅对当前目录有效，不影响系统）
pyenv local 3.10.12
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;便利点：&lt;/strong&gt; &lt;strong&gt;“目录级”的版本控制&lt;/strong&gt;。你可以让项目 A 用 Python 3.8，项目 B 用 3.12，进入文件夹后版本自动切换，完全不会出现 Windows 上那种 &lt;code&gt;python&lt;/code&gt; 和 &lt;code&gt;python3&lt;/code&gt; 命令打架的情况&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;Go环境&lt;/h2&gt;
&lt;p&gt;Go 语言虽然在 Windows 上也有安装包，但配置 &lt;code&gt;GOPATH&lt;/code&gt; 和 &lt;code&gt;GOROOT&lt;/code&gt; 曾是很多人的噩梦&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Linux 的做法：&lt;/strong&gt; 除了包管理器，Go 在 Linux 下通常只需解压到 &lt;code&gt;/usr/local&lt;/code&gt; 即可。更方便的是使用 &lt;code&gt;snap&lt;/code&gt; 或 &lt;code&gt;asdf&lt;/code&gt;：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;sudo snap install go --classic
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;便利点：&lt;/strong&gt; Go 的交叉编译在 Linux 下表现极佳。由于 Linux 环境本身就是服务器的标准环境，你在 Linux 下编译出的二进制文件，丢到服务器上直接就能跑，不需要担心像 Windows 编译时可能带入的特定 DLL 依赖问题&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>User Space and Kernel Space</title><link>https://blog.imicola.com/posts/user-space-and-kernel-space/</link><guid isPermaLink="true">https://blog.imicola.com/posts/user-space-and-kernel-space/</guid><description>Linux操作空间的区分</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;img src=&quot;image.png&quot; alt=&quot;alt text&quot; /&gt;&lt;/p&gt;
&lt;p&gt;在 Linux 操作系统中，&lt;strong&gt;用户空间 (User Space)&lt;/strong&gt; 和 &lt;strong&gt;内核空间 (Kernel Space)&lt;/strong&gt; 是操作系统为了保证安全性和稳定性，在硬件（CPU）支持下进行的逻辑隔离&lt;/p&gt;
&lt;h2&gt;定义&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;用户空间&lt;/strong&gt;&lt;br /&gt;
用户空间是普通应用程序运行的地方，具有低特权级别，不能直接访问底层硬件和受保护的内存区域&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内核空间&lt;/strong&gt;&lt;br /&gt;
内核空间是操作系统的核心（Kernel）运行的地方。它拥有最高特权，可以直接控制 CPU、内存、磁盘、网卡等所有硬件资源&lt;/p&gt;
&lt;h2&gt;异同&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;特性&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;**用户空间 **&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;内核空间&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;特权等级&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;低特权 (Ring 3)&lt;/td&gt;
&lt;td&gt;高特权 (Ring 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;硬件访问&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;不允许直接访问，必须通过系统调用&lt;/td&gt;
&lt;td&gt;可以直接访问 CPU、内存、I/O 等&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;内存分配&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;只能访问被分配的私有内存&lt;/td&gt;
&lt;td&gt;可以访问整个物理内存&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;奔溃后果&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;仅当前进程崩溃，系统依然稳定&lt;/td&gt;
&lt;td&gt;可能导致内核恐慌 (Kernel Panic)，系统重启&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;主要任务&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;运行各类应用软件&lt;/td&gt;
&lt;td&gt;资源调度、内存管理、硬件驱动、进程管理&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;误解:权限和空间&lt;/h2&gt;
&lt;p&gt;当我们使用 &lt;code&gt;root&lt;/code&gt; 权限执行命令的时候，这个命令也是在用户空间操作的，而不是在内核空间下完成的&lt;/p&gt;
&lt;p&gt;运行模式(即空间的划分)是在硬件层面的，两个空间就如同一个有窗户相隔的两个房间，代码的执行在用户空间进行，只是需要使用资源的时候通过窗户call一下内核空间，而不可能直接打破墙去内核空间操作&lt;/p&gt;
&lt;hr /&gt;
&lt;blockquote&gt;
&lt;p&gt;[!note]
当然也有可以直接在内核空间操作的方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;编写并加载内核模块 (LKM)&lt;/strong&gt;：比如你写了一个 &lt;code&gt;.ko&lt;/code&gt; 驱动文件，用 &lt;code&gt;insmod&lt;/code&gt; 加载。这段代码会直接并入内核，拥有毁灭一切的权限。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;修改内核源码&lt;/strong&gt;：重新编译 Linux 内核（比如你打算给 Arch Linux 换个自定义内核）&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;查看内核空间命令&lt;/h2&gt;
&lt;p&gt;我们知道用户空间执行命令的时候会向内核空间请求和发送系统调用，内核处理完成后会返回给程序返回值，而我们可以使用[[系统调用追踪]]也就是&lt;code&gt;strace&lt;/code&gt;来观察用户空间和内核空间的交互通信&lt;/p&gt;
</content:encoded></item><item><title>多线程与并发</title><link>https://blog.imicola.com/posts/%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%B8%8E%E5%B9%B6%E5%8F%91/</link><guid isPermaLink="true">https://blog.imicola.com/posts/%E5%A4%9A%E7%BA%BF%E7%A8%8B%E4%B8%8E%E5%B9%B6%E5%8F%91/</guid><description>Go语言并发编程：Goroutine、Channel 与 GMP 调度模型</description><pubDate>Wed, 04 Feb 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&lt;em&gt;&lt;strong&gt;不要通过共享内存来通信，而要通过通信来共享内存&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h1&gt;概念明晰&lt;/h1&gt;
&lt;p&gt;在深入了解并发前我们需要了解一些关于多线程和并发编程中常见的概念&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;并发：是指你的程序有&lt;strong&gt;处理&lt;/strong&gt;多个任务的能力。在一个单核 CPU 上，通过快速切换任务，看起来像是同时在做很多事，但实际上某一时刻只有一个任务在运行。&lt;/li&gt;
&lt;li&gt;并行：是指你的程序有&lt;strong&gt;同时执行&lt;/strong&gt;多个任务的能力。这需要多核 CPU 的硬件支持，让不同的任务真正地在不同的核心上同时运转。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;同时在我们需要了解一下 进程，线程和协程三者的区别&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;特性&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;进程 (Process)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;线程 (Thread)&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;协程 (Goroutine)&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;拥有者&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;操作系统&lt;/td&gt;
&lt;td&gt;操作系统&lt;/td&gt;
&lt;td&gt;Go 运行时 (Runtime)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;内存占用&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;很大 (MB 级别)&lt;/td&gt;
&lt;td&gt;较大 (~1MB)&lt;/td&gt;
&lt;td&gt;极小 (初始仅 2KB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;切换开销&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;极高 (内核态切换)&lt;/td&gt;
&lt;td&gt;高 (涉及寄存器保存)&lt;/td&gt;
&lt;td&gt;极低 (用户态切换)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;通信方式&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;IPC (管道、信号量)&lt;/td&gt;
&lt;td&gt;共享内存 (需加锁)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Channel (推荐)&lt;/strong&gt;、共享内存&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
&lt;h1&gt;Goroutine&lt;/h1&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;p&gt;在Go语言中开启一个并发任务十分简单，只需要在函数之前添加一个 &lt;code&gt;go&lt;/code&gt; 关键字&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;go function(params)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;底层逻辑：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;非阻塞：调用后，主程序会立即向下执行，不会等待 &lt;code&gt;doSomething&lt;/code&gt; 返回。&lt;/li&gt;
&lt;li&gt;匿名函数：常用 &lt;code&gt;go func() { ... }()&lt;/code&gt; 的形式开启，但要注意&lt;strong&gt;闭包捕获变量&lt;/strong&gt;的坑（在循环中直接使用循环变量可能会导致所有协程引用同一个值）。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;生命周期管理&lt;/h2&gt;
&lt;p&gt;协程虽然轻量，但是对于其的生命周期管理也是必须的，如果管理不当，会导致 &lt;strong&gt;Goroutime泄露&lt;/strong&gt; 问题&lt;/p&gt;
&lt;p&gt;生命周期：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;启动&lt;/strong&gt; : 通过 &lt;code&gt;go&lt;/code&gt; 关键字&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;运行&lt;/strong&gt; : 由GMP调度器分配到逻辑处理器上&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;退出&lt;/strong&gt; ：
&lt;ol&gt;
&lt;li&gt;函数体执行完毕&lt;/li&gt;
&lt;li&gt;main函数结束&lt;/li&gt;
&lt;li&gt;发生不可恢复的 &lt;code&gt;panic&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;GMP模型&lt;/h2&gt;
&lt;p&gt;在Go语言协程调度中有三个关键角色&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;角色&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;全称&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;作用&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;Goroutine&lt;/td&gt;
&lt;td&gt;协程。包含栈内存、指令指针等。它不直接运行，而是被调度的单位。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;M&lt;/td&gt;
&lt;td&gt;Machine&lt;/td&gt;
&lt;td&gt;操作系统线程（OS Thread）。它是真正的执行单元。M 必须绑定 P 才能运行 G。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P&lt;/td&gt;
&lt;td&gt;Processor&lt;/td&gt;
&lt;td&gt;逻辑处理器。它像一个“入场券”和“资源桶”，维护着一个本地待运行的 G 队列。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;调度全景：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;我们执行 &lt;code&gt;go func()&lt;/code&gt; 产生一个G&lt;/li&gt;
&lt;li&gt;G优先放入当前 P 的本地队列，如果本地满了，才放入全局队列&lt;/li&gt;
&lt;li&gt;M 获取一个 P，然后从P的本地队列中取出G执行&lt;/li&gt;
&lt;li&gt;如果本地队列、全局队列都空了，M 就会启动 Work Stealing 从其他 P 偷任务。&lt;/li&gt;
&lt;/ol&gt;
&lt;h1&gt;Channel&lt;/h1&gt;
&lt;p&gt;Channel可以翻译成通道，和linux下的[[Shell#^shttyz|pipe]]不同，Go语言中是我们可以看作是一个有类型的管道&lt;/p&gt;
&lt;h2&gt;语法&lt;/h2&gt;
&lt;p&gt;channel的使用符号为&lt;code&gt;&amp;lt;-&lt;/code&gt;，我们可以将其看作数据流动的方向&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;创建Channel：&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;// 创建一个传递整型的管道
ch := make(chan int)

// 创建一个转递字符串的管道，并且带有3个容量的缓冲区
ch1 := make(chan string,3)
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;发送数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;ch &amp;lt;- 100   //将数字100发送到管道ch中
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;接受数据&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;data := &amp;lt;- ch   //从管道中取出一个值并且赋值给 data
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;关闭管道：&lt;br /&gt;
这是用来通知接收方以后没有数据进来了&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt;close(ch)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;阻塞&lt;/h2&gt;
&lt;p&gt;管道的发送和接受收到 &lt;em&gt;阻塞&lt;/em&gt; 的约束&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;发送阻塞&lt;/strong&gt;：如果你往管道里塞东西，但没人接，你就会在那儿&lt;strong&gt;等着（阻塞）&lt;/strong&gt;，直到有人来拿。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;接收阻塞&lt;/strong&gt;：如果你从管道里取东西，但里面是空的，你也会在那儿&lt;strong&gt;等着（阻塞）&lt;/strong&gt;，直到有人塞进一个东西。&lt;br /&gt;
需要注意的是有缓冲管道和无缓冲管道的阻塞情况不同&lt;/li&gt;
&lt;li&gt;无缓冲管道要求管道的接受和发送都必须同步，否则就会一直阻塞&lt;/li&gt;
&lt;li&gt;有缓冲管道会在管道满或空的时候发生阻塞&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;h2&gt;select多路复用&lt;/h2&gt;
&lt;p&gt;我们在处理多个协程通信的时候可以使用&lt;code&gt;select&lt;/code&gt;来帮助我们&lt;/p&gt;
&lt;p&gt;&lt;code&gt;select&lt;/code&gt; 看起来像 &lt;code&gt;switch&lt;/code&gt;，但它是专门为通道设计的，它会&lt;strong&gt;伪随机的&lt;/strong&gt;选择一个可运行的 case 执行。&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;ch1 := make(chan int, 1)
ch2 := make(chan int, 1)

ch1 &amp;lt;- 1
ch2 &amp;lt;- 2

select {
case v1 := &amp;lt;-ch1:
    fmt.Println(&quot;执行了 ch1:&quot;, v1)
case v2 := &amp;lt;-ch2:
    fmt.Println(&quot;执行了 ch2:&quot;, v2)
}

fmt.Println(&quot;select 结束了&quot;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们如果想要实现处理所有数据，标准的做法是套上 &lt;code&gt;for&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;for {
    select {
    case v1 := &amp;lt;-ch1:
        fmt.Println(&quot;处理 ch1:&quot;, v1)
    case v2 := &amp;lt;-ch2:
        fmt.Println(&quot;处理 ch2:&quot;, v2)
    default:
        // 如果两个都没数据了，可以在这里退出循环或者做点别的
        fmt.Println(&quot;暂时没数据了&quot;)
        return 
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Channel的实际使用&lt;/h2&gt;
&lt;h3&gt;在匿名函数中使用&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func fu() {
	ch := make(chan int) 
	go func(){
		fmt.Println(&quot;ok&quot;)
		ch &amp;lt;- 1     // 发送数据
	}()
	&amp;lt;- ch	// 等待接受到数据
	fmt.Println(&quot;喵&quot;)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;作为具体参数传入&lt;/h3&gt;
&lt;pre&gt;&lt;code&gt;func downloader(url string, resultCh chan string) {
    // 模拟下载
    res := &quot;下载完成: &quot; + url
    resultCh &amp;lt;- res // 把结果发回管道
}

func main() {
    ch := make(chan string)
    // 启动具名函数
    go downloader(&quot;https://google.com&quot;, ch)
    fmt.Println(&amp;lt;-ch)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;作为结构体的成员&lt;/h3&gt;
&lt;p&gt;当我们构建复杂系统的时候，channel通常是结构体的一部分，用来控制这个实例的状态或传递数据&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;type Worker struct {
    ID     int
    JobCh  chan int    // 接收任务的管道
    QuitCh chan bool   // 接收停止信号的管道
}

func (w *Worker) Start() {
    go func() {
        for {
            select {
            case job := &amp;lt;-w.JobCh:
                fmt.Printf(&quot;工人 %d 正在处理任务 %d\n&quot;, w.ID, job)
            case &amp;lt;-w.QuitCh:
                fmt.Printf(&quot;工人 %d 关机下班\n&quot;, w.ID)
                return
            }
        }
    }()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;工厂模式&lt;/h3&gt;
&lt;p&gt;作为一个函数的返回值用来创建一个管道并且启动一个协程往里面灌数据，然后将管道返回调用者&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;func dataProducer() &amp;lt;-chan int {
    ch := make(chan int)
    go func() {
        for i := 0; i &amp;lt; 5; i++ {
            ch &amp;lt;- i
        }
        close(ch)
    }()
    return ch // 返回一个只读管道
}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr /&gt;
</content:encoded></item></channel></rss>