<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>实践经验归档 - 枫阿雨&#039;s blog</title>
	<atom:link href="https://www.crazyfay.com/category/practical-experience/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.crazyfay.com/category/practical-experience/</link>
	<description>CrazyFay</description>
	<lastBuildDate>Tue, 05 Dec 2023 06:27:56 +0000</lastBuildDate>
	<language>zh-CN</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.5.2</generator>

<image>
	<url>https://www.crazyfay.com/wp-content/uploads/2023/04/cropped-DockerGopher-32x32.png</url>
	<title>实践经验归档 - 枫阿雨&#039;s blog</title>
	<link>https://www.crazyfay.com/category/practical-experience/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>&#8220;熟练掌握Windows环境下的开发&#8221;</title>
		<link>https://www.crazyfay.com/2023/04/21/%e7%86%9f%e7%bb%83%e6%8e%8c%e6%8f%a1windows%e7%8e%af%e5%a2%83%e4%b8%8b%e7%9a%84%e5%bc%80%e5%8f%91/</link>
					<comments>https://www.crazyfay.com/2023/04/21/%e7%86%9f%e7%bb%83%e6%8e%8c%e6%8f%a1windows%e7%8e%af%e5%a2%83%e4%b8%8b%e7%9a%84%e5%bc%80%e5%8f%91/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Fri, 21 Apr 2023 06:28:17 +0000</pubDate>
				<category><![CDATA[实践经验]]></category>
		<category><![CDATA[效率工具]]></category>
		<guid isPermaLink="false">https://www.crazyfay.com/?p=272</guid>

					<description><![CDATA[<p>熟练掌握Windows环境下的开发 前记：如果不是生活所迫，谁想用Windows作为开发环境呢？ Window [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/21/%e7%86%9f%e7%bb%83%e6%8e%8c%e6%8f%a1windows%e7%8e%af%e5%a2%83%e4%b8%8b%e7%9a%84%e5%bc%80%e5%8f%91/">&#8220;熟练掌握Windows环境下的开发&#8221;</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>熟练掌握Windows环境下的开发</h1>
<blockquote>
<p>前记：如果不是生活所迫，谁想用Windows作为开发环境呢？</p>
</blockquote>
<p>Windows是一个好的操作系统，但绝对不是个最适合开发者的OS。穷学生又没钱上Mac，Linux的各个发行版虽然好用，但是生态又不够完整，一些开发之外的其他需求难以满足，想尝试远程开发环境，却发现延迟高的要死，性能实在拉跨。所以看来看去还是只能将就着使用Windows作为自己的开发环境。</p>
<p>本文则是我这几年来使用Windows作为开发环境的过程对提升使用体验各种摸索后得到的结果。</p>
<h2>善于Windows的快捷键</h2>
<p>我想多数开发者都更希望减少鼠标的操作，能用键盘解决的都用键盘解决，手能不离开键盘就不离开（典型的就是vim神教），虽然大家都说Windows的种种不是，但是其实Windows系统对快捷键的支持其实还是很优秀的。下面我将尽可能全面的列举一些我平时经常能够用到的快捷键，<strong>注意！本部分的快捷键针对于Windows11，Win11以下的版本可能有些许不同，但大体上还是一致的。</strong></p>
<h3>Win系快捷键</h3>
<h4>win + 数字</h4>
<p>★★★★☆</p>
<p>打开任务栏对应位置的应用，按住win点击对应数字进行切换，如果同时按住shift会打开新窗口，如果同时按住ctrl + shift会以管理员身份打开新窗口</p>
<blockquote>
<p>这个快捷键比较有用，尤其是配合ctrl + shift，如果你把终端放到了底部任务栏，那么就可以快捷地以管理员身份打开终端，否则会一管理员身份打开会有些许麻烦</p>
</blockquote>
<h4>win + Q</h4>
<p>★★☆☆☆</p>
<p>windows自带搜索，</p>
<blockquote>
<p>Q是Query的意思</p>
</blockquote>
<h4>win + E</h4>
<p>★★★☆☆</p>
<p>打开快速访问文件夹</p>
<blockquote>
<p>用来查找一些最近打开的文件目录或者打开此电脑比较方便，我的一个使用习惯是进入到快速访问文件夹之后用快捷键ctrl + L打开定位到资源导航栏，手动输入&quot;D:/&quot;访问到D盘</p>
</blockquote>
<h4>win + R</h4>
<p>★★★☆☆</p>
<p>打开&quot;窗口&quot;，可以快速访问cmd等</p>
<blockquote>
<p>主要用于打开cmd，但是不能通过此方式以管理员身份运行，可选择ctrl + shift + win + 数字以管理员身份打开终端后新建cmd窗口会话，或者通过win + Q查询cmd，再用鼠标以管理员身份打开</p>
<p>R是Run的意思</p>
</blockquote>
<h4>win + T</h4>
<p>★★☆☆☆</p>
<p>windows底栏应用选择（按空格或者回车后可以打开）</p>
<blockquote>
<p>我一般更多使用alt + Tab切换已打开窗口，主要是我的底部任务栏的内容一般都会被打开XD</p>
<p>T是Tab的意思</p>
</blockquote>
<h4>win + A</h4>
<p>★★☆☆☆</p>
<p>打开设置栏</p>
<blockquote>
<p>偶尔切个Wifi什么的会用到</p>
</blockquote>
<p></p>
<p>win + shifit + S</p>
<p>★★★☆☆</p>
<p>微软自带截图工具，截图保存到剪切板</p>
<blockquote>
<p>可应急使用，功能较简单，使用上不如QQ、Tim、微信截图方便</p>
</blockquote>
<p></p>
<h4>win + D</h4>
<p>★★★★★</p>
<p>快速切换到桌面</p>
<blockquote>
<p>应用场景非常非常多，尤其当你打开了一堆页面之后，快速切到桌面非常的爽</p>
<p>D是Desktop的意思</p>
</blockquote>
<h4>win + G</h4>
<p>★☆☆☆☆</p>
<p>xbox bar 游戏相关</p>
<blockquote>
<p>可以用来录屏</p>
<p>G是Game的意思</p>
</blockquote>
<h4>win + L</h4>
<p>★★★☆☆</p>
<p>锁屏</p>
<blockquote>
<p>快速锁屏，写完代码要出去走动走动，win + L一下子锁好屏幕感觉用起来比较舒服，但是貌似我用到的场景不多，毕竟我经常懒得锁屏XD，（进入锁屏界面后，按空格或者回车可以触发登录按钮）</p>
<p>L是Lock的意思</p>
</blockquote>
<h4>win + Z</h4>
<p>★★☆☆☆</p>
<p>快速屏幕分页</p>
<blockquote>
<p>分页规则很清晰，但是我一般用win + 上下左右 更多一些</p>
</blockquote>
<h4>win + X</h4>
<p>★☆☆☆☆</p>
<p>相当于右键开始按钮</p>
<blockquote>
<p>点开之后可以通过上下+空格/回车或者alt+提示的快捷键打开别的功能界面（也可以用来快速以管理员身份打开终端），有用，但是又没有那么有用</p>
</blockquote>
<h4>win + V</h4>
<p>★★★★★</p>
<p>剪切板</p>
<blockquote>
<p>离不开的功能，非常非常非常有用</p>
</blockquote>
<h4>win + B</h4>
<p>★★★☆☆</p>
<p>定位到图标栏</p>
<blockquote>
<p>直接定位到隐藏栏的箭头，再点击空格或者回车可以打开隐藏栏，也可以通过左右键操作隐藏栏右侧的各个菜单</p>
</blockquote>
<h4>win + N</h4>
<p>★★☆☆☆</p>
<p>打开通知和日期</p>
<blockquote>
<p>想看日历可以用</p>
<p>N是Notice的意思</p>
</blockquote>
<h4>win + M</h4>
<p>★★★☆☆</p>
<p>将已打开的所有应用最小化<br />
和win + D的区别是不能再按一次复原，<br />
Win键+Shift+M 将最小化的窗口还原到桌面</p>
<blockquote>
<p>在屏幕中同一个窗口打开数量过多时可以使用，再通过alt + tab切换到自己想要打开的窗口</p>
</blockquote>
<h4>win + ,</h4>
<p>★☆☆☆☆</p>
<p>按住时显示桌面</p>
<blockquote>
<p>emmmm，使用场景很少</p>
</blockquote>
<h4>win + .</h4>
<p>★★★★☆</p>
<p>v模式中的内容选择栏</p>
<blockquote>
<p>找一些特殊字符的时候很好用</p>
</blockquote>
<h3>浏览器快捷键</h3>
<p>多数浏览器通用，尤其edge与chrome</p>
<h4>ctrl + W</h4>
<p>★★★★★<br />
关闭一个页面</p>
<blockquote>
<p>也可以关闭文件目录，必备不解释XD</p>
</blockquote>
<h4>ctrl + F/G</h4>
<p>★★★★★</p>
<p>搜索</p>
<blockquote>
<p>按关键词搜索，对于懒加载的数据无效哦</p>
</blockquote>
<h4>ctrl + R</h4>
<p>★★★★★</p>
<p>刷新当前页面</p>
<blockquote>
<p>必备不解释</p>
</blockquote>
<p>ctrl + T</p>
<p>★★★★★</p>
<p>打开新的空白标签页</p>
<blockquote>
<p>可结合ctrl + L进行搜索，即组合为打开新的标签页并搜索/跳转到新的内容</p>
</blockquote>
<h4>ctrl + U</h4>
<p>★★★☆☆</p>
<p>查看页面源代码</p>
<blockquote>
<p>偶尔用得到</p>
</blockquote>
<h4>ctrl + O</h4>
<p>★☆☆☆☆</p>
<p>在浏览器中范围本地文件</p>
<blockquote>
<p>使用场景较少，偶尔想要快速打开某个文本文件或者pdf看看内容的时候可以用</p>
</blockquote>
<h4>ctrl + shift + O</h4>
<p>★★★★★</p>
<p>打开收藏夹</p>
<blockquote>
<p>必备不解释</p>
</blockquote>
<h4>ctrl + P</h4>
<p>★★★☆☆</p>
<p>打印当前界面</p>
<blockquote>
<p>需要保存网页内容时可用，可代替长截图</p>
</blockquote>
<p></p>
<h4>ctrl + H</h4>
<p>★★★★★</p>
<p>浏览记录历史</p>
<blockquote>
<p>必备不解释</p>
</blockquote>
<p></p>
<h4>ctrl + D</h4>
<p>★★★★★</p>
<p>收藏当前界面</p>
<blockquote>
<p>必备不解释</p>
</blockquote>
<p></p>
<h4>ctrl + J</h4>
<p>★★★★★</p>
<p>浏览下载记录</p>
<blockquote>
<p>必备不解释</p>
</blockquote>
<p></p>
<h4>ctrl + L</h4>
<p>★★★★★</p>
<p>定位到浏览器URL框</p>
<blockquote>
<p>可以进行搜索，URL跳转的功能，对于我来说是浏览器的核心快捷键。在Windows目录里也可以使用ctrl + L快速定位到的URL框</p>
</blockquote>
<p></p>
<h4>ctrl + K</h4>
<p>★★☆☆☆</p>
<p>快速搜索</p>
<blockquote>
<p>个人几户不用，完全可由ctrl + L代替</p>
</blockquote>
<p></p>
<h4>ctrl + N</h4>
<p>★★★★★</p>
<p>打开新的窗口，并打开一个空白标签页</p>
<blockquote>
<p>当一个浏览器窗口中的标签页过多时使用，打开一个新的浏览器窗口</p>
</blockquote>
<p></p>
<h4>ctrl + shift + N</h4>
<p>★★★★★</p>
<p>打开新的隐私窗口，并打开一个空白标签页</p>
<blockquote>
<p>隐私窗口不会附带cookie等内容，必备</p>
</blockquote>
<p></p>
<h4>ctrl + 数字</h4>
<p>★★★★☆</p>
<p>切换到第N个标签页</p>
<blockquote>
<p>标签页少的时候可以用，多的时候不是很方便，推荐使用ctrl + Tab替代</p>
</blockquote>
<p></p>
<h4>ctrl + Tab</h4>
<p>★★★★★</p>
<p>切换到右边一个标签页</p>
<blockquote>
<p>必备不解释</p>
</blockquote>
<p></p>
<h4>ctrl + shift + Tab</h4>
<p>★★★★★</p>
<p>切换到左边一个标签页</p>
<blockquote>
<p>必备不解释</p>
</blockquote>
<p></p>
<h4>通用快捷键</h4>
<p>ctrl + A</p>
<p>全选</p>
<p>ctrl + S</p>
<p>保存</p>
<p>ctrl + Z</p>
<p>撤销</p>
<p>ctrl + X</p>
<p>剪切</p>
<p>ctrl + C</p>
<p>复制</p>
<p>ctrl + V</p>
<p>粘贴</p>
<blockquote>
<p>通通不解释</p>
</blockquote>
<p></p>
<h3>其他常用快捷键</h3>
<h4>ctrl + shift + N</h4>
<p>★★★☆☆</p>
<p>快速创建文件夹</p>
<h4>ctrl + shift + 左键</h4>
<p>★★★★★</p>
<p>以管理员身份打开应用</p>
<p>★★★★★</p>
<h4>ctrl + shift + Esc</h4>
<p>快速打开任务管理器</p>
<h3>other tips</h3>
<p>选择文本时善用shift进行选择，ctrl进行光标快速移动</p>
<p></p>
<h2>代码环境分离</h2>
<p>因为当代服务端应用绝大多数都是以Linux系统为基础的，所以我们使用Linux作为自己开发环境觉得是更合适的，装双系统切换起来太麻烦，VMWare这类虚拟机又太重，在Windows系统下，我们有个更好的选择，这便是WSL(Windows Subsystem for Linux <strong>适用于Linux的Windows子系统</strong>)。简而言之，就是微软官方提供了一个Windows系统下的Linux系统，我们就可以在Windows系统中通过WSL使用Linux的环境。</p>
<p>目前为止，WSL拥有两个版本WSL1和WSL2，其中WSL1其实更像是在Windows系统上封装一层Linux的接口，而WSL2则是依赖于Hyper-V ，你可以理解为WSL2是利用了Windows原生的虚拟机， 支持完整的Linux内核，且比VMWare等专业的虚拟机应用轻量级许多，而且因为拥有完整的Linux内核，在WSL2上可以使用Docker，更加一步方便了我们环境的搭建。</p>
<p>那我们就先开始吧</p>
<h3>all in wsl2</h3>
<p>首先，确保你的Windows是专业版及以上（不能是学生版、家庭版，否则无法开启WSL，如果非专业版自行搜索升级方法），并且是 Windows 10 版本 2004 及更高版本（内部版本 19041 及更高版本）或 Windows 11 。</p>
<p>可在系统信息或者其他位置查看，如果不满足请升级系统。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/59af8cbf4c5694ca2cd3e2f8f91b1540.png" alt="截图" /></p>
<p>并确定已开启虚拟化。</p>
<p>可在任务管理器中查看，一般都会默认开启</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/8a7fe49929f2f811b261e57d88efca8d.png" alt="截图" /></p>
<p>进入控制面板-&gt;程序-&gt;启用或关闭 windows 功能</p>
<p>开启虚拟机平台、适用于Linux的Windows子系统以及Hyper-V</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/86c33fb7f37e9278ce01e00133d9fae8.png" alt="截图" /></p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/9091deb6b2321010ae632ba05498b84f.png" alt="截图" /></p>
<p>以管理员权限进入Powershell（可以win+Q搜索Powershell然后再选择以管理员方式打开）</p>
<p>执行以下命令</p>
<pre><code class="language-sh">wsl --install
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
wsl --set-default-version 2</code></pre>
<p>电脑重启之后wsl2应该就已经装好了，我们可以直接在终端里选择打开wsl2，或者在cmd或者powershell中执行</p>
<pre><code class="language-sh">wsl</code></pre>
<p>命令，也会进入wsl的终端。</p>
<p>跟着指引走设置默认用户名和密码就可以使用了。</p>
<h4>配合JetBrains产品体验更佳</h4>
<p>我们可以把项目的代码放在wsl中，这样我们就可以使用linux环境下进行开发了，可以使用linux的系统函数调用，非常的舒服。</p>
<p>我们可以直接使用JetBrains产品，打开或者新建一个wsl中的项目</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/ea24778a34a0a882c857da14d080e37f.png" alt="截图" /></p>
<p>在IDE中打开的终端也是默认wsl的终端</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/5965c2447f903ef13533660b32a28e1e.png" alt="截图" /></p>
<p>于是我们就可以在Linux环境下愉快地coding了~</p>
<h3>善用Docker</h3>
<p>Docker 是一种工具，用于创建、部署和运行应用程序（通过使用容器）。 容器使开发人员可以将应用与需要的所有部件（库、框架、依赖项等）打包为一个包一起交付。 使用容器可确保此应用的运行与之前相同，而不受任何自定义设置或运行该应用的计算机上先前安装的库的影响（运行应用的计算机可能与用于编写和测试应用代码的计算机不同）。 这使开发人员可以专注于编写代码，而无需操心将运行代码的系统。</p>
<p>Docker 容器与虚拟机类似，但不会创建整个虚拟操作系统。 相反，Docker 允许应用使用与运行它的系统相同的 Linux 内核。 这使得应用包能够仅要求主计算机上尚未安装的部件，从而降低包大小以及提高性能。</p>
<p>利用docker安装软件可以省去很多复杂的配置环境，而且也方便未来数据的迁移等工作，Docker是依赖于Linux系统的Namespace，Cgroup和UnionFS实现的，所以本身是不可以运行在Windows操作系统上的，但是有了wsl2这个好东西，Docker也可以基于wsl2在Windows上运行了。</p>
<h4>安装Docker Desktop</h4>
<ol>
<li>
<p>下载 Docker Desktop 并按照安装说明进行操作。</p>
</li>
<li>
<p>安装后，从 Windows 开始菜单启动 Docker Desktop，然后从任务栏的隐藏图标菜单中选择 Docker 图标。 右键单击该图标以显示 Docker 命令菜单，然后选择“设置”。 Docker Desktop 仪表板图标</p>
</li>
</ol>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/docker-starting.png" alt="" /></p>
<ol start="3">
<li>确保在“设置”&gt;“常规”中选中“使用基于 WSL 2 的引擎”。 Docker Desktop 常规设置</li>
</ol>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/docker-running.png" alt="" /></p>
<ol start="4">
<li>通过转到“设置”&gt;“资源”&gt;“WSL 集成”，从要启用 Docker 集成的已安装 WSL 2 发行版中进行选择。 Docker Desktop 资源设置</li>
</ol>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/docker-dashboard.png" alt="" /></p>
<ol start="5">
<li>
<p>若要确认已安装 Docker，请打开 WSL 发行版（例如 Ubuntu），并通过输入 docker --version 来显示版本和内部版本号</p>
</li>
<li>
<p>通过使用 docker run hello-world 运行简单的内置 Docker 映像，测试安装是否正常工作</p>
</li>
</ol>
<blockquote>
<p>安装配置好之后，我们在Powershell和WSL2中都可以使用docker工具</p>
</blockquote>
<h5>docker与docker-compose</h5>
<p>docker-compose是用于定义和运行多容器 Docker 应用程序的工具。通过 docker-compose，您可以使用 .yaml 文件来配置应用程序需要的所有服务。然后，使用一个命令，就可以从 .yaml 文件配置中创建并启动所有服务。</p>
<p>docker-compose是用来运行一组容器服务的，但是也可以用来运行单个容器服务，因为docker-compose部署的容器更具管理上的优势，也是为了方便管理配置信息，我们采用docker-compose来在开发环境中部署一些必要的服务。部署之后就可以通过Docker Desktop直接进行服务容器的管理了</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/a5ef3dd29aedcfbe163bd84c84f094e8.png" alt="截图" /></p>
<h5>docker-compose安装 MySQL</h5>
<p>在你喜欢的地方新建一个文件夹，创建一个docker-compose-mysql.yaml文件，并入以下内容：</p>
<pre><code class="language-yaml">version: &#039;3&#039;
services:
  mysql:
    container_name: mysql8
    image: mysql:8.0.20
    restart: always
    ports:
      - 3306:3306
    environment:
      TZ: Asia/Shanghai
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: root
    volumes:
      - D:/program/data/mysql/data:/var/lib/mysql
      - D:/program/data/mysql/conf:/etc/mysql/conf.d/
      - D:/program/data/mysql/logs:/logs
    command:
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_general_ci
      --explicit_defaults_for_timestamp=true
      --lower_case_table_names=1</code></pre>
<blockquote>
<p>请注意，我强烈不推荐在服务器的生产环境下使用Docker部署MySQL这样基础的数据服务，因为这类数据库服务应该建立在更稳定的存储介质上，故不推荐容器部署。但是在个人的开发环境下，使用容器部署可以更便捷和易于管理。</p>
<p>其中volumes将数据挂载到本地磁盘中，可按照需求挂载到指定位置</p>
</blockquote>
<p>在此目录下进入powershell，执行以下命令：</p>
<pre><code class="language-sh">docker-compose -f docker-compose-mysql.yaml up -d</code></pre>
<blockquote>
<p>因为指定的路径是windows下的路径，所以只能在powershell中执行</p>
<p>也可以不进入当前文件夹，但是-f后面的要写出对应.yaml文件的路径</p>
</blockquote>
<h5>docker-compose安装 Redis</h5>
<p>在你喜欢的地方新建一个文件夹，创建一个docker-compose-redis.yaml文件，并入以下内容：</p>
<pre><code class="language-yaml">version: &#039;3&#039;
services:
  Redis:
    container_name: redis6
    image: redis:6.2.7
    restart: always
    volumes:
      - D:/program/data/redis/data:/data
      - D:/program/data/redis/conf/redis.conf:/etc/redis/redis.conf
    ports:
      - 6379:6379
    command: redis-server /etc/redis/redis.conf
</code></pre>
<p>在此目录下进入powershell，执行以下命令：</p>
<pre><code class="language-sh">docker-compose -f docker-compose-redis.yaml up -d</code></pre>
<h5>docker-compose安装 etcd</h5>
<p>在你喜欢的地方新建一个文件夹，创建一个docker-compose-etcd.yaml文件，并入以下内容：</p>
<pre><code class="language-yaml">version: &#039;3&#039;
services:
  Etcd:
    container_name: etcd3
    image: bitnami/etcd:3.5.6
    deploy:
      replicas: 1
      restart_policy:
        condition: on-failure
    environment:
      - ALLOW_NONE_AUTHENTICATION=yes
    privileged: true
    volumes:
      - D:/program/data/etcd/data:/bitnami/etcd/data
    ports:
      - 2379:2379
      - 2380:2380</code></pre>
<p>在此目录下进入powershell，执行以下命令：</p>
<pre><code class="language-sh">docker-compose -f docker-compose-etcd.yaml up -d</code></pre>
<h2>一些提高体验感的软件</h2>
<h3>clash</h3>
<p>某猫型科学上网客户端，个人感觉比v2ray好用很多，而且可以直接代理wsl2的网络，用起来非常方便</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/53c4d45700d523566628fe6eddb370c9.png" alt="截图" /></p>
<p>这些选项全部都可以打开，配合着写好的代理规则，实现无心智负担的愉快冲浪之旅</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/834c75bbd2cd8d50660ce63197d029dd.png" alt="截图" /></p>
<h3>utools</h3>
<p>非常非常好用的快捷效率工具，我个人已经完全离不开了</p>
<p>你在电脑上想找的每个东西、想做的每件事都可以问一问utools，比如要配置环境变量</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/093a5b79648edb2878891374365ec7f0.png" alt="截图" /></p>
<p>又比如想要快速关机</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/1b805bad12b51ce286728ed338a24019.png" alt="截图" /></p>
<p>快速打开某个应用软件</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/4ecff6f180388fd022fefceba834079e.png" alt="截图" /></p>
<p>快速翻译</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/5e1d0905afbbf1a711467568d1fd77de.png" alt="截图" /></p>
<p>各种工具箱</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/98fe5fe3e452f75a9f7c14fbbd0a2c6a.png" alt="截图" /></p>
<p>还有很好用的markdown工具等等等</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/a8ae56f0d9d9163a0a428480047d0182.png" alt="截图" /></p>
<p>〉 后记：已换mac，诶嘿嘿</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/21/%e7%86%9f%e7%bb%83%e6%8e%8c%e6%8f%a1windows%e7%8e%af%e5%a2%83%e4%b8%8b%e7%9a%84%e5%bc%80%e5%8f%91/">&#8220;熟练掌握Windows环境下的开发&#8221;</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/04/21/%e7%86%9f%e7%bb%83%e6%8e%8c%e6%8f%a1windows%e7%8e%af%e5%a2%83%e4%b8%8b%e7%9a%84%e5%bc%80%e5%8f%91/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>JetBrains系列产品高效食用指北</title>
		<link>https://www.crazyfay.com/2023/04/20/jetbrains%e7%b3%bb%e5%88%97%e4%ba%a7%e5%93%81%e9%ab%98%e6%95%88%e9%a3%9f%e7%94%a8%e6%8c%87%e5%8c%97/</link>
					<comments>https://www.crazyfay.com/2023/04/20/jetbrains%e7%b3%bb%e5%88%97%e4%ba%a7%e5%93%81%e9%ab%98%e6%95%88%e9%a3%9f%e7%94%a8%e6%8c%87%e5%8c%97/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Thu, 20 Apr 2023 05:35:36 +0000</pubDate>
				<category><![CDATA[实践经验]]></category>
		<category><![CDATA[效率工具]]></category>
		<guid isPermaLink="false">https://www.crazyfay.com/?p=269</guid>

					<description><![CDATA[<p>从我高考完的暑假第一次学习python开始，就使用JetBrains产品Pycharm开始了我的编程之旅，再后 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/20/jetbrains%e7%b3%bb%e5%88%97%e4%ba%a7%e5%93%81%e9%ab%98%e6%95%88%e9%a3%9f%e7%94%a8%e6%8c%87%e5%8c%97/">JetBrains系列产品高效食用指北</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>从我高考完的暑假第一次学习python开始，就使用JetBrains产品Pycharm开始了我的编程之旅，再后面的IDEA、Goland也都是我最主要的学习与生产力工具，发现我身边的同学很多也都在用JetBrains的产品，但是大多数的使用仅限于作为带有代码高亮与代码提示的大号编辑器来用。于是就有了今天的这篇文章。<br />
JetBrain产品的功能与快捷键基本都大同小异，本篇文章会以Goland为例，分享一下我平时在coding时那些很有的功能与快捷键。</p>
<h2>快捷键</h2>
<p><code>ctrl + shift + F</code> 全局搜索<br />
Goland的全局搜索很给力，用起来也很舒服，<strong>但是</strong>，如果你windows系统，记得一定一定一定要关掉输入法默认的<strong>繁体字切换</strong>的快捷键！否则你将永远无法用快捷键来点开全局搜索！</p>
<blockquote>
<p>我当初踩了这个坑还以为是JetBrains的bug，导致我用了好久好久的grep来进行全局搜索 XD</p>
</blockquote>
<p><code>ctrl + </code> 快速切换模式（背景、快捷键模式）<br />
<code>ctrl + -</code> 收起括号里的内容<br />
<code>ctrl + +</code> 展开括号里的内容</p>
<p><code>ctrl + Q</code> 查看函数的参数<br />
<code>ctrl + W</code> 扩选<br />
<code>ctrl + E</code> 打开最近的文件<br />
<code>ctrl + R</code> 在文件内替换<br />
<code>ctrl + T</code> update project<br />
<code>ctrl + Y</code> 删除本行(不是还原)<br />
<code>ctrl + U</code> 未知<br />
<code>ctrl + I</code> 实现接口<br />
<code>ctrl + O</code> 未知<br />
<code>ctrl + P</code> 未知<br />
<code>ctrl + [/]</code> 向上下找代码块的括号</p>
<p><code>ctrl + A</code> 全选<br />
<code>ctrl + S</code> 保存<br />
<code>ctrl + D</code> 复制到下一行<br />
<code>ctrl + F</code> 页内搜索<br />
<code>ctrl + G</code> 跳转到某行某列<br />
<code>ctrl + H</code> 未知<br />
<code>ctrl + J</code> 生成模板代码<br />
<code>ctrl + K</code> commit<br />
<code>ctrl + L</code> 未知</p>
<p><code>ctrl + Z</code> 撤销<br />
<code>ctrl + X</code> 剪切<br />
<code>ctrl + C</code> 复制<br />
<code>ctrl + V</code> 粘贴<br />
<code>ctrl + B</code> 查看引用信息<br />
<code>ctrl + N</code> 搜索各种东西<br />
<code>ctrl + M</code> 移动视图？</p>
<p><code>ctrl + B/鼠标左键</code> 查看引用信息<br />
<code>ctrl + alt + B</code> 查看接口实现列表<br />
<code>ctrl + 空格</code> 提示，推荐改键<br />
<code>ctrl + tab</code> switcher 个人主要用来快速打开终端<br />
<code>ctrl + /</code> （取消）注释全行<br />
<code>ctrl + shift + ↑/↓</code> 移动本行代码到上一行<br />
<code>alt + ↑/↓</code> 跳转到当前函数/上（下）一个函数<br />
<code>alt + ←/→</code> 切换到左边的窗口/右边的窗口 （终端可用）<br />
<code>ctrl + shift + ←/→</code> 跳转到上一次跳转的位置（DFS式读代码神器）</p>
<p><code>ctrl + 左键（点击文件）</code> 选择跳转目录<br />
<code>双击shift </code>搜索类型、文件</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/04/20/jetbrains%e7%b3%bb%e5%88%97%e4%ba%a7%e5%93%81%e9%ab%98%e6%95%88%e9%a3%9f%e7%94%a8%e6%8c%87%e5%8c%97/">JetBrains系列产品高效食用指北</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/04/20/jetbrains%e7%b3%bb%e5%88%97%e4%ba%a7%e5%93%81%e9%ab%98%e6%95%88%e9%a3%9f%e7%94%a8%e6%8c%87%e5%8c%97/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CFC-Golang 开发规范</title>
		<link>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/</link>
					<comments>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Thu, 16 Feb 2023 02:36:55 +0000</pubDate>
				<category><![CDATA[实践经验]]></category>
		<category><![CDATA[CFC]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[开发规范]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=19</guid>

					<description><![CDATA[<p>CFC-Golang 开发规范 注：此开发规范整合了部分网络上有价值的参考意见和开发实践中的总结 Github [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/">CFC-Golang 开发规范</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<h1>CFC-Golang 开发规范</h1>
<blockquote>
<p>注：此开发规范整合了部分网络上有价值的参考意见和开发实践中的总结<br />
Github地址：<a href="https://github.com/Kirov7/cfc-golang-develop-norms">https://github.com/Kirov7/cfc-golang-develop-norms</a></p>
</blockquote>
<h2>为什么需要编程规范？</h2>
<p>编程规范又叫代码规范，是团队之间在程序开发时需要遵守的约定。俗话说，无规矩不成方圆，一个开发团队应该就一种编程规范达成一致。编程规范有很多好处，我们简单说几个最主要的。</p>
<ul>
<li>促进团队合作</li>
</ul>
<p>现代项目大多是由团队完成的，但是如果每个人书写出的代码风格迥异，最后集成代码时很容易杂乱无章、可读性极差。相反，风格统一的代码将大大提高可读性，易于理解，促进团队协作。</p>
<ul>
<li>规避错误</li>
</ul>
<p>每一种语言都有容易犯的错误，Go语言也不例外。但是编码规范可以规避掉像Map并发读写等问题。不仅如此，规范的日志处理、错误处理还能够加快我们查找问题的速度。</p>
<ul>
<li>提升性能</li>
</ul>
<p>优秀的开发者，能够在头脑中想象出不同程序运行的过程和结果，写出高性能的程序非常考验开发者的内功。但每个人的水平都有差异，这一点并不可控。但是如果我们将高性能编程的常见手段归纳整理出来，开发者只需要遵守这些简单的规则，就能够规避性能陷阱、极大提升程序性能。</p>
<ul>
<li>便于维护</li>
</ul>
<p>我们习惯于关注编写代码的成本，但实际上维护代码的成本要高得多。大部分的项目是在前人的基础上完成开发的。我们在开发代码的时候，也会花大量时间阅读之前的代码。符合规范的代码更容易上手维护、更少出现牵一发动全身的耦合现象、也更容易看出业务处理逻辑。</p>
<p>知道了编程规范的好处，那我们应该规范什么内容呢？这其实涉及到我们对好代码的定义。针对这个问题，每个人都能够说个几句。但总体来说，好的代码首先是整洁、一致的，同时它还是高效、健壮和可扩展的。</p>
<p>有一些规范可以是强制的，因为我们可以通过工具和代码review强制要求用户遵守，还有一些规范是建议的，因为它更具有灵活性，很难被约束。在后面的规范中，[强制 xxx]中的“xxx”代表的就是可强制检查的工具。</p>
<h2>整洁、一致</h2>
<p>好代码的第一个要求，是整洁和一致。有一句话是这样说的：</p>
<blockquote>
<p>Any fool can write code that a computer can understand. Good programmers write code that humans can understand.</p>
</blockquote>
<p>它的意思是，任何傻瓜都可以编写计算机可以理解的代码，而优秀的程序员编写的是人类可以理解的代码。</p>
<p>如果我们的代码看起来乱七八糟，就像喝醉的人写的那样，这样不严谨的代码让我们有理由相信，项目的其他各个方面也隐藏着对细节的疏忽，并埋下了重大的隐患。</p>
<p>阅读整洁的代码就像看到精心设计的手表或汽车一样赏心悦目，因为它凝聚了团队的智慧。</p>
<p>阅读整洁的代码也像读到的武侠小说，书中的文字被脑中的图像取代，你看到了角色，听到了声音，体验到了悲怆和幽默。</p>
<p>但是，明白什么是整洁的代码并不意味着你能写出整洁的代码。就好像我们知道如何欣赏一幅画不意味着我们能成为画家。</p>
<p>整洁的代码包括对格式化、命名、函数等细节的密切关注，更需要在项目中具体实践。接下来我们就来看看整洁代码关注的这些细节和最佳的实践。</p>
<h3><strong>格式化</strong></h3>
<p><strong>代码长度</strong></p>
<p>代码应该有足够的垂直密度，能够肉眼一次获得到更多的信息。同时，单个函数、单行、单文件也需要限制长度，保证可阅读性和可维护性。</p>
<p>[强制 lll] 一行内不超过 120 个字符，同时应当避免刻意断行。如果你发现某一行太长了，要么改名，要么调整语义，往往就可以解决问题了。</p>
<p>[强制 funlen] 单个函数的行数不超过 40 行，过长则表示函数功能不专一、定义不明确、程序结构不合理，不易于理解。当函数过长时，可以提取函数以保持正文小且易读。</p>
<p>[强制] 单个文件不超过 2000 行，过长说明定义不明确，程序结构划分不合理，不利于维护。</p>
<p><strong>代码布局</strong></p>
<p>我们先试想一篇写得很好的报纸文章。在顶部，你希望有一个标题，它会告诉你故事的大致内容，并影响你是否要阅读它。文章的第一段会为你提供整个故事的概要、粗略的概念，但是隐藏了所有细节。继续向下阅读，详细信息会逐步增加。</p>
<p>[建议]  Go 文件推荐按以下顺序进行布局。</p>
<ol>
<li>
<p>包注释：对整个模块和功能的完整描述，写在文件头部</p>
</li>
<li>
<p>Package：包名称</p>
</li>
<li>
<p>Imports：引入的包</p>
</li>
<li>
<p>Constants：常量定义</p>
</li>
<li>
<p>Typedefs：类型定义</p>
</li>
<li>
<p>Globals：全局变量定义</p>
</li>
<li>
<p>Functions：函数实现</p>
</li>
</ol>
<p>每个部分之间用一个空行分割。每个部分有多个类型定义或者有多个函数时，也用一个空行分割。示例如下：</p>
<pre><code class="language-go">/*
注释
*/
package http

import (
 &quot;fmt&quot;
 &quot;time&quot;
)

const (
 VERSION = &quot;1.0.0&quot;
)
type Request struct{
}

var msg = &quot;HTTP success&quot;

func foo() {
 //...
}</code></pre>
<p>[强制 goimports] 当 import 多个包时，应该对包进行分组。同一组的包之间不需要有空行，不同组之间的包需要一个空行。标准库的包应该放在第一组。</p>
<p><strong>空格与缩进</strong></p>
<p>为了让阅读代码时视线畅通，自上而下思路不被打断，我们需要使用一些空格和缩进。</p>
<p>空格是为了分离关注点，将不同的组件分开。缩进是为了处理错误和边缘情况，与正常的代码分隔开。</p>
<p>较常用的有下面这些规范：</p>
<p>[强制 gofmt] 注释和声明应该对齐。示例如下：</p>
<pre><code class="language-go">type T struct {
    name    string // name of the object
    value   int    // its value
}</code></pre>
<p>[强制 gofmt] 小括号()、中括号[]、大括号{} 内侧都不加空格。</p>
<p>[强制 gofmt] 逗号、冒号（slice中冒号除外）前都不加空格，后面加 1 个空格。</p>
<p>[强制 gofmt] 所有二元运算符前后各加一个空格，作为函数参数时除外。例如<code>b := 1 + 2</code>。[强制 gofmt] 使用 Tab 而不是空格进行缩进。</p>
<p>[强制 nlreturn] return前方需要加一个空行，让代码逻辑更清晰。</p>
<p>[强制 gofmt] 判断语句、for语句需要缩进1个 Tab，并且右大括号<code>}</code>与对应的 if 关键字垂直对齐。例如：</p>
<pre><code class="language-go">if xxx {

} else {

}</code></pre>
<p>[强制 goimports] 当 import 多个包时，应该对包进行分组。同一组的包之间不需要有空行，不同组之间的包需要一个空行。标准库的包应该放在第一组。这同样适用于常量、变量和类型声明：</p>
<pre><code class="language-go">import (
    &quot;fmt&quot;
    &quot;hash/adler32&quot;
    &quot;os&quot;

    &quot;appengine/foo&quot;
    &quot;appengine/user&quot;
    &quot;github.com/foo/bar&quot;
    &quot;rsc.io/goversion/version&quot;
)</code></pre>
<p>[推荐] 避免 else 语句中处理错误返回，避免正常的逻辑位于缩进中。如下代码实例，else中进行错误处理，代码逻辑阅读起来比较费劲。</p>
<pre><code class="language-go">if something.OK() {
        something.Lock()
        defer something.Unlock()
        err := something.Do()
        if err == nil {
                stop := StartTimer()
                defer stop()
                log.Println(&quot;working...&quot;)
                doWork(something)
                &lt;-something.Done() // wait for it
                log.Println(&quot;finished&quot;)
                return nil
        } else {
                return err
        }
} else {
        return errors.New(&quot;something not ok&quot;)
}</code></pre>
<p>如果把上面的代码修改成下面这样会更加清晰：</p>
<pre><code class="language-go">if !something.OK() {
    return errors.New(&quot;something not ok&quot;)
}
something.Lock()
defer something.Unlock()
err := something.Do()
if err != nil {
    return err
}
stop := StartTimer()
defer stop()
log.Println(&quot;working...&quot;)
doWork(something)
&lt;-something.Done() // wait for it
log.Println(&quot;finished&quot;)
return nil</code></pre>
<p>[推荐] 函数内不同的业务逻辑处理建议用单个空行加以分割。</p>
<p>[推荐] 注释之前的空行通常有助于提高可读性——新注释的引入表明新思想的开始。</p>
<h3><strong>命名</strong></h3>
<blockquote>
<p>Good naming is like a good joke. If you have to explain it, it’s not funny.<br />
———Dave Cheney</p>
</blockquote>
<p>一个好的名字应该满足几个要素：</p>
<ul>
<li>短，容易拼写；</li>
<li>保持一致性；</li>
<li>意思准确，容易理解，没有虚假和无意义的信息。</li>
</ul>
<p>例如，像下面这样的命名就是让人迷惑的：</p>
<pre><code class="language-go">int d; // elapsed time in days</code></pre>
<p>[强制 revive] Go中的命名统一使用驼峰式、不要加下划线。</p>
<p>[强制 revive] 缩写的专有名词应该大写，例如： ServeHTTP、IDProcessor。</p>
<p>[强制] 区分变量名应该用有意义的名字，而不是使用阿拉伯数字：a1, a2, .. aN。</p>
<p>[强制] 不要在变量名称中包含你的类型名称。</p>
<p>[建议]变量的作用域越大，名字应该越长。</p>
<p>现代 IDE 已经让更改名称变得更容易了，巧妙地使用IDE的功能，能够级联地同时修改多处命名。</p>
<p><strong>包名</strong></p>
<p>包名应该简短而清晰。</p>
<p>[强制] 使用简短的小写字母，不需要下划线或混合大写字母。</p>
<p>[建议]  合理使用缩写，例如：</p>
<pre><code>strconv（字符串转换）
syscall（系统调用）
fmt（格式化的 I/O）</code></pre>
<p>[强制] 避免无意义的包名，例如<code>util</code>,<code>common,base</code>等。</p>
<p><strong>接口命名</strong></p>
<p>[建议]单方法接口由方法名称加上 -er 后缀或类似修饰来命名。例如：<code>Reader</code>, <code>Writer</code>, <code>Formatter</code>, <code>CloseNotifier</code> ，当一个接口包含多个方法时，请选择一个能够准确描述其用途的名称（例如：net.Conn、http.ResponseWriter、io.ReadWriter）。</p>
<p><strong>本地变量命名</strong></p>
<p>[建议]尽可能地短。在这里，i 指代 index，r 指代 reader，b 指代 buffer。</p>
<p>例如，下面这段代码就可以做一个简化：</p>
<pre><code class="language-go">for index := 0; index &lt; len(s); index++ {
    //
}</code></pre>
<p>可以替换为：</p>
<pre><code class="language-go">for i := 0; i &lt; len(s); i++ {
    //
}</code></pre>
<p><strong>函数参数命名</strong></p>
<p>[建议]如果函数参数的类型已经能够看出参数的含义，那么函数参数的命名应该尽量简短：</p>
<pre><code class="language-go">func AfterFunc(d Duration, f func()) *Timer
func Escape(w io.Writer, s []byte)</code></pre>
<p>[建议]如果函数参数的类型不能表达参数的含义，那么函数参数的命名应该尽量准确：</p>
<pre><code class="language-go">func Unix(sec, nsec int64) Time
func HasPrefix(s, prefix []byte) bool</code></pre>
<p><strong>函数返回值命名</strong></p>
<p>[建议] 对于公开的函数，返回值具有文档意义，应该准确表达含义，如下所示：</p>
<pre><code class="language-go">func Copy(dst Writer, src Reader) (written int64, err error)

func ScanBytes(data []byte, atEOF bool) (advance int, token []byte, err error)</code></pre>
<p><strong>可导出的变量名</strong></p>
<p>[建议] 由于使用可导出的变量时会带上它所在的包名，因此，不需要对变量重复命名。例如bytes包中的ByteBuffer替换为Buffer，这样在使用时就是bytes.Buffer，显得更简洁。类似的还有把strings.StringReader修改为strings.Reader，把<strong>errors.NewError 修改为errors.New。</strong></p>
<p><strong>Error值命名</strong></p>
<p>[建议] 错误类型应该以Error结尾。</p>
<p>[建议] Error变量名应该以Err开头。</p>
<pre><code class="language-go">type ExitError struct {
    ...
}
var ErrFormat = errors.New(&quot;image: unknown format&quot;)</code></pre>
<h3><strong>函数</strong></h3>
<p>[强制 cyclop] 圈复杂度（Cyclomatic complexity）&lt;10。</p>
<p>[强制 gochecknoinits] 避免使用init函数。</p>
<p>[强制 revive] Context 应该作为函数的第一个参数。</p>
<p>[强制] 正常情况下禁用unsafe。</p>
<p>[强制] 禁止return裸返回，如下例中第一个return：</p>
<pre><code class="language-go">func (f *Filter) Open(name string) (file File, err error) {
    for _, c := range f.chain {
        file, err = c.Open(name)
        if err != nil {
            return
        }
    }
    return f.source.Open(name)
}</code></pre>
<p>[强制] 不要在循环里面使用defer，除非你真的确定defer的执行流程。</p>
<p>[强制] 对于通过:=进行变量赋值的场景，禁止出现仅部分变量初始化的情况。例如在下面这个例子中，f函数返回的res是初始化的变量，但是函数返回的err其实复用了之前的err：</p>
<pre><code class="language-go">var err error
res,err := f()</code></pre>
<p>[建议] 函数返回值大于 3 个时，建议通过 struct 进行包装。</p>
<p>[建议] 函数参数不建议超过 3 个，大于 3 个时建议通过 struct 进行包装。</p>
<h3>控制结构</h3>
<p>[强制] 禁止使用goto。</p>
<p>[强制 gosimple] 当一个表达式为 bool 类型时，应该使用 expr 或 !expr 判断，禁止使用 == 或 != 与 true / false 比较。</p>
<p>[强制 nestif] if 嵌套深度不大于5。</p>
<h3><strong>方法</strong></h3>
<p>[强制 revive] receiver 的命名要保持一致，如果你在一个方法中将接收器命名为 &quot;c&quot;，那么在其他方法中不要把它命名为 &quot;cl&quot;。</p>
<p>[强制] receiver 的名字要尽量简短并有意义，禁止使用 this、self 等。</p>
<pre><code class="language-go">func (c Client) done() error {
 // ...
}
func (cl Client) call() error {
 // ...
}</code></pre>
<h3>注释</h3>
<p>Go提供C风格的注释。有/**/ 的块注释和 // 的单行注释两种注释风格。注释主要有下面几个用处。</p>
<ol>
<li>注释不仅仅可以提供具体的逻辑细节，还可以提供代码背后的意图和决策。</li>
<li>帮助澄清一些晦涩的参数或返回值的含义。一般来说，我们会尽量找到一种方法让参数或返回值的名字本身就是清晰的。但是当它是标准库的一部分时，或者在你无法更改的第三方库中，一个清晰的注释会非常有用。</li>
<li>强调某一个重要的功能。例如，提醒开发者修改了这一处代码必须连带修改另一处代码。</li>
</ol>
<p>总之，好的注释给我们讲解了what、how、why，方便后续的代码维护。</p>
<p>[强制] 无用注释直接删除，无用的代码不应该注释而应该直接删除。即使日后需要，我们也可以通过Git快速找到。</p>
<p>[强制] 使用行注释而不是尾注释，注释一律写在所描述内容的上一行。</p>
<p>[强制] 统一使用中文注释，中英文字符之间严格使用空格分隔。</p>
<pre><code class="language-go">// 从 Redis 中批量读取属性，对于没有读取到的 id ， 记录到一个数组里面，准备从 DB 中读取</code></pre>
<p>[强制] 注释不需要额外的格式，例如星号横幅。</p>
<p>[强制] 包、函数、方法和类型的注释说明都是一个完整的句子，以被描述的对象为主语开头。Go源码中都是这样的。</p>
<p>示例如下：</p>
<pre><code class="language-go">// queueForIdleConn queues w to receive the next idle connection for w.cm.
// As an optimization hint to the caller, queueForIdleConn reports whether
// it successfully delivered an already-idle connection.
func (t *Transport) queueForIdleConn(w *wantConn) (delivered bool)</code></pre>
<p>[强制] Go语言提供了<a href="https://tip.golang.org/doc/comment">文档注释工具go doc</a>，可以生成注释和导出函数的文档。文档注释的写法可以参考文稿中的链接。</p>
<p>[强制 godot] 注释最后应该以句号结尾。</p>
<p>[建议] 当某个部分等待完成时，可用 <code>TODO:</code> 开头的注释来提醒维护人员。</p>
<p>[建议] 大部分情况下使用行注释。块注释主要用在包的注释上，不过块注释在表达式中或禁用大量代码时很有用。</p>
<p>[建议] 当某个部分存在已知问题需要修复或改进时，可用 <code>FIXME:</code> 开头的注释来提醒维护人员。</p>
<p>[建议] 需要特别说明某个问题时，可用 <code>NOTE:</code> 开头的注释。</p>
<h3>结构体</h3>
<p>[强制] 不要将 Context 成员添加到 Struct 类型中。</p>
<h3>Commit 规范</h3>
<p><strong>commit message格式</strong></p>
<pre><code class="language-text">&lt;type&gt;(&lt;scope&gt;): &lt;subject&gt;</code></pre>
<p>[强制] <strong>type(必须)</strong></p>
<p>用于说明git commit的类别，只允许使用下面的标识。</p>
<p>feat：新功能（feature）。</p>
<p>fix/to：修复bug，可以是QA发现的BUG，也可以是研发自己发现的BUG。</p>
<ul>
<li>fix：产生diff并自动修复此问题。适合于一次提交直接修复问题</li>
<li>to：只产生diff不自动修复此问题。适合于多次提交。最终修复问题提交时使用fix</li>
</ul>
<p>docs：文档（documentation）。</p>
<p>style：格式（不影响代码运行的变动）。</p>
<p>refactor：重构（即不是新增功能，也不是修改bug的代码变动）。</p>
<p>perf：优化相关，比如提升性能、体验。</p>
<p>test：增加测试。</p>
<p>chore：构建过程或辅助工具的变动。</p>
<p>revert：回滚到上一个版本。</p>
<p>merge：代码合并。</p>
<p>sync：同步主线或分支的Bug。</p>
<p>[建议] <strong>scope(可选)</strong></p>
<p>scope用于说明 commit 影响的范围，比如数据层、控制层、视图层等等，视项目不同而不同。</p>
<p>例如在Angular，可以是location，browser，compile，compile，rootScope， ngHref，ngClick，ngView等。如果你的修改影响了不止一个scope，你可以使用*代替。</p>
<p>[强制] <strong>subject(必须)</strong></p>
<p>subject是commit目的的简短描述，不超过50个字符。</p>
<p>建议使用中文（感觉中国人用中文描述问题能更清楚一些）。</p>
<ul>
<li>结尾不加句号或其他标点符号。</li>
<li>根据以上规范git commit message将是如下的格式：</li>
</ul>
<pre><code class="language-text">fix(DAO):用户查询缺少username属性 
feat(Controller):用户查询接口开发</code></pre>
<blockquote>
<p>这样规范 git commit 有以下优点</p>
</blockquote>
<ul>
<li>便于开发者对提交历史进行追溯，了解发生了什么情况。</li>
<li>一旦约束了commit message，意味着我们将慎重的进行每一次提交，不能再一股脑的把各种各样的改动都放在一个git commit里面，这样一来整个代码改动的历史也将更加清晰。</li>
<li>格式化的commit message才可以用于自动化输出Change log。</li>
</ul>
<h2>高效</h2>
<p>[强制] Map在初始化时需要指定长度<code>make(map[T1]T2, hint)</code>。</p>
<p>[强制] Slice在初始化时需要指定长度和容量<code>make([]T, length, capacity)</code>。</p>
<p>我们来看下下面这段程序，它的目的是往切片中循环添加元素。</p>
<pre><code class="language-go">func createSlice(n int) (slice []string) {
   for i := 0; i &lt; n; i++ {
      slice = append(slice, &quot;I&quot;, &quot;love&quot;, &quot;go&quot;)
   }
   return slice
}</code></pre>
<p>从功能上来看，这段代码没有问题。但是，这种写法忽略了一个事实，如下图所示，往切片中添加数据时，切片会自动扩容，Go运行时会创建新的内存空间并执行拷贝。</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20221124144357089.png" alt="image-20221124144357089" /></p>
<p>自动扩容显然是有成本的。在循环操作中执行这样的代码会放大性能损失，减慢程序的运行速度。性能损失的对比可参考<a href="https://github.com/uber-go/guide/blob/master/style.md#prefer-specifying-container-capacity">这篇文章</a>。我们可以改写一下上面这段程序，在初始化时指定合适的切片容量：</p>
<pre><code class="language-go">func createSlice(n int) []string {
   slice := make([]string, 0, n*3)
   for i := 0; i &lt; n; i++ {
      slice = append(slice, &quot;I&quot;, &quot;love&quot;, &quot;go&quot;)
   }
   return slice
}</code></pre>
<p>这段代码在一开始就指定了需要的容量，最大程度避免了内存的浪费。同时，运行时不需要再执行自动扩容操作，加速了程序的运行。</p>
<p>[强制]  time.After()在某些情况下会发生泄露，替换为使用Timer。</p>
<p>[强制] 数字与字符串转换时，<a href="https://gist.github.com/evalphobia/caee1602969a640a4530">使用strconv，而不是fmt</a>。</p>
<p>[强制] 读写磁盘时，使用<a href="https://www.instana.com/blog/practical-golang-benchmarks/#file-i-o">读写buffer</a>。</p>
<p>[建议] 谨慎使用Slice的截断操作和append操作，除非你知道下面的代码输出什么：</p>
<pre><code class="language-go">x := []int{1, 2, 3, 4}
y := x[:2]
fmt.Println(cap(x), cap(y))
y = append(y, 30)
fmt.Println(&quot;x:&quot;, x)
fmt.Println(&quot;y:&quot;, y)</code></pre>
<p>[建议] 任何书写的协程，都需要明确协程什么时候退出。</p>
<p>[建议] 热点代码中，内存分配复用内存可以使用 sync.Pool <a href="https://www.instana.com/blog/practical-golang-benchmarks/#object-creation">提速</a>。</p>
<p>[建议] 将频繁的字符串拼接操作（+=），替换为<strong>StringBuffer 或 StringBuilder。</strong></p>
<p>[建议] 使用正则表达式重复匹配时，利用Compile提前编译<a href="https://www.instana.com/blog/practical-golang-benchmarks/#regular-expressions">提速</a>。</p>
<p>[建议]  当程序严重依赖Map时，Map的Key使用int而不是string将<a href="https://www.instana.com/blog/practical-golang-benchmarks/#map-access">提速</a>。</p>
<p>[建议]  多读少写的场景，使用读写锁而不是写锁将提速。</p>
<h2>健壮性</h2>
<p>[强制] 除非出现不可恢复的程序错误，否则不要使用 panic 来处理常规错误，使用 error 和多返回值。</p>
<p>[强制] 永远只在 main 函数和 init 函数中调用 log.Fatal() 方法。</p>
<p>[强制] 永远先关闭 写channel 再关闭 读channel，否则对已关闭 写channel 进行写入时会引发 panic 。</p>
<p>[强制 <a href="https://revive.run/r#error-strings">revive</a>] 错误信息不应该首字母大写（除专有名词和缩写词外），也不应该以标点符号结束。因为错误信息通常在其他上下文中被打印。</p>
<p>[强制 <a href="https://golangci-lint.run/usage/linters/#errcheck">errcheck</a>] 不要使用 _ 变量来丢弃 error。如果函数返回 error，应该强制检查。</p>
<p>[强制] 在 Release模式 使用 context 的时候需要设定超时时间。</p>
<p>[建议] 在处理错误时，如果我们逐层返回相同的错误，那么在最后日志打印时，我们并不知道代码中间的执行路径。例如找不到文件时打印的<code>No such file or directory</code>，这会减慢我们排查问题的速度。因此，在中间处理err时，需要使用fmt.Errorf 或<a href="https://godoc.org/github.com/pkg/errors">第三方包</a>给错误添加额外的上下文信息。像下面这个例子，在fmt.Errorf中，除了实际报错的信息，还加上了授权错误信息<code>authenticate failed</code> ：</p>
<pre><code class="language-go">func AuthenticateRequest(r *Request) error {
        err := authenticate(r.User)
        if err != nil {
                return fmt.Errorf(&quot;authenticate failed: %v&quot;, err)
        }
        return nil
}</code></pre>
<p>当有多个错误需要处理时，可以考虑将fmt.Errorf放入defer中：</p>
<pre><code class="language-go">func DoSomeThings(val1 int, val2 string) (_ string, err error) {
    defer func() {
        if err != nil {
            err = fmt.Errorf(&quot;in DoSomeThings: %w&quot;, err)
        }
    }()
    val3, err := doThing1(val1)
    if err != nil {
        return &quot;&quot;, err
    }
    val4, err := doThing2(val2)
    if err != nil {
        return &quot;&quot;, err
    }
    return doThing3(val3, val4)
}</code></pre>
<p>[强制] 利用recover捕获panic时，需要由defer函数直接调用。</p>
<p>例如，下面例子中的panic是可以被捕获的：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer printRecover()

    panic(&quot;OMG!&quot;)
}</code></pre>
<p>但是下面这个例子中的panic却不能被捕获：</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer func() {
        printRecover()
    }()

    panic(&quot;OMG!&quot;)
}</code></pre>
<p>[强制] 不用重复使用recover，只需要在每一个协程的最上层函数拦截即可。recover只能够捕获当前协程，而不能跨协程捕获panic，下例中的panic就是无法被捕获的。</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func printRecover() {
    r := recover()
    fmt.Println(&quot;Recovered:&quot;, r)
}

func main() {
    defer printRecover()
    go func() {
        panic(&quot;OMG!&quot;)
    }()
    // ...
}</code></pre>
<p>[强制] 有些特殊的错误是recover不住的，例如Map的并发读写冲突。这种错误可以通过race工具来检查。</p>
<h2>扩展性</h2>
<p>[建议] 利用接口实现扩展性。接口特别适用于访问外部组件的情况，例如访问数据库、访问下游服务。另外，接口可以方便我们进行功能测试。关于接口的最佳实践，需要单独论述。</p>
<p>[建议] 使用功能选项模式(options 模式)对一些公共API的构造函数进行扩展，大量第三方库例如gomicro、zap等都使用了这种策略。</p>
<pre><code class="language-go">db.Open(addr, db.DefaultCache, zap.NewNop())
可以替换为=&gt;
db.Open(
    addr,
    db.WithCache(false),
    db.WithLogger(log),
)</code></pre>
<h2>内部实践</h2>
<h3>Gin</h3>
<h4>项目启动</h4>
<p>[强制] 在大型的项目开发中，采用优雅服务启停设计，采用 httpServer.ListenAndServer() 方法进行web服务的启动，而不是 Gin 框架的方法，web服务需额外启动一个 goroutine 来运行，通过信号监听的方式控制结束程序。</p>
<pre><code class="language-golang">func main() {
    r := gin.Default()
    srv := &amp;http.Server{
        Addr:    &quot;:80&quot;,
        Handler: r,
    }

    // 优雅的启停
    go func() {
        log.Printf(&quot;web server running in %s \n&quot;, srv.Addr)
        if err := srv.ListenAndServe(); err != nil &amp;&amp; err != http.ErrServerClosed {
            log.Fatalln(err)
        }
    }()

    quit := make(chan os.Signal)
    // SIGINT 用户发送 INTR 字符(Ctrl + C)触发 kill -2
    // SIGTERM 结束程序 (可以被捕获、阻塞或忽略)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    &lt;-quit
    log.Println(&quot;Shutting Down project Web server...&quot;)

    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()
    if err := srv.Shutdown(ctx); err != nil {
        log.Fatalln(&quot;Web server shutdown, cause by : &quot;, err)
    }
    select {
    case &lt;-ctx.Done():
        log.Println(&quot;wait timeout...&quot;)
    }
    log.Println(&quot;Web Server stop success...&quot;)
}</code></pre>
<blockquote>
<p>以上代码可进一步封装，以保证 main 的代码结构清晰</p>
</blockquote>
<h4>返回值格式</h4>
<p>[强制] json格式的返回值，code部分统一采用 net/http 包中的 http.StatusOK，obj 则为业务对象的统一格式封装。</p>
<pre><code class="language-golang">ctx.JSON(http.StatusOK, result.Success(list))
ctx.JSON(http.StatusOK, result.Fail(http.StatusBadRequest, &quot;参数错误&quot;))</code></pre>
<p>[强制] 业务对象中的统一字段为：code、msg、data，其中code为错误代码，由具体的服务实现方定义，需要保证全局唯一，如果没有错误则固定为 200 ，msg为错误信息，由具体的服务实现方定义，需要简洁的描述错误的原因，错误可统一定义为全局变量，当在api层产生参数错误、数据校验失败等请求值错误，则使用 http.StatusBadRequest 作为错误码， data 则为前端所需要的业务对象，推荐使用在api服务中封装返回值结构体实例。</p>
<p>对应结构体实例：</p>
<pre><code class="language-golang">type BusinessCode int
type Result struct {
    Code BusinessCode `json:&quot;code&quot;`
    Msg  string       `json:&quot;msg&quot;`
    Data any          `json:&quot;data&quot;`
}</code></pre>
<p>[强制] 在给前端返回 json 后必须 return 。</p>
<h3>grpc</h3>
<h4>proto文件与命名规范</h4>
<p>[强制] proto文件中的 service、rpc Function、message 以大驼峰的方式命名。</p>
<p>[推荐] 尽可能的将 proto 文件中 service 命名为 xxxService 、 RPC 方法的入参命名为 XxxRequest，出参命名为 XxxResponse。</p>
<p>[强制] 将引用的生成 rpc 包重命名为 xxxRpc 以避免module中的本地模块产生冲突。</p>
<pre><code class="language-go">import(
    departmentRpc &quot;github.com/Kirov7/project-grpc/project/department&quot;   
)</code></pre>
<p>[推荐] 编译 proto 文件时推荐不要直接生成到会被其他模块引用的包下，建议生成到指定目录后手动迁移文件。</p>
<h4>注册grpc服务</h4>
<p>[建议] 优雅的启动 grpcServer 示例，其中 RegisterGrpc() 返回的 grpc.Server 可供上层调用 grpcSever.Stop() 等，可对 grpcServer 生命周期进行管控。</p>
<pre><code class="language-golang">type gRPCConfig struct {
    Addr         string
    RegisterFunc func(server *grpc.Server)
}

func RegisterGrpc() *grpc.Server {
    c := gRPCConfig{
        Addr: config.AppConf.GrpcConfig.Addr,
        RegisterFunc: func(g *grpc.Server) {
            login.RegisterLoginServiceServer(g, loginServiceV1.NewLoginService())
      },
    }
    //cacheInterceptor := interceptor.NewCacheInterceptor()
    //s := grpc.NewServer(cacheInterceptor.CacheInterceptor())
    s := grpc.NewServer()
    c.RegisterFunc(s)
    listen, err := net.Listen(&quot;tcp&quot;, c.Addr)
    if err != nil {
        log.Printf(&quot;listen port %s fail\n&quot;, c.Addr)
    }
    go func() {
        log.Printf(&quot;grpc server started as %s \n&quot;, c.Addr)
        err = s.Serve(listen)
        if err != nil {
            log.Printf(&quot;server started error: %s\n&quot;, err)
            return
        }
    }()
    return s
}</code></pre>
<h3>项目结构核心部分示例</h3>
<p>此示例结合了部分DDD领域驱动设计的思想，但并非完全按照DDD的模式进行组织，旨在优化项目组织结构的同时，尽量避免过于复杂抽象的概念。</p>
<h4>main.go</h4>
<p>进行各类初始化操作，如初始化http服务、路由注册、初始化grpc客户端、grpc服务注册等。</p>
<h4>config 包</h4>
<p>内有 config.go 和 config.yaml 文件，config.go 中拥有包含配置的全局变量，<strong>推荐使用 viper 进行配置读取</strong>。</p>
<h4>internal 包</h4>
<p>internal包 是 golang 中特殊的一个包，它对外部module不可见，可用来存放数据操作等敏感内容。</p>
<p>可包含domain、repository、dao、rpc、data等包</p>
<ul>
<li>
<p>domain：领域服务，负责表达业务概念，业务状态信息以及业务规则，通过调用 repository 、rpc 或其他 domain 进行数据操作与数据整合，实现完整的某一领域模块的业务逻辑。在进行单元测试时可以绕过 service 直接对 domain 进行细粒度的测试，简化测试流程。</p>
</li>
<li>
<p>repository：仓库，负责封装数据的查询、创建、更新、删除等逻辑的接口，屏蔽底层实现，供使用者调用</p>
</li>
<li>
<p>dao：数据操作层，实现 repository 的接口，封装对MySQL、Redis等数据库操作的具体实现的。</p>
</li>
<li>
<p>rpc：rpc客户端，对其他rpc服务进行远程调用的客户端。</p>
</li>
<li>
<p>data：数据表的直接映射，并包含对于数据传输模型的转换。</p>
</li>
</ul>
<h4>pkg 包</h4>
<p>pkg包 包含了服务具体实现与其所依赖的预定义的常量与全局变量。</p>
<p>可包含model、service等包</p>
<ul>
<li>model：预定义的常量与全局变量，便于在业务中重用，简化操作逻辑。可包含预定于的 rediskey、业务所需的枚举值、预定义的错误等。</li>
<li>service：业务层，接受客户端传输的数据，对请求数据进行校验与转换，通过调用 domain 层进行业务操作，整合调用的结果值，返回给客户端。</li>
</ul>
<h2>工具</h2>
<p>要人工来保证团队成员遵守了上述的编程规范并不是一件容易的事情。因此，我们有许多静态的和动态的代码分析工具帮助团队识别代码规范的错误，甚至可以发现一些代码的bug。</p>
<h3>golangci-lint</h3>
<p>golangci-lint 是当前大多数公司采用的静态代码分析工具，词语Linter 指的是一种分析源代码以此标记编程错误、代码缺陷、风格错误的工具。</p>
<p>而golangci-lint是集合多种Linter的工具。要查看支持的 Linter 列表以及启用/禁用了哪些Linter，可以使用下面的命令：</p>
<pre><code>golangci-lint help linters</code></pre>
<p>Go语言定义了实现Linter的API，它还提供了golint工具，用于集成了几种常见的Linter。在<a href="https://cs.opensource.google/go/x/tools/+/refs/tags/v0.1.11:go/analysis/passes/unreachable/unreachable.go">源码</a>中，我们可以查看怎么在标准库中实现典型的Linter。</p>
<p>Linter的实现原理是静态扫描代码的AST（抽象语法树），Linter的标准化意味着我们可以灵活实现自己的Linters。不过golangci-lint里面其实已经集成了包括golint在内的总多Linter，并且有灵活的配置能力。所以在自己写Linter之前，建议先了解golangci-lint现有的能力。</p>
<p>在大型项目中刚开始使用golang-lint会出现大量的错误，这种情况下我们只希望扫描增量的代码。如下所示，可以通过在<a href="https://golangci-lint.run/usage/configuration/">golangci-lint配置文件</a>中调整new-from-rev参数，配置以当前基准分支为基础实现增量扫描</p>
<pre><code class="language-yaml">linters:
 enable-all: true
issues:
 new-from-rev: master</code></pre>
<h3>Pre-Commit</h3>
<p>在代码通过Git Commit提交到代码仓库之前，git 提供了一种pre-commit的hook能力，用于执行一些前置脚本。在脚本中加入检查的代码，就可以在本地拦截住一些不符合规范的代码，避免频繁触发CI或者浪费时间。pre-commit的配置和使用方法，可以参考<a href="https://github.com/pingcap/tidb/blob/master/hooks/pre-commit">TiDB</a>。</p>
<h3>并发检测 race</h3>
<p>Go 1.1 提供了强大的检查工具race来排查数据争用问题。race 可以用在多个Go指令中，一旦检测器在程序中找到数据争用，就会打印报告。这份报告包含发生race冲突的协程栈，以及此时正在运行的协程栈。可以在编译时和运行时执行race，方法如下：</p>
<pre><code class="language-shell">$ go test -race mypkg
$ go run -race mysrc.go
$ go build -race mycmd
$ go install -race mypkg</code></pre>
<p>在下面这个例子中， 运行中加入race检查后直接报错。从报错后输出的栈帧信息中，我们能看出具体发生并发冲突的位置。</p>
<pre><code class="language-shell">» go run -race 2_race.go
==================
WARNING: DATA RACE
Read at 0x00000115c1f8 by goroutine 7:
    main.add()
        bookcode/concurrence_control/2_race.go:5 +0x3a
Previous write at 0x00000115c1f8 by goroutine 6:
    main.add()
        bookcode/concurrence_control/2_race.go:5 +0x56</code></pre>
<p>第四行Read at 表明读取发生在2_race.go 文件的第5行，而第七行Previous write 表明前一个写入也发生在2_race.go 文件的第5行。这样我们就可以非常快速地定位数据争用问题了。</p>
<p>竞争检测的成本因程序而异。对于典型的程序，内存使用量可能增加 5~10 倍，执行时间会增加2~20倍。同时，竞争检测器会为当前每个defer和recover语句额外分配8字节。在Goroutine退出前，这些额外分配的字节不会被回收。这意味着，如果有一个长期运行的Goroutine，而且定期有defer 和recover调用，那么程序内存的使用量可能无限增长。（这些内存分配不会显示到 runtime.ReadMemStats或runtime / pprof 的输出。）</p>
<h3>覆盖率</h3>
<p>一般我们会使用代码覆盖率来判断代码书写的质量，识别无效代码。go tool cover 是go语言提供的识别代码覆盖率的工具。</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/">CFC-Golang 开发规范</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2023/02/16/cfc-golang-%e5%bc%80%e5%8f%91%e8%a7%84%e8%8c%83/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>记一次gorm.ErrRecordNotFound踩坑记录</title>
		<link>https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/</link>
					<comments>https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/#respond</comments>
		
		<dc:creator><![CDATA[crazyfay]]></dc:creator>
		<pubDate>Sat, 20 Aug 2022 03:05:31 +0000</pubDate>
				<category><![CDATA[实践经验]]></category>
		<category><![CDATA[Golang]]></category>
		<category><![CDATA[踩坑日记]]></category>
		<guid isPermaLink="false">http://net.crazyfay.xyz/?p=164</guid>

					<description><![CDATA[<p>在某个项目中，有个数据验证的业务，即在数据库中查询数据是否存在，若数据已存在则返回错误并给前端提示。稍想了一下 [&#8230;]</p>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/">记一次gorm.ErrRecordNotFound踩坑记录</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></description>
										<content:encoded><![CDATA[<p>在某个项目中，有个数据验证的业务，即在数据库中查询数据是否存在，若数据已存在则返回错误并给前端提示。稍想了一下就能写出如下代码<code>Func01</code></p>
<p><code>Func01</code></p>
<pre><code class="language-go">func (t *ServiceInfo) Find(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceInfo, error) {
    out := &amp;ServiceInfo{}
    err := tx.WithContext(c).Where(search).Find(out).Error
    if err != nil {
        return nil, err
    }
    return out, nil
}</code></pre>
<p>gorm在之前的版本中，因为gorm的查询是链式的语句，所以中间出现的错误会存入到Error的参数集中处理。而且当没有查询到数据的时候也会得到错误ErrRecordNotFound。所以此代码就把错误同一处理，当controller中没有收到任何错误时，可以说明数据库中查询到了此数据，即校验重复了。</p>
<p>但是经过测试后发现无论如何err都是nil，且RowsAffected也明明为0。在网上也没有直接搜到这个坑的blog，于是我去翻了gorm最新的文档，发现了此段话！</p>
<p><img decoding="async" src="https://img-1307890592.cos.ap-chengdu.myqcloud.com/typroa/image-20220725104008262.png" alt="image-20220725104008262" /></p>
<p>即说明Find()方法不会再得到ErrRecordNotFound的错误</p>
<p>于是我采用了First()进行了测试</p>
<p><code>Func02</code></p>
<pre><code class="language-go">func (t *ServiceInfo) Find(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceInfo, error) {
    out := &amp;ServiceInfo{}
    resultFind := tx.WithContext(c).Where(search).Find(out)
    resultFirst := tx.WithContext(c).Where(search).First(out)
    log.Print(&quot;Find() Err: &quot;, resultFind.Error, &quot;\tFind Rows Affected: &quot;, resultFind.RowsAffected)
    log.Print(&quot;First() Err: &quot;, resultFirst.Error, &quot;\tFind Rows Affected: &quot;, resultFirst.RowsAffected)
    err := resultFind.Error
    if err != nil {
        return nil, err
    }
    return out, nil
}</code></pre>
<p>得到输出</p>
<pre><code class="language-shell">2022/07/25 10:00:35 Find() Err: &lt;nil&gt;   Find Rows Affected: 0
2022/07/25 10:00:35 First() Err: record not found       Find Rows Affected: 0</code></pre>
<p>很明显，当查询不到结果的时候First()方法会返回ErrRecordNotFound，而Find()方法并不会</p>
<p>因此，若不改变代码原有的逻辑基础上，可以通过手动添加Error的方法来完成数据校验的工作</p>
<p><code>Func03</code></p>
<pre><code class="language-go">func (t *ServiceInfo) Find(c *gin.Context, tx *gorm.DB, search *ServiceInfo) (*ServiceInfo, error) {
    out := &amp;ServiceInfo{}
    resultFind := tx.WithContext(c).Where(search).Find(out)
    if resultFind.RowsAffected &lt; 1 {
        err := resultFind.AddError(gorm.ErrRecordNotFound)
        if err != nil {
            return nil, err
        }
    }
    err := resultFind.Error
    if err != nil {
        return nil, err
    }
    return out, nil
}</code></pre>
<p>即通过<code>resultFind.RowsAffected &lt; 1</code>来判断是否查询到数据，再通过 <code>resultFind.AddError(gorm.ErrRecordNotFound)</code> 手动添加</p>
<p>ErrRecordNotFound错误，藉此来完成在旧版本中存在的功能。</p>
<blockquote>
<p>PS：我个人并不是很理解为什么要取消Find()方法中的这个错误提示</p>
<p>此实现方式仅供参考，如有更漂亮的方法希望不吝赐教</p>
</blockquote>
<p><a rel="nofollow" href="https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/">记一次gorm.ErrRecordNotFound踩坑记录</a>最先出现在<a rel="nofollow" href="https://www.crazyfay.com">枫阿雨&#039;s blog</a>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://www.crazyfay.com/2022/08/20/%e8%ae%b0%e4%b8%80%e6%ac%a1gorm-errrecordnotfound%e8%b8%a9%e5%9d%91%e8%ae%b0%e5%bd%95/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
